@latimer-woods-tech/llm 0.3.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 — 2026-06-03
4
+
5
+ ### Added (no breaking changes)
6
+
7
+ - Export `MODEL_PRICE_PER_1M` — the canonical USD-per-1M-tokens rate table. This
8
+ makes it the single source of truth for pricing across the platform;
9
+ `@latimer-woods-tech/llm-meter` now derives its cents table from it and enforces
10
+ parity with a drift-guard test. Make all rate changes here.
11
+
12
+ ---
13
+
14
+ ## 0.3.4 — 2026-05-28
15
+
16
+ ### Added (no breaking changes)
17
+
18
+ - **`workbench` tier** routes to `deepseek-chat` with Groq fallback for boring,
19
+ reviewable, non-sensitive internal batch work.
20
+ - **`DEEPSEEK_API_KEY`** added as an optional `LLMEnv` binding. It is required only
21
+ for `tier: 'workbench'` or explicit `deepseek-*` model overrides.
22
+ - **DeepSeek pricing entries** for `deepseek-chat` and `deepseek-reasoner` so cost
23
+ caps and ledger rows use known rates instead of the conservative Opus fallback.
24
+
25
+ ### Guardrail
26
+
27
+ - `workbench` is for docs summaries, changelog drafts, issue triage, and classification.
28
+ Do not route secrets, customer PII, billing data, production ops, or final
29
+ customer-facing answers through DeepSeek.
30
+
31
+ ---
32
+
33
+ ## 0.3.3 — 2026-05-27
34
+
35
+ ### Changed (no breaking changes)
36
+
37
+ - **`fast` tier now routes to Grok 4.3 (primary) → Anthropic Haiku (fallback).**
38
+ When `GROK_API_KEY` is present in `LLMEnv`, the `fast` tier sends completions to
39
+ `grok-4.3` first and falls back to `claude-haiku-4-20250514` if Grok is unavailable
40
+ or returns an error. When `GROK_API_KEY` is absent, the request goes directly to
41
+ Anthropic Haiku (same behaviour as before). Callers that already set `tier: 'fast'`
42
+ pick up Grok routing automatically; no code change needed.
43
+
44
+ ### Added
45
+
46
+ - **`LLMOptions.reasoningEffort`** — `'none' | 'low' | 'medium' | 'high'`
47
+ (optional, defaults to `'none'`). Forwarded to the Grok `reasoning_effort` parameter
48
+ for `grok-4.3`; ignored for all other providers.
49
+ - **`MODELS.grok.fast`** updated from `'grok-4-fast'` to `'grok-4.3'`. Old alias
50
+ `'grok-4-fast'` retained as deprecated with updated pricing so historical ledger
51
+ rows remain accurate.
52
+ - **Grok pricing update**: `grok-4.3` at $1.25/$2.50 per MTok (in/out).
53
+ Old `grok-4-fast` and `grok-3-mini-latest` re-priced to the same $1.25/$2.50 rate.
54
+ - **`for (const [legIndex, leg] of routeLegs.entries())`** — renamed loop variable so
55
+ `legIndex` is accessible for the last-leg 429 rate-limit short-circuit.
56
+
57
+ ### Consumers
58
+
59
+ - To opt-in to Grok 4.3: add `GROK_API_KEY` to your `LLMEnv` binding. No other code
60
+ change required for `tier: 'fast'` callers.
61
+ - `GROK_API_KEY` is optional. If absent, fast-tier routing is identical to 0.3.2
62
+ (Anthropic Haiku only).
63
+
64
+ ---
65
+
66
+ ## 0.3.2 — 2026-05-27
67
+
68
+ ### Added (no breaking changes)
69
+
70
+ - **`LLMOptions.workload`** — optional string label (`'insights'`, `'copy'`, `'lead-qualification'`, …)
71
+ forwarded to cost-recording calls for per-workload cost attribution in dashboards.
72
+ - **Missing model pricing entries** in `MODEL_PRICE_PER_1M`:
73
+ `claude-haiku-4-5-20251001`, `claude-sonnet-4-20250514`, `claude-opus-4-20250514`
74
+ (aliases for variants that share pricing with their shorthand names; prevents
75
+ `estimateCostUsd` from silently returning `$0` for these model IDs).
76
+ - **`workload` forwarded** to `recordOrgCostUsage` so per-workload breakdowns appear
77
+ in the cost-tracking KV store.
78
+
79
+ ### Consumers
80
+
81
+ - Existing callers do not need to set `workload`; it is optional and defaults to `undefined`.
82
+ - The `recordOrgCostUsage` signature is unchanged; `workload` is an additive internal field.
83
+
84
+ ---
85
+
3
86
  ## 0.3.1 — 2026-05-02 PM
4
87
 
5
88
  ### Added (no breaking changes)
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @latimer-woods-tech/llm
2
2
 
3
3
  Tier-routed LLM orchestration for the Factory platform, with Cloudflare AI Gateway, Anthropic
4
- primary, Gemini 2.5 Pro long-context fallback, and Groq verifier.
4
+ primary, Gemini 2.5 Pro long-context fallback, Groq verifier, and DeepSeek workbench routing.
5
5
 
6
6
  ## Routing (0.3.0)
7
7
 
@@ -11,6 +11,7 @@ primary, Gemini 2.5 Pro long-context fallback, and Groq verifier.
11
11
  | `balanced` *(default)* | Claude Sonnet 4 | Gemini 2.5 Pro | swaps to Gemini when est. tokens ≥ 150k |
12
12
  | `smart` | Claude Opus 4 | Gemini 2.5 Pro | ditto; tools, long reasoning |
13
13
  | `verifier` | Groq Llama 3.3 70B | — | cheap second opinion; no fallback |
14
+ | `workbench` | DeepSeek Chat | Groq Llama | boring, reviewable, non-sensitive batch work |
14
15
 
15
16
  All traffic flows through `AI_GATEWAY_BASE_URL`. The gateway handles caching, rate-limit shedding,
16
17
  and per-project cost telemetry.
@@ -29,6 +30,7 @@ const res = await complete(
29
30
  AI_GATEWAY_BASE_URL: env.AI_GATEWAY_BASE_URL,
30
31
  ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY,
31
32
  GROQ_API_KEY: env.GROQ_API_KEY,
33
+ DEEPSEEK_API_KEY: env.DEEPSEEK_API_KEY,
32
34
  VERTEX_ACCESS_TOKEN: env.VERTEX_ACCESS_TOKEN,
33
35
  VERTEX_PROJECT: env.VERTEX_PROJECT,
34
36
  VERTEX_LOCATION: env.VERTEX_LOCATION,
@@ -47,6 +49,11 @@ if (res.ok) {
47
49
  }
48
50
  ```
49
51
 
52
+ Use `tier: 'workbench'` only for low-risk internal jobs: docs summaries, changelog drafts,
53
+ classification, issue triage, and other outputs a human or higher-trust model can review. Do not
54
+ send secrets, customer PII, production ops requests, billing data, or final customer-facing answers
55
+ through this lane.
56
+
50
57
  ## Vertex access token
51
58
 
52
59
  Minted via the JWT-bearer flow from a GCP service account. See
package/dist/index.d.mts CHANGED
@@ -10,12 +10,13 @@ interface LLMMessage {
10
10
  }
11
11
  /**
12
12
  * Quality tier selected by the caller. Routing is workload-split:
13
- * - `fast` → Anthropic Haiku (short, latency-sensitive)
13
+ * - `fast` → Grok 4.3 with Anthropic Haiku fallback (routine drafts/small jobs)
14
14
  * - `balanced` → Anthropic Sonnet (default)
15
15
  * - `smart` → Anthropic Opus OR Gemini 2.5 Pro if input is long-context (>150k tokens estimated)
16
16
  * - `verifier` → Groq Llama (cheap second opinion; only used from verifier code path)
17
+ * - `workbench` → DeepSeek Chat with Groq fallback (boring, reviewable, non-sensitive batch work)
17
18
  */
18
- type LLMTier = 'fast' | 'balanced' | 'smart' | 'verifier';
19
+ type LLMTier = 'fast' | 'balanced' | 'smart' | 'verifier' | 'workbench';
19
20
  /**
20
21
  * Options that influence LLM completion behaviour.
21
22
  */
@@ -37,13 +38,47 @@ interface LLMOptions {
37
38
  project?: string;
38
39
  /** Optional actor identifier (supervisor / worker / human). */
39
40
  actor?: string;
41
+ /** Optional workload label used in logs and cost-policy call sites. */
42
+ workload?: string;
43
+ /** Grok reasoning effort. Defaults to `none` for cost-controlled fast/draft calls. */
44
+ reasoningEffort?: 'none' | 'low' | 'medium' | 'high';
40
45
  /** Anthropic prompt-cache control. Defaults to `true` for `system` prompts ≥ 1024 tokens. */
41
46
  promptCache?: boolean;
47
+ /**
48
+ * Maximum estimated cost in USD for this completion.
49
+ * This cap is enforced after the provider returns because it uses actual
50
+ * response token counts to compute the final cost.
51
+ * If the post-call estimated cost exceeds this cap, `complete` returns a
52
+ * {@link RateLimitError} with code `LLM_COST_CAP_EXCEEDED` and
53
+ * `completionStream` throws the same error.
54
+ * Pricing is based on {@link MODEL_PRICE_PER_1M}; unknown models default to
55
+ * Opus rates (conservative upper bound).
56
+ */
57
+ maxCostUsd?: number;
58
+ /**
59
+ * Org-level daily cost cap in USD. Requires `env.LLM_COST_KV` to be set.
60
+ * When today's cumulative spend read from KV is >= this value, `complete`
61
+ * returns a {@link RateLimitError} with code `LLM_DAILY_CAP_EXCEEDED`
62
+ * without making any provider call. After a successful call the daily
63
+ * accumulator is updated in KV (TTL: 48 h).
64
+ */
65
+ dailyCapUsd?: number;
66
+ /**
67
+ * Org-level monthly cost cap in USD. Requires `env.LLM_COST_KV` to be set.
68
+ * Same enforcement pattern as {@link dailyCapUsd} but keyed by YYYY-MM.
69
+ * KV TTL: 40 days.
70
+ */
71
+ monthlyCapUsd?: number;
72
+ /**
73
+ * Metering context. When supplied and `deps.onRecord` is set, a {@link LLMRecordRow}
74
+ * is emitted after every successful completion. Errors are swallowed.
75
+ */
76
+ ledger?: LLMRecordContext;
42
77
  }
43
78
  /**
44
- * Provider that produced an LLM response. `grok` removed in 0.3.0.
79
+ * Provider that produced an LLM response.
45
80
  */
46
- type LLMProvider = 'anthropic' | 'gemini' | 'groq' | 'grok';
81
+ type LLMProvider = 'anthropic' | 'gemini' | 'groq' | 'grok' | 'deepseek';
47
82
  /**
48
83
  * Result returned by a successful completion.
49
84
  */
@@ -75,6 +110,8 @@ interface LLMEnv {
75
110
  AI_GATEWAY_BASE_URL: string;
76
111
  ANTHROPIC_API_KEY: string;
77
112
  GROQ_API_KEY: string;
113
+ /** Optional — only required for `{ tier: 'workbench' }` or `deepseek-*` model overrides. */
114
+ DEEPSEEK_API_KEY?: string;
78
115
  /** Optional — only required when caller passes `{ model: 'grok-*' }` override. */
79
116
  GROK_API_KEY?: string;
80
117
  /**
@@ -85,6 +122,51 @@ interface LLMEnv {
85
122
  VERTEX_ACCESS_TOKEN: string;
86
123
  VERTEX_PROJECT: string;
87
124
  VERTEX_LOCATION: string;
125
+ /**
126
+ * Optional KV store for org-level daily/monthly cost tracking and enforcement.
127
+ * When provided alongside {@link LLMOptions.dailyCapUsd} or {@link LLMOptions.monthlyCapUsd},
128
+ * `complete` will block calls that would exceed the declared cap.
129
+ * Any KV-like store satisfying `get`/`put` works (e.g. Cloudflare KV, in-memory stub).
130
+ */
131
+ LLM_COST_KV?: CostKvStore;
132
+ }
133
+ /**
134
+ * Minimal KV store interface for org-level LLM cost tracking.
135
+ * Cloudflare KV satisfies this. An in-memory stub is sufficient for tests.
136
+ */
137
+ interface CostKvStore {
138
+ get(key: string): Promise<string | null>;
139
+ put(key: string, value: string, options?: {
140
+ expirationTtl?: number;
141
+ }): Promise<void>;
142
+ }
143
+ /**
144
+ * Caller-supplied context stamped on every metering row.
145
+ * Mirrors the `LLMRecordContext` in `@latimer-woods-tech/llm-meter`; kept inline
146
+ * to avoid a circular dependency (llm-meter imports llm).
147
+ */
148
+ interface LLMRecordContext {
149
+ project: string;
150
+ actor: string;
151
+ runId?: string;
152
+ workload?: string;
153
+ tenantId?: string;
154
+ }
155
+ /**
156
+ * Row shape passed to the optional {@link LLMDeps.onRecord} callback.
157
+ * Callers can wire this directly to `recordCall` from `@latimer-woods-tech/llm-meter`.
158
+ */
159
+ interface LLMRecordRow extends LLMRecordContext {
160
+ model: string;
161
+ provider: LLMProvider;
162
+ tier: LLMTier;
163
+ inputTokens: number;
164
+ outputTokens: number;
165
+ cacheReadTokens: number;
166
+ cacheWriteTokens: number;
167
+ latencyMs: number;
168
+ costUsd: number;
169
+ yyyyMm: string;
88
170
  }
89
171
  /**
90
172
  * Optional dependencies for {@link complete}.
@@ -93,6 +175,12 @@ interface LLMDeps {
93
175
  fetch?: typeof fetch;
94
176
  logger?: Logger;
95
177
  now?: () => number;
178
+ /**
179
+ * Optional metering callback. Called after every successful completion.
180
+ * Errors are swallowed so metering never blocks the caller.
181
+ * Wire to `recordCall` from `@latimer-woods-tech/llm-meter`.
182
+ */
183
+ onRecord?: (row: LLMRecordRow) => Promise<void>;
96
184
  }
97
185
  declare const MODELS: {
98
186
  readonly anthropic: {
@@ -104,12 +192,13 @@ declare const MODELS: {
104
192
  readonly smart: "gemini-2.5-pro";
105
193
  };
106
194
  readonly groq: {
107
- readonly verifier: "llama-3.3-70b-versatile";
195
+ readonly verifier: "llama-4-maverick";
108
196
  };
109
197
  readonly grok: {
110
- /** Opt-in only via `{ model: 'grok-*' }`. Not in default tier routing. */
111
- readonly fast: "grok-4-fast";
112
- readonly mini: "grok-3-mini-latest";
198
+ readonly fast: "grok-4.3";
199
+ };
200
+ readonly deepseek: {
201
+ readonly workbench: "deepseek-chat";
113
202
  };
114
203
  };
115
204
  /** Cooldown duration in ms after a provider exhausts all retries. */
@@ -119,6 +208,22 @@ declare const PROVIDER_COOLDOWN_MS = 30000;
119
208
  * Uses the injected `now` function (or `Date.now`) for testability.
120
209
  */
121
210
  declare function isProviderCoolingDown(provider: LLMProvider, now?: () => number): boolean;
211
+ /**
212
+ * USD cost per 1 million tokens for each model.
213
+ * Source: Anthropic / Google / xAI pricing pages as of 2026-05.
214
+ * Keep these model names in sync with the default routing constants in
215
+ * {@link MODELS}; unknown models fall back to Opus rates (conservative upper bound).
216
+ *
217
+ * CANONICAL pricing source for the platform. `@latimer-woods-tech/llm-meter`
218
+ * derives its cents-denominated rates from this table and a drift-guard test
219
+ * there fails CI if they diverge — make all rate changes here.
220
+ */
221
+ declare const MODEL_PRICE_PER_1M: Record<string, {
222
+ input: number;
223
+ output: number;
224
+ cacheRead: number;
225
+ cacheWrite: number;
226
+ }>;
122
227
  /**
123
228
  * Marks a provider as cooling down for {@link PROVIDER_COOLDOWN_MS} milliseconds.
124
229
  */
@@ -132,10 +237,11 @@ declare const BASE_BACKOFF_MS = 250;
132
237
  * Run a completion through the routing plan for the requested tier.
133
238
  *
134
239
  * Routing summary (0.3.0):
135
- * - `fast` → Anthropic Haiku
240
+ * - `fast` → Grok 4.3; Anthropic Haiku fallback when Grok is unavailable
136
241
  * - `balanced` → Anthropic Sonnet; Gemini 2.5 Pro if `longContextThreshold` exceeded
137
242
  * - `smart` → Anthropic Opus; Gemini 2.5 Pro if long-context
138
243
  * - `verifier` → Groq Llama 3.3 70B (no fallback — verifier is inherently cheap/best-effort)
244
+ * - `workbench` → DeepSeek Chat; Groq fallback for boring/reviewable internal batch jobs
139
245
  *
140
246
  * All provider traffic flows through Cloudflare AI Gateway at `AI_GATEWAY_BASE_URL`.
141
247
  *
@@ -203,4 +309,4 @@ declare function completionStream(messages: LLMMessage[], env: LLMEnv, opts?: LL
203
309
  */
204
310
  declare function assertGrounding(response: string, sources: string[]): boolean;
205
311
 
206
- export { BASE_BACKOFF_MS, type LLMDeps, type LLMEnv, type LLMMessage, type LLMOptions, type LLMProvider, type LLMResult, type LLMTier, MODELS, PROVIDER_COOLDOWN_MS, assertGrounding, clearProviderCooldown, complete, completionStream, isProviderCoolingDown, markProviderCoolingDown };
312
+ export { BASE_BACKOFF_MS, type CostKvStore, type LLMDeps, type LLMEnv, type LLMMessage, type LLMOptions, type LLMProvider, type LLMRecordContext, type LLMRecordRow, type LLMResult, type LLMTier, MODELS, MODEL_PRICE_PER_1M, PROVIDER_COOLDOWN_MS, assertGrounding, clearProviderCooldown, complete, completionStream, isProviderCoolingDown, markProviderCoolingDown };
package/dist/index.mjs CHANGED
@@ -15,12 +15,13 @@ var MODELS = {
15
15
  smart: "gemini-2.5-pro"
16
16
  },
17
17
  groq: {
18
- verifier: "llama-3.3-70b-versatile"
18
+ verifier: "llama-4-maverick"
19
19
  },
20
20
  grok: {
21
- /** Opt-in only via `{ model: 'grok-*' }`. Not in default tier routing. */
22
- fast: "grok-4-fast",
23
- mini: "grok-3-mini-latest"
21
+ fast: "grok-4.3"
22
+ },
23
+ deepseek: {
24
+ workbench: "deepseek-chat"
24
25
  }
25
26
  };
26
27
  var DEFAULT_MAX_TOKENS = 1024;
@@ -37,6 +38,58 @@ function isProviderCoolingDown(provider, now = Date.now) {
37
38
  if (until === void 0) return false;
38
39
  return now() < until;
39
40
  }
41
+ function isoDate(nowMs) {
42
+ return new Date(nowMs).toISOString().slice(0, 10);
43
+ }
44
+ async function recordOrgCostUsage(kv, todayKey, monthKey, costUsd, opts) {
45
+ if (opts.dailyCapUsd !== void 0) {
46
+ const raw = await kv.get(todayKey).catch(() => null);
47
+ const spent = parseFloat(raw ?? "0");
48
+ await kv.put(todayKey, String(spent + costUsd), {
49
+ expirationTtl: 172800
50
+ /* 48 h */
51
+ }).catch(() => void 0);
52
+ }
53
+ if (opts.monthlyCapUsd !== void 0) {
54
+ const raw = await kv.get(monthKey).catch(() => null);
55
+ const spent = parseFloat(raw ?? "0");
56
+ await kv.put(monthKey, String(spent + costUsd), {
57
+ expirationTtl: 3456e3
58
+ /* 40 d */
59
+ }).catch(() => void 0);
60
+ }
61
+ }
62
+ var MODEL_PRICE_PER_1M = {
63
+ // Anthropic Haiku 4
64
+ "claude-haiku-4-20250514": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
65
+ "claude-haiku-4-5-20251001": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
66
+ // Anthropic Sonnet 4
67
+ "claude-sonnet-4-20250514": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
68
+ "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
69
+ // Anthropic Opus 4
70
+ "claude-opus-4-20250514": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
71
+ "claude-opus-4-7": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
72
+ // Gemini 2.5 Pro
73
+ "gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.31, cacheWrite: 4.5 },
74
+ // Groq Llama 4 Maverick
75
+ "llama-4-maverick": { input: 0.5, output: 0.77, cacheRead: 0.05, cacheWrite: 0.5 },
76
+ // Grok 4.3
77
+ "grok-4.3": { input: 1.25, output: 2.5, cacheRead: 0, cacheWrite: 0 },
78
+ // DeepSeek API pricing as of 2026-05: cache-write conservatively uses cache-miss input pricing.
79
+ "deepseek-chat": { input: 0.27, output: 1.1, cacheRead: 0.07, cacheWrite: 0.27 },
80
+ "deepseek-reasoner": { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 },
81
+ // Deprecated aliases retained for historical ledger rows.
82
+ "grok-4-fast": { input: 1.25, output: 2.5, cacheRead: 0, cacheWrite: 0 },
83
+ "grok-3-mini-latest": { input: 1.25, output: 2.5, cacheRead: 0, cacheWrite: 0 }
84
+ };
85
+ var PRICE_FALLBACK = MODEL_PRICE_PER_1M["claude-opus-4-7"];
86
+ function estimateCostUsd(tokens, model) {
87
+ const price = MODEL_PRICE_PER_1M[model] ?? PRICE_FALLBACK;
88
+ return (tokens.input * price.input + tokens.output * price.output + (tokens.cacheRead ?? 0) * price.cacheRead + (tokens.cacheWrite ?? 0) * price.cacheWrite) / 1e6;
89
+ }
90
+ function isoMonth(nowMs) {
91
+ return new Date(nowMs).toISOString().slice(0, 7);
92
+ }
40
93
  function markProviderCoolingDown(provider, now = Date.now) {
41
94
  providerCooldownUntil.set(provider, now() + PROVIDER_COOLDOWN_MS);
42
95
  }
@@ -149,12 +202,38 @@ function buildGrokRequest(model, messages, opts, env) {
149
202
  const merged = [];
150
203
  if (sys) merged.push({ role: "system", content: sys });
151
204
  for (const m of messages) if (m.role !== "system") merged.push(m);
205
+ const body = {
206
+ model,
207
+ max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
208
+ temperature: opts.temperature ?? DEFAULT_TEMPERATURE,
209
+ messages: merged
210
+ };
211
+ if (model === MODELS.grok.fast) {
212
+ body.reasoning_effort = opts.reasoningEffort ?? "none";
213
+ }
152
214
  return {
153
215
  url: `${env.AI_GATEWAY_BASE_URL}/grok/v1/chat/completions`,
154
216
  headers: {
155
217
  "content-type": "application/json",
156
218
  authorization: `Bearer ${env.GROK_API_KEY}`
157
219
  },
220
+ body: JSON.stringify(body)
221
+ };
222
+ }
223
+ function buildDeepSeekRequest(model, messages, opts, env) {
224
+ if (!env.DEEPSEEK_API_KEY) {
225
+ throw new ValidationError("DEEPSEEK_API_KEY required for workbench tier or deepseek-* model override");
226
+ }
227
+ const sys = opts.system ?? messages.find((m) => m.role === "system")?.content;
228
+ const merged = [];
229
+ if (sys) merged.push({ role: "system", content: sys });
230
+ for (const m of messages) if (m.role !== "system") merged.push(m);
231
+ return {
232
+ url: `${env.AI_GATEWAY_BASE_URL}/deepseek/chat/completions`,
233
+ headers: {
234
+ "content-type": "application/json",
235
+ authorization: `Bearer ${env.DEEPSEEK_API_KEY}`
236
+ },
158
237
  body: JSON.stringify({
159
238
  model,
160
239
  max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
@@ -261,10 +340,16 @@ function plan(tier, opts, tokenEstimate) {
261
340
  if (m.startsWith("claude")) return { primary: { provider: "anthropic", model: m } };
262
341
  if (m.startsWith("gemini")) return { primary: { provider: "gemini", model: m } };
263
342
  if (m.startsWith("grok")) return { primary: { provider: "grok", model: m } };
343
+ if (m.startsWith("deepseek")) return { primary: { provider: "deepseek", model: m } };
264
344
  return { primary: { provider: "groq", model: m } };
265
345
  }
266
346
  const longContext = tokenEstimate >= (opts.longContextThreshold ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
267
347
  switch (tier) {
348
+ case "workbench":
349
+ return {
350
+ primary: { provider: "deepseek", model: MODELS.deepseek.workbench },
351
+ fallback: { provider: "groq", model: MODELS.groq.verifier }
352
+ };
268
353
  case "verifier":
269
354
  return { primary: { provider: "groq", model: MODELS.groq.verifier } };
270
355
  case "smart":
@@ -276,7 +361,10 @@ function plan(tier, opts, tokenEstimate) {
276
361
  fallback: { provider: "gemini", model: MODELS.gemini.smart }
277
362
  };
278
363
  case "fast":
279
- return { primary: { provider: "anthropic", model: MODELS.anthropic.fast } };
364
+ return {
365
+ primary: { provider: "grok", model: MODELS.grok.fast },
366
+ fallback: { provider: "anthropic", model: MODELS.anthropic.fast }
367
+ };
280
368
  case "balanced":
281
369
  default:
282
370
  return longContext ? {
@@ -288,6 +376,14 @@ function plan(tier, opts, tokenEstimate) {
288
376
  };
289
377
  }
290
378
  }
379
+ function buildAigMetadata(opts) {
380
+ const meta = {};
381
+ if (opts.project) meta.project = opts.project;
382
+ if (opts.workload) meta.workload = opts.workload;
383
+ if (opts.actor) meta.actor = opts.actor;
384
+ if (opts.runId) meta.runId = opts.runId;
385
+ return Object.keys(meta).length > 0 ? JSON.stringify(meta) : void 0;
386
+ }
291
387
  async function callOne(leg, messages, opts, env, fetchImpl, logger, nowFn) {
292
388
  let req;
293
389
  switch (leg.provider) {
@@ -303,7 +399,12 @@ async function callOne(leg, messages, opts, env, fetchImpl, logger, nowFn) {
303
399
  case "grok":
304
400
  req = buildGrokRequest(leg.model, messages, opts, env);
305
401
  break;
402
+ case "deepseek":
403
+ req = buildDeepSeekRequest(leg.model, messages, opts, env);
404
+ break;
306
405
  }
406
+ const aigMetadata = buildAigMetadata(opts);
407
+ if (aigMetadata) req.headers["cf-aig-metadata"] = aigMetadata;
307
408
  const { json, gatewayRequestId, attempts } = await callWithBackoff(
308
409
  leg.provider,
309
410
  req,
@@ -321,6 +422,8 @@ async function callOne(leg, messages, opts, env, fetchImpl, logger, nowFn) {
321
422
  return { parsed: parseGroq(json), gatewayRequestId, attempts };
322
423
  case "grok":
323
424
  return { parsed: parseGroq(json), gatewayRequestId, attempts };
425
+ case "deepseek":
426
+ return { parsed: parseGroq(json), gatewayRequestId, attempts };
324
427
  }
325
428
  }
326
429
  async function complete(messages, env, opts = {}, deps = {}) {
@@ -338,13 +441,48 @@ async function complete(messages, env, opts = {}, deps = {}) {
338
441
  const system = opts.system ?? messages.find((m) => m.role === "system")?.content;
339
442
  const tokenEstimate = estimateTokens(messages, system);
340
443
  const route = plan(tier, opts, tokenEstimate);
444
+ const kv = env.LLM_COST_KV;
445
+ const todayKey = `llm:daily-cost:${isoDate(now())}`;
446
+ const monthKey = `llm:monthly-cost:${isoMonth(now())}`;
447
+ if (kv) {
448
+ if (opts.dailyCapUsd !== void 0) {
449
+ const raw = await kv.get(todayKey).catch(() => null);
450
+ const spent = parseFloat(raw ?? "0");
451
+ if (spent >= opts.dailyCapUsd) {
452
+ return toErrorResponse(
453
+ new RateLimitError("LLM_DAILY_CAP_EXCEEDED", {
454
+ spentUsd: spent,
455
+ dailyCapUsd: opts.dailyCapUsd
456
+ })
457
+ );
458
+ }
459
+ }
460
+ if (opts.monthlyCapUsd !== void 0) {
461
+ const raw = await kv.get(monthKey).catch(() => null);
462
+ const spent = parseFloat(raw ?? "0");
463
+ if (spent >= opts.monthlyCapUsd) {
464
+ return toErrorResponse(
465
+ new RateLimitError("LLM_MONTHLY_CAP_EXCEEDED", {
466
+ spentUsd: spent,
467
+ monthlyCapUsd: opts.monthlyCapUsd
468
+ })
469
+ );
470
+ }
471
+ }
472
+ }
341
473
  const attemptLog = [];
342
- for (const leg of [route.primary, route.fallback].filter(Boolean)) {
474
+ const routeLegs = [route.primary, route.fallback].filter(Boolean);
475
+ for (const [legIndex, leg] of routeLegs.entries()) {
343
476
  if (isProviderCoolingDown(leg.provider, now)) {
344
477
  logger?.warn?.("llm.provider.coolingDown", { provider: leg.provider });
345
478
  attemptLog.push({ provider: leg.provider, message: "skipped: cooling down" });
346
479
  continue;
347
480
  }
481
+ if (opts.signal?.aborted) {
482
+ return toErrorResponse(
483
+ new InternalError("llm call aborted", { provider: leg.provider, model: leg.model })
484
+ );
485
+ }
348
486
  try {
349
487
  const result = await callOne(leg, messages, opts, env, fetchImpl, logger, now);
350
488
  if (!result.parsed.content) {
@@ -358,26 +496,60 @@ async function complete(messages, env, opts = {}, deps = {}) {
358
496
  attempts: result.attempts,
359
497
  runId: opts.runId,
360
498
  project: opts.project,
361
- actor: opts.actor
499
+ actor: opts.actor,
500
+ workload: opts.workload
362
501
  });
363
- return {
364
- data: {
365
- content: result.parsed.content,
366
- provider: leg.provider,
367
- model: result.parsed.model ?? leg.model,
368
- tier,
369
- tokens: {
370
- input: result.parsed.input,
371
- output: result.parsed.output,
372
- cacheRead: result.parsed.cacheRead,
373
- cacheWrite: result.parsed.cacheWrite
374
- },
375
- latency: now() - startedAt,
376
- attempts: result.attempts,
377
- gatewayRequestId: result.gatewayRequestId
502
+ const llmResult = {
503
+ content: result.parsed.content,
504
+ provider: leg.provider,
505
+ model: result.parsed.model ?? leg.model,
506
+ tier,
507
+ tokens: {
508
+ input: result.parsed.input,
509
+ output: result.parsed.output,
510
+ cacheRead: result.parsed.cacheRead,
511
+ cacheWrite: result.parsed.cacheWrite
378
512
  },
379
- error: null
513
+ latency: now() - startedAt,
514
+ attempts: result.attempts,
515
+ gatewayRequestId: result.gatewayRequestId
380
516
  };
517
+ const costUsd = estimateCostUsd(llmResult.tokens, llmResult.model);
518
+ if (opts.maxCostUsd !== void 0 && costUsd > opts.maxCostUsd) {
519
+ if (kv && (opts.dailyCapUsd !== void 0 || opts.monthlyCapUsd !== void 0)) {
520
+ await recordOrgCostUsage(kv, todayKey, monthKey, costUsd, opts);
521
+ }
522
+ return toErrorResponse(
523
+ new RateLimitError("LLM_COST_CAP_EXCEEDED", {
524
+ costUsd,
525
+ maxCostUsd: opts.maxCostUsd,
526
+ model: llmResult.model,
527
+ tokens: llmResult.tokens
528
+ })
529
+ );
530
+ }
531
+ if (kv && (opts.dailyCapUsd !== void 0 || opts.monthlyCapUsd !== void 0)) {
532
+ await recordOrgCostUsage(kv, todayKey, monthKey, costUsd, opts);
533
+ }
534
+ if (deps.onRecord && opts.ledger) {
535
+ const row = {
536
+ ...opts.ledger,
537
+ model: llmResult.model,
538
+ provider: llmResult.provider,
539
+ tier: llmResult.tier,
540
+ inputTokens: llmResult.tokens.input,
541
+ outputTokens: llmResult.tokens.output,
542
+ cacheReadTokens: llmResult.tokens.cacheRead ?? 0,
543
+ cacheWriteTokens: llmResult.tokens.cacheWrite ?? 0,
544
+ latencyMs: llmResult.latency,
545
+ costUsd,
546
+ yyyyMm: isoMonth(now())
547
+ };
548
+ deps.onRecord(row).catch((e) => {
549
+ logger?.warn?.("llm.onRecord.error", { message: e instanceof Error ? e.message : String(e) });
550
+ });
551
+ }
552
+ return { data: llmResult, error: null };
381
553
  } catch (e) {
382
554
  if (e instanceof DOMException && e.name === "AbortError") {
383
555
  return toErrorResponse(
@@ -386,7 +558,7 @@ async function complete(messages, env, opts = {}, deps = {}) {
386
558
  }
387
559
  if (isProviderError(e)) {
388
560
  attemptLog.push({ provider: e.provider, status: e.status, message: e.message });
389
- if (e.status === 429 && !route.fallback) {
561
+ if (e.status === 429 && legIndex === routeLegs.length - 1) {
390
562
  return toErrorResponse(
391
563
  new RateLimitError(`llm rate limited on ${e.provider}`, { attempts: attemptLog })
392
564
  );
@@ -417,7 +589,8 @@ async function* completionStream(messages, env, opts = {}) {
417
589
  const system = opts.system ?? messages.find((m) => m.role === "system")?.content;
418
590
  const tokenEstimate = estimateTokens(messages, system);
419
591
  const route = plan(tier, opts, tokenEstimate);
420
- if (route.primary.provider !== "anthropic") {
592
+ const streamLeg = route.primary.provider === "grok" && !env.GROK_API_KEY && route.fallback?.provider === "anthropic" ? route.fallback : route.primary;
593
+ if (streamLeg.provider !== "anthropic") {
421
594
  const result = await complete(messages, env, opts, deps);
422
595
  if (result.error !== null || result.data === null) {
423
596
  throw new InternalError("LLM_ALL_PROVIDERS_FAILED", { error: result.error });
@@ -425,8 +598,8 @@ async function* completionStream(messages, env, opts = {}) {
425
598
  yield result.data.content;
426
599
  return result.data;
427
600
  }
428
- if (isProviderCoolingDown(route.primary.provider, now)) {
429
- logger?.warn?.("llm.provider.coolingDown", { provider: route.primary.provider });
601
+ if (isProviderCoolingDown(streamLeg.provider, now)) {
602
+ logger?.warn?.("llm.provider.coolingDown", { provider: streamLeg.provider });
430
603
  const result = await complete(messages, env, opts, deps);
431
604
  if (result.error !== null || result.data === null) {
432
605
  throw new InternalError("LLM_ALL_PROVIDERS_FAILED", { error: result.error });
@@ -434,7 +607,9 @@ async function* completionStream(messages, env, opts = {}) {
434
607
  yield result.data.content;
435
608
  return result.data;
436
609
  }
437
- const req = buildAnthropicRequest(route.primary.model, messages, opts, env, true);
610
+ const req = buildAnthropicRequest(streamLeg.model, messages, opts, env, true);
611
+ const streamAigMetadata = buildAigMetadata(opts);
612
+ if (streamAigMetadata) req.headers["cf-aig-metadata"] = streamAigMetadata;
438
613
  let response;
439
614
  try {
440
615
  response = await fetchImpl(req.url, {
@@ -448,8 +623,8 @@ async function* completionStream(messages, env, opts = {}) {
448
623
  } catch (e) {
449
624
  if (e instanceof DOMException && e.name === "AbortError") {
450
625
  throw new InternalError("llm call aborted", {
451
- provider: route.primary.provider,
452
- model: route.primary.model
626
+ provider: streamLeg.provider,
627
+ model: streamLeg.model
453
628
  });
454
629
  }
455
630
  throw new InternalError("llm stream fetch failed", {
@@ -460,12 +635,12 @@ async function* completionStream(messages, env, opts = {}) {
460
635
  const text = await response.text().catch(() => "");
461
636
  const retryable = isRetryableForBackoff(response.status);
462
637
  if (retryable && response.status === 429) {
463
- markProviderCoolingDown(route.primary.provider, now);
638
+ markProviderCoolingDown(streamLeg.provider, now);
464
639
  }
465
640
  const result = await complete(messages, env, opts, deps);
466
641
  if (result.error !== null || result.data === null) {
467
642
  throw new InternalError("LLM_ALL_PROVIDERS_FAILED", {
468
- streamError: `${route.primary.provider} ${String(response.status)}: ${text.slice(0, 300)}`,
643
+ streamError: `${streamLeg.provider} ${String(response.status)}: ${text.slice(0, 300)}`,
469
644
  error: result.error
470
645
  });
471
646
  }
@@ -474,7 +649,7 @@ async function* completionStream(messages, env, opts = {}) {
474
649
  }
475
650
  if (!response.body) {
476
651
  throw new InternalError("llm stream response body is null", {
477
- provider: route.primary.provider
652
+ provider: streamLeg.provider
478
653
  });
479
654
  }
480
655
  const decoder = new TextDecoder();
@@ -528,20 +703,21 @@ async function* completionStream(messages, env, opts = {}) {
528
703
  } finally {
529
704
  reader.releaseLock();
530
705
  }
531
- clearProviderCooldown(route.primary.provider);
706
+ clearProviderCooldown(streamLeg.provider);
532
707
  logger?.info?.("llm.completionStream", {
533
- provider: route.primary.provider,
534
- model: route.primary.model,
708
+ provider: streamLeg.provider,
709
+ model: streamLeg.model,
535
710
  tier,
536
711
  tokenEstimate,
537
712
  runId: opts.runId,
538
713
  project: opts.project,
539
- actor: opts.actor
714
+ actor: opts.actor,
715
+ workload: opts.workload
540
716
  });
541
717
  return {
542
718
  content: accumulatedText,
543
- provider: route.primary.provider,
544
- model: modelName ?? route.primary.model,
719
+ provider: streamLeg.provider,
720
+ model: modelName ?? streamLeg.model,
545
721
  tier,
546
722
  tokens: { input: inputTokens, output: outputTokens, cacheRead, cacheWrite },
547
723
  latency: now() - startedAt,
@@ -572,6 +748,7 @@ function assertGrounding(response, sources) {
572
748
  export {
573
749
  BASE_BACKOFF_MS,
574
750
  MODELS,
751
+ MODEL_PRICE_PER_1M,
575
752
  PROVIDER_COOLDOWN_MS,
576
753
  assertGrounding,
577
754
  clearProviderCooldown,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n InternalError,\n RateLimitError,\n ValidationError,\n toErrorResponse,\n type FactoryResponse,\n} from '@latimer-woods-tech/errors';\nimport type { Logger } from '@latimer-woods-tech/logger';\n\n/**\n * Single chat message exchanged with an LLM provider.\n */\nexport interface LLMMessage {\n role: 'user' | 'assistant' | 'system';\n content: string;\n}\n\n/**\n * Quality tier selected by the caller. Routing is workload-split:\n * - `fast` → Anthropic Haiku (short, latency-sensitive)\n * - `balanced` → Anthropic Sonnet (default)\n * - `smart` → Anthropic Opus OR Gemini 2.5 Pro if input is long-context (>150k tokens estimated)\n * - `verifier` → Groq Llama (cheap second opinion; only used from verifier code path)\n */\nexport type LLMTier = 'fast' | 'balanced' | 'smart' | 'verifier';\n\n/**\n * Options that influence LLM completion behaviour.\n */\nexport interface LLMOptions {\n /** Quality tier; see {@link LLMTier}. Defaults to `balanced`. */\n tier?: LLMTier;\n /** Explicit model override. Takes precedence over tier. */\n model?: string;\n maxTokens?: number;\n temperature?: number;\n system?: string;\n /** Token budget above which we force long-context routing (Gemini). */\n longContextThreshold?: number;\n /** Per-call cancellation signal. Aborts the in-flight provider request. */\n signal?: AbortSignal;\n /** Optional run identifier stamped on ledger rows + logs. */\n runId?: string;\n /** Optional project identifier stamped on ledger rows + logs. */\n project?: string;\n /** Optional actor identifier (supervisor / worker / human). */\n actor?: string;\n /** Anthropic prompt-cache control. Defaults to `true` for `system` prompts ≥ 1024 tokens. */\n promptCache?: boolean;\n}\n\n/**\n * Provider that produced an LLM response. `grok` removed in 0.3.0.\n */\nexport type LLMProvider = 'anthropic' | 'gemini' | 'groq' | 'grok';\n\n/**\n * Result returned by a successful completion.\n */\nexport interface LLMResult {\n content: string;\n provider: LLMProvider;\n model: string;\n tier: LLMTier;\n tokens: { input: number; output: number; cacheRead?: number; cacheWrite?: number };\n latency: number;\n /** Number of attempts before success (1 = primary succeeded). */\n attempts: number;\n /** Monotonic request id from AI Gateway, if present in headers. */\n gatewayRequestId?: string;\n}\n\n/**\n * Environment bindings required by {@link complete}.\n *\n * `AI_GATEWAY_BASE_URL` is REQUIRED in 0.3.0. All provider calls flow through the\n * Cloudflare AI Gateway for unified logging, rate limiting, and cost telemetry.\n * In test/dev the caller may pass a custom fetch impl that short-circuits this.\n */\nexport interface LLMEnv {\n AI_GATEWAY_BASE_URL: string;\n ANTHROPIC_API_KEY: string;\n GROQ_API_KEY: string;\n /** Optional — only required when caller passes `{ model: 'grok-*' }` override. */\n GROK_API_KEY?: string;\n /**\n * Google Cloud short-lived access token with `aiplatform.endpoints.predict`.\n * Callers mint this via the JWT-bearer flow (service account → token exchange);\n * see `docs/runbooks/rotate-gcp-sa.md`. Token must be valid for ≥ 5 minutes.\n */\n VERTEX_ACCESS_TOKEN: string;\n VERTEX_PROJECT: string;\n VERTEX_LOCATION: string;\n}\n\n/**\n * Optional dependencies for {@link complete}.\n */\nexport interface LLMDeps {\n fetch?: typeof fetch;\n logger?: Logger;\n now?: () => number;\n}\n\n// Model catalogue — keep in sync with docs/architecture/FACTORY_V1.md § LLM substrate.\nconst MODELS = {\n anthropic: {\n fast: 'claude-haiku-4-20250514',\n balanced: 'claude-sonnet-4-6',\n smart: 'claude-opus-4-7',\n },\n gemini: {\n smart: 'gemini-2.5-pro',\n },\n groq: {\n verifier: 'llama-3.3-70b-versatile',\n },\n grok: {\n /** Opt-in only via `{ model: 'grok-*' }`. Not in default tier routing. */\n fast: 'grok-4-fast',\n mini: 'grok-3-mini-latest',\n },\n} as const;\n\nconst DEFAULT_MAX_TOKENS = 1024;\nconst DEFAULT_TEMPERATURE = 0.7;\nconst DEFAULT_LONG_CONTEXT_THRESHOLD = 150_000; // tokens\n\n// ─── Per-provider exponential backoff constants ────────────────────────────\n/** Base delay in ms for the first retry. */\nconst BACKOFF_BASE_MS = 500;\n/** Maximum backoff cap in ms. */\nconst BACKOFF_CAP_MS = 8_000;\n/** Max random jitter added to each backoff delay, in ms. */\nconst BACKOFF_JITTER_MAX_MS = 250;\n/** Maximum number of attempts per provider (1 initial + 2 retries). */\nconst PER_PROVIDER_MAX_ATTEMPTS = 3;\n\n// ─── Per-provider cooldown state (module-level) ────────────────────────────\n/**\n * Tracks when a provider's cooldown period expires.\n * Keyed by {@link LLMProvider}; value is the `Date.now()` epoch ms at which\n * the cooldown expires. Absent key means \"not cooling down\".\n */\nconst providerCooldownUntil: Map<LLMProvider, number> = new Map();\n\n/** Cooldown duration in ms after a provider exhausts all retries. */\nconst PROVIDER_COOLDOWN_MS = 30_000;\n\n/**\n * Returns `true` if the provider is currently in its cooldown window.\n * Uses the injected `now` function (or `Date.now`) for testability.\n */\nfunction isProviderCoolingDown(provider: LLMProvider, now: () => number = Date.now): boolean {\n const until = providerCooldownUntil.get(provider);\n if (until === undefined) return false;\n return now() < until;\n}\n\n/**\n * Marks a provider as cooling down for {@link PROVIDER_COOLDOWN_MS} milliseconds.\n */\nfunction markProviderCoolingDown(provider: LLMProvider, now: () => number = Date.now): void {\n providerCooldownUntil.set(provider, now() + PROVIDER_COOLDOWN_MS);\n}\n\n/**\n * Clears the cooldown state for a provider after a successful call.\n */\nfunction clearProviderCooldown(provider: LLMProvider): void {\n providerCooldownUntil.delete(provider);\n}\n\n// ─── Legacy backoff constant (kept for the existing callWithBackoff signature) ─\nconst BASE_BACKOFF_MS = 250;\n\ninterface ProviderError {\n provider: LLMProvider;\n status: number;\n retryable: boolean;\n message: string;\n}\n\n/**\n * Returns `true` for status codes that should trigger a retry.\n * Only 429 and 5xx (transient server errors) qualify; other 4xx are terminal.\n */\nfunction isRetryableForBackoff(status: number): boolean {\n return status === 429 || (status >= 500 && status < 600);\n}\n\nfunction estimateTokens(messages: LLMMessage[], system?: string): number {\n // Cheap estimator: ~4 chars/token. Good enough for threshold routing.\n let chars = system?.length ?? 0;\n for (const m of messages) chars += m.content.length;\n return Math.ceil(chars / 4);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const t = setTimeout(resolve, ms);\n if (signal) {\n const onAbort = () => {\n clearTimeout(t);\n reject(new DOMException('Aborted', 'AbortError'));\n };\n if (signal.aborted) onAbort();\n else signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n}\n\n/**\n * Computes the exponential backoff delay for a given attempt with jitter.\n *\n * Formula: `Math.min(base * 2^attempt + jitter, cap)`\n * where `jitter` is a random value in `[0, BACKOFF_JITTER_MAX_MS)`.\n *\n * @param attempt - Zero-based attempt index (0 = first retry after initial failure).\n */\nfunction computeBackoffMs(attempt: number): number {\n const jitter = Math.floor(Math.random() * BACKOFF_JITTER_MAX_MS);\n return Math.min(BACKOFF_BASE_MS * Math.pow(2, attempt) + jitter, BACKOFF_CAP_MS);\n}\n\n// ─── Provider request builders ─────────────────────────────────────────────\n\nfunction buildAnthropicRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n streaming = false,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const filtered = messages.filter((m) => m.role !== 'system');\n const body: Record<string, unknown> = {\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: filtered.map((m) => ({ role: m.role, content: m.content })),\n };\n if (streaming) {\n body.stream = true;\n }\n if (sys) {\n const cache = opts.promptCache ?? sys.length >= 4096;\n body.system = cache\n ? [{ type: 'text', text: sys, cache_control: { type: 'ephemeral' } }]\n : sys;\n }\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/anthropic/v1/messages`,\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': env.ANTHROPIC_API_KEY,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n };\n}\n\nfunction buildGeminiRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const contents = messages\n .filter((m) => m.role !== 'system')\n .map((m) => ({\n role: m.role === 'assistant' ? 'model' : 'user',\n parts: [{ text: m.content }],\n }));\n const body: Record<string, unknown> = {\n contents,\n generationConfig: {\n maxOutputTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n },\n };\n if (sys) {\n body.systemInstruction = { parts: [{ text: sys }] };\n }\n const path = `v1/projects/${env.VERTEX_PROJECT}/locations/${env.VERTEX_LOCATION}/publishers/google/models/${model}:generateContent`;\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/google-vertex-ai/${path}`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.VERTEX_ACCESS_TOKEN}`,\n },\n body: JSON.stringify(body),\n };\n}\n\nfunction buildGroqRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const merged: LLMMessage[] = [];\n if (sys) merged.push({ role: 'system', content: sys });\n for (const m of messages) if (m.role !== 'system') merged.push(m);\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/groq/openai/v1/chat/completions`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.GROQ_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: merged,\n }),\n };\n}\n\nfunction buildGrokRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n if (!env.GROK_API_KEY) {\n throw new ValidationError('GROK_API_KEY required for grok-* model override');\n }\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const merged: LLMMessage[] = [];\n if (sys) merged.push({ role: 'system', content: sys });\n for (const m of messages) if (m.role !== 'system') merged.push(m);\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/grok/v1/chat/completions`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.GROK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: merged,\n }),\n };\n}\n\n// ─── Response parsers ──────────────────────────────────────────────────────\n\ninterface AnthropicResponse {\n content?: Array<{ type: string; text?: string }>;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n model?: string;\n}\n\ninterface GeminiResponse {\n candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>;\n usageMetadata?: {\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n };\n}\n\ninterface GroqResponse {\n choices?: Array<{ message?: { content?: string } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n model?: string;\n}\n\nfunction parseAnthropic(\n json: unknown,\n): { content: string; input: number; output: number; cacheRead: number; cacheWrite: number; model?: string } {\n const r = json as AnthropicResponse;\n return {\n content: r.content?.find((c) => c.type === 'text')?.text ?? '',\n input: r.usage?.input_tokens ?? 0,\n output: r.usage?.output_tokens ?? 0,\n cacheRead: r.usage?.cache_read_input_tokens ?? 0,\n cacheWrite: r.usage?.cache_creation_input_tokens ?? 0,\n model: r.model,\n };\n}\n\nfunction parseGemini(json: unknown): { content: string; input: number; output: number } {\n const r = json as GeminiResponse;\n const text =\n r.candidates?.[0]?.content?.parts?.map((p) => p.text ?? '').join('') ?? '';\n return {\n content: text,\n input: r.usageMetadata?.promptTokenCount ?? 0,\n output: r.usageMetadata?.candidatesTokenCount ?? 0,\n };\n}\n\nfunction parseGroq(json: unknown): { content: string; input: number; output: number; model?: string } {\n const r = json as GroqResponse;\n return {\n content: r.choices?.[0]?.message?.content ?? '',\n input: r.usage?.prompt_tokens ?? 0,\n output: r.usage?.completion_tokens ?? 0,\n model: r.model,\n };\n}\n\n// ─── Core call with backoff ────────────────────────────────────────────────\n\n/**\n * Calls a provider with per-provider exponential backoff.\n *\n * Retries up to {@link PER_PROVIDER_MAX_ATTEMPTS} times on 429 or transient 5xx.\n * Other 4xx codes are treated as terminal and not retried.\n * AbortError is never retried — it bubbles immediately.\n *\n * @param provider - Provider name, used for error tagging.\n * @param request - Pre-built HTTP request descriptor.\n * @param fetchImpl - Fetch implementation (injectable for tests).\n * @param signal - Optional AbortSignal for cancellation.\n * @param logger - Optional logger for per-attempt warnings.\n * @param nowFn - Optional clock injection for testability.\n * @returns Parsed JSON body, optional AI Gateway request ID, and attempt count.\n */\nasync function callWithBackoff(\n provider: LLMProvider,\n request: { url: string; headers: Record<string, string>; body: string },\n fetchImpl: typeof fetch,\n signal: AbortSignal | undefined,\n logger: Logger | undefined,\n nowFn?: () => number,\n): Promise<{ json: unknown; gatewayRequestId?: string; attempts: number }> {\n /**\n * Helper: mark provider cooling down and then throw the error.\n * Called whenever we determine we've exhausted all retries for the provider.\n * AbortError is never counted as a provider exhaustion — it bypasses this.\n */\n function exhaustAndThrow(err: ProviderError): never {\n markProviderCoolingDown(provider, nowFn ?? Date.now);\n throw err;\n }\n\n let lastErr: ProviderError | undefined;\n for (let attempt = 1; attempt <= PER_PROVIDER_MAX_ATTEMPTS; attempt++) {\n try {\n const response = await fetchImpl(request.url, {\n method: 'POST',\n headers: request.headers,\n body: request.body,\n signal,\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n const retryable = isRetryableForBackoff(response.status);\n const err: ProviderError = {\n provider,\n status: response.status,\n retryable,\n message: `${provider} ${String(response.status)}: ${text.slice(0, 300)}`,\n };\n logger?.warn?.('llm.provider.error', { provider, status: response.status, attempt });\n if (!err.retryable || attempt === PER_PROVIDER_MAX_ATTEMPTS) {\n if (err.retryable) exhaustAndThrow(err); // retryable but exhausted\n throw err; // terminal non-retryable error — no cooldown\n }\n lastErr = err;\n } else {\n const gatewayRequestId = response.headers.get('cf-aig-request-id') ?? undefined;\n clearProviderCooldown(provider);\n return { json: await response.json(), gatewayRequestId, attempts: attempt };\n }\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n if (typeof e === 'object' && e !== null && 'retryable' in e) {\n const err = e as ProviderError;\n if (!err.retryable || attempt === PER_PROVIDER_MAX_ATTEMPTS) {\n if (err.retryable) exhaustAndThrow(err); // retryable but exhausted\n throw err; // terminal — no cooldown\n }\n lastErr = err;\n } else {\n const err: ProviderError = {\n provider,\n status: 0,\n retryable: true,\n message: e instanceof Error ? e.message : String(e),\n };\n if (attempt === PER_PROVIDER_MAX_ATTEMPTS) exhaustAndThrow(err);\n lastErr = err;\n }\n }\n // Exponential backoff with jitter: base=500ms, cap=8000ms, jitter up to 250ms\n const backoffMs = computeBackoffMs(attempt - 1);\n await sleep(backoffMs, signal);\n }\n // Fallthrough — should not be reached, but mark cooling down defensively.\n markProviderCoolingDown(provider, nowFn ?? Date.now);\n throw lastErr ?? ({ provider, status: 0, retryable: false, message: 'exhausted' } as ProviderError);\n}\n\nfunction isProviderError(err: unknown): err is ProviderError {\n return (\n typeof err === 'object' &&\n err !== null &&\n typeof (err as { status?: unknown }).status === 'number' &&\n typeof (err as { message?: unknown }).message === 'string' &&\n typeof (err as { provider?: unknown }).provider === 'string'\n );\n}\n\n// ─── Routing ───────────────────────────────────────────────────────────────\n\ninterface RoutePlan {\n primary: { provider: LLMProvider; model: string };\n fallback?: { provider: LLMProvider; model: string };\n}\n\nfunction plan(tier: LLMTier, opts: LLMOptions, tokenEstimate: number): RoutePlan {\n if (opts.model) {\n // Explicit override — best-effort provider detection.\n const m = opts.model;\n if (m.startsWith('claude')) return { primary: { provider: 'anthropic', model: m } };\n if (m.startsWith('gemini')) return { primary: { provider: 'gemini', model: m } };\n if (m.startsWith('grok')) return { primary: { provider: 'grok', model: m } };\n return { primary: { provider: 'groq', model: m } };\n }\n const longContext = tokenEstimate >= (opts.longContextThreshold ?? DEFAULT_LONG_CONTEXT_THRESHOLD);\n switch (tier) {\n case 'verifier':\n return { primary: { provider: 'groq', model: MODELS.groq.verifier } };\n case 'smart':\n return longContext\n ? {\n primary: { provider: 'gemini', model: MODELS.gemini.smart },\n fallback: { provider: 'anthropic', model: MODELS.anthropic.smart },\n }\n : {\n primary: { provider: 'anthropic', model: MODELS.anthropic.smart },\n fallback: { provider: 'gemini', model: MODELS.gemini.smart },\n };\n case 'fast':\n return { primary: { provider: 'anthropic', model: MODELS.anthropic.fast } };\n case 'balanced':\n default:\n return longContext\n ? {\n primary: { provider: 'gemini', model: MODELS.gemini.smart },\n fallback: { provider: 'anthropic', model: MODELS.anthropic.balanced },\n }\n : {\n primary: { provider: 'anthropic', model: MODELS.anthropic.balanced },\n fallback: { provider: 'gemini', model: MODELS.gemini.smart },\n };\n }\n}\n\nasync function callOne(\n leg: { provider: LLMProvider; model: string },\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n fetchImpl: typeof fetch,\n logger: Logger | undefined,\n nowFn?: () => number,\n): Promise<{ parsed: { content: string; input: number; output: number; cacheRead?: number; cacheWrite?: number; model?: string }; gatewayRequestId?: string; attempts: number }> {\n let req: { url: string; headers: Record<string, string>; body: string };\n switch (leg.provider) {\n case 'anthropic':\n req = buildAnthropicRequest(leg.model, messages, opts, env);\n break;\n case 'gemini':\n req = buildGeminiRequest(leg.model, messages, opts, env);\n break;\n case 'groq':\n req = buildGroqRequest(leg.model, messages, opts, env);\n break;\n case 'grok':\n req = buildGrokRequest(leg.model, messages, opts, env);\n break;\n }\n const { json, gatewayRequestId, attempts } = await callWithBackoff(\n leg.provider,\n req,\n fetchImpl,\n opts.signal,\n logger,\n nowFn,\n );\n switch (leg.provider) {\n case 'anthropic':\n return { parsed: parseAnthropic(json), gatewayRequestId, attempts };\n case 'gemini':\n return { parsed: parseGemini(json), gatewayRequestId, attempts };\n case 'groq':\n return { parsed: parseGroq(json), gatewayRequestId, attempts };\n case 'grok':\n return { parsed: parseGroq(json), gatewayRequestId, attempts };\n }\n}\n\n/**\n * Run a completion through the routing plan for the requested tier.\n *\n * Routing summary (0.3.0):\n * - `fast` → Anthropic Haiku\n * - `balanced` → Anthropic Sonnet; Gemini 2.5 Pro if `longContextThreshold` exceeded\n * - `smart` → Anthropic Opus; Gemini 2.5 Pro if long-context\n * - `verifier` → Groq Llama 3.3 70B (no fallback — verifier is inherently cheap/best-effort)\n *\n * All provider traffic flows through Cloudflare AI Gateway at `AI_GATEWAY_BASE_URL`.\n *\n * Per-provider reliability guarantees (0.4.0):\n * - Exponential backoff with jitter on 429 / 5xx (base 500ms, cap 8s, up to 2 retries).\n * - Provider cooldown: after exhausting retries the provider is marked cooling down\n * for 30 seconds; subsequent calls skip it and go straight to the fallback leg.\n *\n * @param messages - Ordered chat history.\n * @param env - API key + gateway bindings.\n * @param opts - Optional tier/model/parameters override.\n * @param deps - Optional fetch/logger/clock injection (for testing).\n * @returns A {@link FactoryResponse} carrying either an {@link LLMResult} or\n * an error (`LLM_ALL_PROVIDERS_FAILED`, `LLM_RATE_LIMITED`, or `INTERNAL_ERROR`).\n */\nexport async function complete(\n messages: LLMMessage[],\n env: LLMEnv,\n opts: LLMOptions = {},\n deps: LLMDeps = {},\n): Promise<FactoryResponse<LLMResult>> {\n if (messages.length === 0) {\n throw new ValidationError('messages must not be empty');\n }\n if (!env.AI_GATEWAY_BASE_URL) {\n throw new ValidationError('AI_GATEWAY_BASE_URL is required in 0.3.0');\n }\n const fetchImpl = deps.fetch ?? fetch;\n const now = deps.now ?? (() => Date.now());\n const logger = deps.logger;\n const startedAt = now();\n\n const tier: LLMTier = opts.tier ?? 'balanced';\n const system = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const tokenEstimate = estimateTokens(messages, system);\n const route = plan(tier, opts, tokenEstimate);\n\n const attemptLog: Array<{ provider: LLMProvider; status?: number; message: string }> = [];\n\n for (const leg of [route.primary, route.fallback].filter(Boolean) as Array<{ provider: LLMProvider; model: string }>) {\n // Skip providers that are currently in their cooldown window.\n if (isProviderCoolingDown(leg.provider, now)) {\n logger?.warn?.('llm.provider.coolingDown', { provider: leg.provider });\n attemptLog.push({ provider: leg.provider, message: 'skipped: cooling down' });\n continue;\n }\n try {\n const result = await callOne(leg, messages, opts, env, fetchImpl, logger, now);\n if (!result.parsed.content) {\n throw { provider: leg.provider, status: 200, retryable: false, message: 'empty content' } satisfies ProviderError;\n }\n logger?.info?.('llm.complete', {\n provider: leg.provider,\n model: leg.model,\n tier,\n tokenEstimate,\n attempts: result.attempts,\n runId: opts.runId,\n project: opts.project,\n actor: opts.actor,\n });\n return {\n data: {\n content: result.parsed.content,\n provider: leg.provider,\n model: result.parsed.model ?? leg.model,\n tier,\n tokens: {\n input: result.parsed.input,\n output: result.parsed.output,\n cacheRead: result.parsed.cacheRead,\n cacheWrite: result.parsed.cacheWrite,\n },\n latency: now() - startedAt,\n attempts: result.attempts,\n gatewayRequestId: result.gatewayRequestId,\n },\n error: null,\n };\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') {\n return toErrorResponse(\n new InternalError('llm call aborted', { provider: leg.provider, model: leg.model }),\n );\n }\n if (isProviderError(e)) {\n attemptLog.push({ provider: e.provider, status: e.status, message: e.message });\n if (e.status === 429 && !route.fallback) {\n return toErrorResponse(\n new RateLimitError(`llm rate limited on ${e.provider}`, { attempts: attemptLog }),\n );\n }\n logger?.warn?.('llm.leg.failed', { provider: leg.provider, status: e.status });\n continue;\n }\n attemptLog.push({ provider: leg.provider, message: e instanceof Error ? e.message : String(e) });\n }\n }\n\n return toErrorResponse(\n new InternalError('LLM_ALL_PROVIDERS_FAILED', { attempts: attemptLog, tier, tokenEstimate }),\n );\n}\n\n// ─── Streaming ────────────────────────────────────────────────────────────\n\n/**\n * Anthropic server-sent event shapes used by the streaming parser.\n * Only the fields we consume are typed; the rest are ignored.\n */\ninterface AnthropicStreamEvent {\n type: string;\n index?: number;\n delta?: { type?: string; text?: string };\n message?: {\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n model?: string;\n };\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n };\n}\n\n/**\n * Streams a completion from the primary Anthropic provider, yielding text chunks\n * as they arrive. Falls back to the non-streaming {@link complete} function when\n * the provider does not support streaming (i.e. a non-Anthropic primary is selected).\n *\n * The generator's **return value** (accessible via `gen.return()` or by consuming\n * the full iteration) is an {@link LLMResult} with the same shape as {@link complete}.\n *\n * Usage pattern:\n * ```ts\n * const gen = completionStream(messages, env, opts);\n * for await (const chunk of gen) {\n * // stream chunk to client\n * }\n * const result = (await gen.return(undefined)).value; // LLMResult\n * ```\n *\n * @param messages - Ordered chat history.\n * @param env - API key + gateway bindings.\n * @param opts - Optional tier/model/parameters override. Accepts `deps` as nested field.\n * @returns An async generator that yields `string` chunks and returns an {@link LLMResult}.\n */\nexport async function* completionStream(\n messages: LLMMessage[],\n env: LLMEnv,\n opts: LLMOptions & { deps?: LLMDeps } = {},\n): AsyncGenerator<string, LLMResult, unknown> {\n if (messages.length === 0) {\n throw new ValidationError('messages must not be empty');\n }\n if (!env.AI_GATEWAY_BASE_URL) {\n throw new ValidationError('AI_GATEWAY_BASE_URL is required in 0.3.0');\n }\n\n const deps: LLMDeps = opts.deps ?? {};\n const fetchImpl = deps.fetch ?? fetch;\n const now = deps.now ?? (() => Date.now());\n const logger = deps.logger;\n const startedAt = now();\n\n const tier: LLMTier = opts.tier ?? 'balanced';\n const system = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const tokenEstimate = estimateTokens(messages, system);\n const route = plan(tier, opts, tokenEstimate);\n\n // Only Anthropic supports streaming in the current implementation.\n // For all other primaries, fall back to non-streaming complete().\n if (route.primary.provider !== 'anthropic') {\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', { error: result.error });\n }\n yield result.data.content;\n return result.data;\n }\n\n // Check cooldown before attempting the streaming call.\n if (isProviderCoolingDown(route.primary.provider, now)) {\n logger?.warn?.('llm.provider.coolingDown', { provider: route.primary.provider });\n // Fall back to non-streaming complete() which will handle the fallback leg.\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', { error: result.error });\n }\n yield result.data.content;\n return result.data;\n }\n\n const req = buildAnthropicRequest(route.primary.model, messages, opts, env, true);\n\n let response: Response;\n try {\n response = await fetchImpl(req.url, {\n method: 'POST',\n headers: req.headers,\n body: req.body,\n // Fall back to a 60 s default when the caller provides no signal — prevents\n // a hung provider connection from consuming the Worker's wall-clock budget.\n signal: opts.signal ?? AbortSignal.timeout(60_000),\n });\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') {\n throw new InternalError('llm call aborted', {\n provider: route.primary.provider,\n model: route.primary.model,\n });\n }\n throw new InternalError('llm stream fetch failed', {\n message: e instanceof Error ? e.message : String(e),\n });\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n const retryable = isRetryableForBackoff(response.status);\n if (retryable && response.status === 429) {\n markProviderCoolingDown(route.primary.provider, now);\n }\n // Fall back to non-streaming complete() which will try the fallback leg.\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', {\n streamError: `${route.primary.provider} ${String(response.status)}: ${text.slice(0, 300)}`,\n error: result.error,\n });\n }\n yield result.data.content;\n return result.data;\n }\n\n if (!response.body) {\n throw new InternalError('llm stream response body is null', {\n provider: route.primary.provider,\n });\n }\n\n // Stream SSE events from Anthropic.\n const decoder = new TextDecoder();\n let accumulatedText = '';\n let inputTokens = 0;\n let outputTokens = 0;\n let cacheRead = 0;\n let cacheWrite = 0;\n let modelName: string | undefined;\n const gatewayRequestId: string | undefined = response.headers.get('cf-aig-request-id') ?? undefined;\n\n const reader = response.body.getReader();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // SSE lines are delimited by '\\n'. Events are separated by '\\n\\n'.\n const lines = buffer.split('\\n');\n // Keep the last (potentially incomplete) line in the buffer.\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const data = line.slice(6).trim();\n if (data === '[DONE]') break;\n let event: AnthropicStreamEvent;\n try {\n event = JSON.parse(data) as AnthropicStreamEvent;\n } catch {\n continue; // Skip malformed SSE lines.\n }\n\n switch (event.type) {\n case 'message_start':\n inputTokens = event.message?.usage?.input_tokens ?? 0;\n cacheRead = event.message?.usage?.cache_read_input_tokens ?? 0;\n cacheWrite = event.message?.usage?.cache_creation_input_tokens ?? 0;\n modelName = event.message?.model;\n break;\n case 'content_block_delta':\n if (event.delta?.type === 'text_delta' && typeof event.delta.text === 'string') {\n accumulatedText += event.delta.text;\n yield event.delta.text;\n }\n break;\n case 'message_delta':\n outputTokens = event.usage?.output_tokens ?? outputTokens;\n break;\n default:\n break;\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n clearProviderCooldown(route.primary.provider);\n logger?.info?.('llm.completionStream', {\n provider: route.primary.provider,\n model: route.primary.model,\n tier,\n tokenEstimate,\n runId: opts.runId,\n project: opts.project,\n actor: opts.actor,\n });\n\n return {\n content: accumulatedText,\n provider: route.primary.provider,\n model: modelName ?? route.primary.model,\n tier,\n tokens: { input: inputTokens, output: outputTokens, cacheRead, cacheWrite },\n latency: now() - startedAt,\n attempts: 1,\n gatewayRequestId,\n };\n}\n\n// ─── Grounding assertion ───────────────────────────────────────────────────\n\n/**\n * Returns `true` if `response` contains at least one verbatim phrase of at\n * least 5 consecutive whitespace-delimited tokens that also appears in one of\n * the `sources` strings.\n *\n * Returns `true` unconditionally when `sources` is empty (no grounding\n * documents means grounding cannot be violated).\n *\n * This is a lightweight guard for RAG pipelines — it detects obvious\n * hallucinations where the model generates content not present in any\n * retrieved source. It is NOT a semantic similarity check.\n *\n * @param response - The LLM-generated text to inspect.\n * @param sources - Retrieved source documents to check against.\n * @returns `true` if the response is grounded, `false` if hallucination detected.\n *\n * @example\n * ```ts\n * const grounded = assertGrounding(llmAnswer, retrievedDocs);\n * if (!grounded) {\n * // flag or re-rank the response\n * }\n * ```\n */\nexport function assertGrounding(response: string, sources: string[]): boolean {\n if (sources.length === 0) return true;\n\n const WINDOW = 5;\n const responseTokens = response.split(/\\s+/).filter((t) => t.length > 0);\n\n if (responseTokens.length < WINDOW) return false;\n\n // Build a set of all 5-token ngrams from each source for O(n) lookup.\n const sourceNgrams = new Set<string>();\n for (const source of sources) {\n const tokens = source.split(/\\s+/).filter((t) => t.length > 0);\n for (let i = 0; i <= tokens.length - WINDOW; i++) {\n const ngram = tokens.slice(i, i + WINDOW).join(' ');\n sourceNgrams.add(ngram);\n }\n }\n\n if (sourceNgrams.size === 0) return false;\n\n // Slide a window of WINDOW tokens over the response and check for a match.\n for (let i = 0; i <= responseTokens.length - WINDOW; i++) {\n const ngram = responseTokens.slice(i, i + WINDOW).join(' ');\n if (sourceNgrams.has(ngram)) return true;\n }\n\n return false;\n}\n\n// ─── Exported helpers (kept for existing consumers) ───────────────────────\n\nexport { MODELS, isProviderCoolingDown, markProviderCoolingDown, clearProviderCooldown, PROVIDER_COOLDOWN_MS };\nexport { BASE_BACKOFF_MS };\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAmGP,IAAM,SAAS;AAAA,EACb,WAAW;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,OAAO;AAAA,EACT;AAAA,EACA,QAAQ;AAAA,IACN,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,UAAU;AAAA,EACZ;AAAA,EACA,MAAM;AAAA;AAAA,IAEJ,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AACF;AAEA,IAAM,qBAAqB;AAC3B,IAAM,sBAAsB;AAC5B,IAAM,iCAAiC;AAIvC,IAAM,kBAAkB;AAExB,IAAM,iBAAiB;AAEvB,IAAM,wBAAwB;AAE9B,IAAM,4BAA4B;AAQlC,IAAM,wBAAkD,oBAAI,IAAI;AAGhE,IAAM,uBAAuB;AAM7B,SAAS,sBAAsB,UAAuB,MAAoB,KAAK,KAAc;AAC3F,QAAM,QAAQ,sBAAsB,IAAI,QAAQ;AAChD,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,IAAI,IAAI;AACjB;AAKA,SAAS,wBAAwB,UAAuB,MAAoB,KAAK,KAAW;AAC1F,wBAAsB,IAAI,UAAU,IAAI,IAAI,oBAAoB;AAClE;AAKA,SAAS,sBAAsB,UAA6B;AAC1D,wBAAsB,OAAO,QAAQ;AACvC;AAGA,IAAM,kBAAkB;AAaxB,SAAS,sBAAsB,QAAyB;AACtD,SAAO,WAAW,OAAQ,UAAU,OAAO,SAAS;AACtD;AAEA,SAAS,eAAe,UAAwB,QAAyB;AAEvE,MAAI,QAAQ,QAAQ,UAAU;AAC9B,aAAW,KAAK,SAAU,UAAS,EAAE,QAAQ;AAC7C,SAAO,KAAK,KAAK,QAAQ,CAAC;AAC5B;AAEA,SAAS,MAAM,IAAY,QAAqC;AAC9D,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,IAAI,WAAW,SAAS,EAAE;AAChC,QAAI,QAAQ;AACV,YAAM,UAAU,MAAM;AACpB,qBAAa,CAAC;AACd,eAAO,IAAI,aAAa,WAAW,YAAY,CAAC;AAAA,MAClD;AACA,UAAI,OAAO,QAAS,SAAQ;AAAA,UACvB,QAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,IAC/D;AAAA,EACF,CAAC;AACH;AAUA,SAAS,iBAAiB,SAAyB;AACjD,QAAM,SAAS,KAAK,MAAM,KAAK,OAAO,IAAI,qBAAqB;AAC/D,SAAO,KAAK,IAAI,kBAAkB,KAAK,IAAI,GAAG,OAAO,IAAI,QAAQ,cAAc;AACjF;AAIA,SAAS,sBACP,OACA,UACA,MACA,KACA,YAAY,OACoD;AAChE,QAAM,MAAM,KAAK,UAAU,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,GAAG;AACtE,QAAM,WAAW,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,QAAQ;AAC3D,QAAM,OAAgC;AAAA,IACpC;AAAA,IACA,YAAY,KAAK,aAAa;AAAA,IAC9B,aAAa,KAAK,eAAe;AAAA,IACjC,UAAU,SAAS,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,EAAE,QAAQ,EAAE;AAAA,EACtE;AACA,MAAI,WAAW;AACb,SAAK,SAAS;AAAA,EAChB;AACA,MAAI,KAAK;AACP,UAAM,QAAQ,KAAK,eAAe,IAAI,UAAU;AAChD,SAAK,SAAS,QACV,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,eAAe,EAAE,MAAM,YAAY,EAAE,CAAC,IAClE;AAAA,EACN;AACA,SAAO;AAAA,IACL,KAAK,GAAG,IAAI,mBAAmB;AAAA,IAC/B,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,aAAa,IAAI;AAAA,MACjB,qBAAqB;AAAA,MACrB,kBAAkB;AAAA,IACpB;AAAA,IACA,MAAM,KAAK,UAAU,IAAI;AAAA,EAC3B;AACF;AAEA,SAAS,mBACP,OACA,UACA,MACA,KACgE;AAChE,QAAM,MAAM,KAAK,UAAU,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,GAAG;AACtE,QAAM,WAAW,SACd,OAAO,CAAC,MAAM,EAAE,SAAS,QAAQ,EACjC,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,EAAE,SAAS,cAAc,UAAU;AAAA,IACzC,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC;AAAA,EAC7B,EAAE;AACJ,QAAM,OAAgC;AAAA,IACpC;AAAA,IACA,kBAAkB;AAAA,MAChB,iBAAiB,KAAK,aAAa;AAAA,MACnC,aAAa,KAAK,eAAe;AAAA,IACnC;AAAA,EACF;AACA,MAAI,KAAK;AACP,SAAK,oBAAoB,EAAE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC,EAAE;AAAA,EACpD;AACA,QAAM,OAAO,eAAe,IAAI,cAAc,cAAc,IAAI,eAAe,6BAA6B,KAAK;AACjH,SAAO;AAAA,IACL,KAAK,GAAG,IAAI,mBAAmB,qBAAqB,IAAI;AAAA,IACxD,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,IAAI,mBAAmB;AAAA,IAClD;AAAA,IACA,MAAM,KAAK,UAAU,IAAI;AAAA,EAC3B;AACF;AAEA,SAAS,iBACP,OACA,UACA,MACA,KACgE;AAChE,QAAM,MAAM,KAAK,UAAU,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,GAAG;AACtE,QAAM,SAAuB,CAAC;AAC9B,MAAI,IAAK,QAAO,KAAK,EAAE,MAAM,UAAU,SAAS,IAAI,CAAC;AACrD,aAAW,KAAK,SAAU,KAAI,EAAE,SAAS,SAAU,QAAO,KAAK,CAAC;AAChE,SAAO;AAAA,IACL,KAAK,GAAG,IAAI,mBAAmB;AAAA,IAC/B,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,IAAI,YAAY;AAAA,IAC3C;AAAA,IACA,MAAM,KAAK,UAAU;AAAA,MACnB;AAAA,MACA,YAAY,KAAK,aAAa;AAAA,MAC9B,aAAa,KAAK,eAAe;AAAA,MACjC,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACF;AAEA,SAAS,iBACP,OACA,UACA,MACA,KACgE;AAChE,MAAI,CAAC,IAAI,cAAc;AACrB,UAAM,IAAI,gBAAgB,iDAAiD;AAAA,EAC7E;AACA,QAAM,MAAM,KAAK,UAAU,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,GAAG;AACtE,QAAM,SAAuB,CAAC;AAC9B,MAAI,IAAK,QAAO,KAAK,EAAE,MAAM,UAAU,SAAS,IAAI,CAAC;AACrD,aAAW,KAAK,SAAU,KAAI,EAAE,SAAS,SAAU,QAAO,KAAK,CAAC;AAChE,SAAO;AAAA,IACL,KAAK,GAAG,IAAI,mBAAmB;AAAA,IAC/B,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,IAAI,YAAY;AAAA,IAC3C;AAAA,IACA,MAAM,KAAK,UAAU;AAAA,MACnB;AAAA,MACA,YAAY,KAAK,aAAa;AAAA,MAC9B,aAAa,KAAK,eAAe;AAAA,MACjC,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACF;AA6BA,SAAS,eACP,MAC2G;AAC3G,QAAM,IAAI;AACV,SAAO;AAAA,IACL,SAAS,EAAE,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM,GAAG,QAAQ;AAAA,IAC5D,OAAO,EAAE,OAAO,gBAAgB;AAAA,IAChC,QAAQ,EAAE,OAAO,iBAAiB;AAAA,IAClC,WAAW,EAAE,OAAO,2BAA2B;AAAA,IAC/C,YAAY,EAAE,OAAO,+BAA+B;AAAA,IACpD,OAAO,EAAE;AAAA,EACX;AACF;AAEA,SAAS,YAAY,MAAmE;AACtF,QAAM,IAAI;AACV,QAAM,OACJ,EAAE,aAAa,CAAC,GAAG,SAAS,OAAO,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK;AAC1E,SAAO;AAAA,IACL,SAAS;AAAA,IACT,OAAO,EAAE,eAAe,oBAAoB;AAAA,IAC5C,QAAQ,EAAE,eAAe,wBAAwB;AAAA,EACnD;AACF;AAEA,SAAS,UAAU,MAAmF;AACpG,QAAM,IAAI;AACV,SAAO;AAAA,IACL,SAAS,EAAE,UAAU,CAAC,GAAG,SAAS,WAAW;AAAA,IAC7C,OAAO,EAAE,OAAO,iBAAiB;AAAA,IACjC,QAAQ,EAAE,OAAO,qBAAqB;AAAA,IACtC,OAAO,EAAE;AAAA,EACX;AACF;AAmBA,eAAe,gBACb,UACA,SACA,WACA,QACA,QACA,OACyE;AAMzE,WAAS,gBAAgB,KAA2B;AAClD,4BAAwB,UAAU,SAAS,KAAK,GAAG;AACnD,UAAM;AAAA,EACR;AAEA,MAAI;AACJ,WAAS,UAAU,GAAG,WAAW,2BAA2B,WAAW;AACrE,QAAI;AACF,YAAM,WAAW,MAAM,UAAU,QAAQ,KAAK;AAAA,QAC5C,QAAQ;AAAA,QACR,SAAS,QAAQ;AAAA,QACjB,MAAM,QAAQ;AAAA,QACd;AAAA,MACF,CAAC;AACD,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,cAAM,YAAY,sBAAsB,SAAS,MAAM;AACvD,cAAM,MAAqB;AAAA,UACzB;AAAA,UACA,QAAQ,SAAS;AAAA,UACjB;AAAA,UACA,SAAS,GAAG,QAAQ,IAAI,OAAO,SAAS,MAAM,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,QACxE;AACA,gBAAQ,OAAO,sBAAsB,EAAE,UAAU,QAAQ,SAAS,QAAQ,QAAQ,CAAC;AACnF,YAAI,CAAC,IAAI,aAAa,YAAY,2BAA2B;AAC3D,cAAI,IAAI,UAAW,iBAAgB,GAAG;AACtC,gBAAM;AAAA,QACR;AACA,kBAAU;AAAA,MACZ,OAAO;AACL,cAAM,mBAAmB,SAAS,QAAQ,IAAI,mBAAmB,KAAK;AACtE,8BAAsB,QAAQ;AAC9B,eAAO,EAAE,MAAM,MAAM,SAAS,KAAK,GAAG,kBAAkB,UAAU,QAAQ;AAAA,MAC5E;AAAA,IACF,SAAS,GAAG;AACV,UAAI,aAAa,gBAAgB,EAAE,SAAS,aAAc,OAAM;AAChE,UAAI,OAAO,MAAM,YAAY,MAAM,QAAQ,eAAe,GAAG;AAC3D,cAAM,MAAM;AACZ,YAAI,CAAC,IAAI,aAAa,YAAY,2BAA2B;AAC3D,cAAI,IAAI,UAAW,iBAAgB,GAAG;AACtC,gBAAM;AAAA,QACR;AACA,kBAAU;AAAA,MACZ,OAAO;AACL,cAAM,MAAqB;AAAA,UACzB;AAAA,UACA,QAAQ;AAAA,UACR,WAAW;AAAA,UACX,SAAS,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,QACpD;AACA,YAAI,YAAY,0BAA2B,iBAAgB,GAAG;AAC9D,kBAAU;AAAA,MACZ;AAAA,IACF;AAEA,UAAM,YAAY,iBAAiB,UAAU,CAAC;AAC9C,UAAM,MAAM,WAAW,MAAM;AAAA,EAC/B;AAEA,0BAAwB,UAAU,SAAS,KAAK,GAAG;AACnD,QAAM,WAAY,EAAE,UAAU,QAAQ,GAAG,WAAW,OAAO,SAAS,YAAY;AAClF;AAEA,SAAS,gBAAgB,KAAoC;AAC3D,SACE,OAAO,QAAQ,YACf,QAAQ,QACR,OAAQ,IAA6B,WAAW,YAChD,OAAQ,IAA8B,YAAY,YAClD,OAAQ,IAA+B,aAAa;AAExD;AASA,SAAS,KAAK,MAAe,MAAkB,eAAkC;AAC/E,MAAI,KAAK,OAAO;AAEd,UAAM,IAAI,KAAK;AACf,QAAI,EAAE,WAAW,QAAQ,EAAG,QAAO,EAAE,SAAS,EAAE,UAAU,aAAa,OAAO,EAAE,EAAE;AAClF,QAAI,EAAE,WAAW,QAAQ,EAAG,QAAO,EAAE,SAAS,EAAE,UAAU,UAAU,OAAO,EAAE,EAAE;AAC/E,QAAI,EAAE,WAAW,MAAM,EAAG,QAAO,EAAE,SAAS,EAAE,UAAU,QAAQ,OAAO,EAAE,EAAE;AAC3E,WAAO,EAAE,SAAS,EAAE,UAAU,QAAQ,OAAO,EAAE,EAAE;AAAA,EACnD;AACA,QAAM,cAAc,kBAAkB,KAAK,wBAAwB;AACnE,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,EAAE,SAAS,EAAE,UAAU,QAAQ,OAAO,OAAO,KAAK,SAAS,EAAE;AAAA,IACtE,KAAK;AACH,aAAO,cACH;AAAA,QACE,SAAS,EAAE,UAAU,UAAU,OAAO,OAAO,OAAO,MAAM;AAAA,QAC1D,UAAU,EAAE,UAAU,aAAa,OAAO,OAAO,UAAU,MAAM;AAAA,MACnE,IACA;AAAA,QACE,SAAS,EAAE,UAAU,aAAa,OAAO,OAAO,UAAU,MAAM;AAAA,QAChE,UAAU,EAAE,UAAU,UAAU,OAAO,OAAO,OAAO,MAAM;AAAA,MAC7D;AAAA,IACN,KAAK;AACH,aAAO,EAAE,SAAS,EAAE,UAAU,aAAa,OAAO,OAAO,UAAU,KAAK,EAAE;AAAA,IAC5E,KAAK;AAAA,IACL;AACE,aAAO,cACH;AAAA,QACE,SAAS,EAAE,UAAU,UAAU,OAAO,OAAO,OAAO,MAAM;AAAA,QAC1D,UAAU,EAAE,UAAU,aAAa,OAAO,OAAO,UAAU,SAAS;AAAA,MACtE,IACA;AAAA,QACE,SAAS,EAAE,UAAU,aAAa,OAAO,OAAO,UAAU,SAAS;AAAA,QACnE,UAAU,EAAE,UAAU,UAAU,OAAO,OAAO,OAAO,MAAM;AAAA,MAC7D;AAAA,EACR;AACF;AAEA,eAAe,QACb,KACA,UACA,MACA,KACA,WACA,QACA,OAC+K;AAC/K,MAAI;AACJ,UAAQ,IAAI,UAAU;AAAA,IACpB,KAAK;AACH,YAAM,sBAAsB,IAAI,OAAO,UAAU,MAAM,GAAG;AAC1D;AAAA,IACF,KAAK;AACH,YAAM,mBAAmB,IAAI,OAAO,UAAU,MAAM,GAAG;AACvD;AAAA,IACF,KAAK;AACH,YAAM,iBAAiB,IAAI,OAAO,UAAU,MAAM,GAAG;AACrD;AAAA,IACF,KAAK;AACH,YAAM,iBAAiB,IAAI,OAAO,UAAU,MAAM,GAAG;AACrD;AAAA,EACJ;AACA,QAAM,EAAE,MAAM,kBAAkB,SAAS,IAAI,MAAM;AAAA,IACjD,IAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA,KAAK;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACA,UAAQ,IAAI,UAAU;AAAA,IACpB,KAAK;AACH,aAAO,EAAE,QAAQ,eAAe,IAAI,GAAG,kBAAkB,SAAS;AAAA,IACpE,KAAK;AACH,aAAO,EAAE,QAAQ,YAAY,IAAI,GAAG,kBAAkB,SAAS;AAAA,IACjE,KAAK;AACH,aAAO,EAAE,QAAQ,UAAU,IAAI,GAAG,kBAAkB,SAAS;AAAA,IAC/D,KAAK;AACH,aAAO,EAAE,QAAQ,UAAU,IAAI,GAAG,kBAAkB,SAAS;AAAA,EACjE;AACF;AAyBA,eAAsB,SACpB,UACA,KACA,OAAmB,CAAC,GACpB,OAAgB,CAAC,GACoB;AACrC,MAAI,SAAS,WAAW,GAAG;AACzB,UAAM,IAAI,gBAAgB,4BAA4B;AAAA,EACxD;AACA,MAAI,CAAC,IAAI,qBAAqB;AAC5B,UAAM,IAAI,gBAAgB,0CAA0C;AAAA,EACtE;AACA,QAAM,YAAY,KAAK,SAAS;AAChC,QAAM,MAAM,KAAK,QAAQ,MAAM,KAAK,IAAI;AACxC,QAAM,SAAS,KAAK;AACpB,QAAM,YAAY,IAAI;AAEtB,QAAM,OAAgB,KAAK,QAAQ;AACnC,QAAM,SAAS,KAAK,UAAU,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,GAAG;AACzE,QAAM,gBAAgB,eAAe,UAAU,MAAM;AACrD,QAAM,QAAQ,KAAK,MAAM,MAAM,aAAa;AAE5C,QAAM,aAAiF,CAAC;AAExF,aAAW,OAAO,CAAC,MAAM,SAAS,MAAM,QAAQ,EAAE,OAAO,OAAO,GAAsD;AAEpH,QAAI,sBAAsB,IAAI,UAAU,GAAG,GAAG;AAC5C,cAAQ,OAAO,4BAA4B,EAAE,UAAU,IAAI,SAAS,CAAC;AACrE,iBAAW,KAAK,EAAE,UAAU,IAAI,UAAU,SAAS,wBAAwB,CAAC;AAC5E;AAAA,IACF;AACA,QAAI;AACF,YAAM,SAAS,MAAM,QAAQ,KAAK,UAAU,MAAM,KAAK,WAAW,QAAQ,GAAG;AAC7E,UAAI,CAAC,OAAO,OAAO,SAAS;AAC1B,cAAM,EAAE,UAAU,IAAI,UAAU,QAAQ,KAAK,WAAW,OAAO,SAAS,gBAAgB;AAAA,MAC1F;AACA,cAAQ,OAAO,gBAAgB;AAAA,QAC7B,UAAU,IAAI;AAAA,QACd,OAAO,IAAI;AAAA,QACX;AAAA,QACA;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,OAAO,KAAK;AAAA,QACZ,SAAS,KAAK;AAAA,QACd,OAAO,KAAK;AAAA,MACd,CAAC;AACD,aAAO;AAAA,QACL,MAAM;AAAA,UACJ,SAAS,OAAO,OAAO;AAAA,UACvB,UAAU,IAAI;AAAA,UACd,OAAO,OAAO,OAAO,SAAS,IAAI;AAAA,UAClC;AAAA,UACA,QAAQ;AAAA,YACN,OAAO,OAAO,OAAO;AAAA,YACrB,QAAQ,OAAO,OAAO;AAAA,YACtB,WAAW,OAAO,OAAO;AAAA,YACzB,YAAY,OAAO,OAAO;AAAA,UAC5B;AAAA,UACA,SAAS,IAAI,IAAI;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,kBAAkB,OAAO;AAAA,QAC3B;AAAA,QACA,OAAO;AAAA,MACT;AAAA,IACF,SAAS,GAAG;AACV,UAAI,aAAa,gBAAgB,EAAE,SAAS,cAAc;AACxD,eAAO;AAAA,UACL,IAAI,cAAc,oBAAoB,EAAE,UAAU,IAAI,UAAU,OAAO,IAAI,MAAM,CAAC;AAAA,QACpF;AAAA,MACF;AACA,UAAI,gBAAgB,CAAC,GAAG;AACtB,mBAAW,KAAK,EAAE,UAAU,EAAE,UAAU,QAAQ,EAAE,QAAQ,SAAS,EAAE,QAAQ,CAAC;AAC9E,YAAI,EAAE,WAAW,OAAO,CAAC,MAAM,UAAU;AACvC,iBAAO;AAAA,YACL,IAAI,eAAe,uBAAuB,EAAE,QAAQ,IAAI,EAAE,UAAU,WAAW,CAAC;AAAA,UAClF;AAAA,QACF;AACA,gBAAQ,OAAO,kBAAkB,EAAE,UAAU,IAAI,UAAU,QAAQ,EAAE,OAAO,CAAC;AAC7E;AAAA,MACF;AACA,iBAAW,KAAK,EAAE,UAAU,IAAI,UAAU,SAAS,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,EAAE,CAAC;AAAA,IACjG;AAAA,EACF;AAEA,SAAO;AAAA,IACL,IAAI,cAAc,4BAA4B,EAAE,UAAU,YAAY,MAAM,cAAc,CAAC;AAAA,EAC7F;AACF;AAiDA,gBAAuB,iBACrB,UACA,KACA,OAAwC,CAAC,GACG;AAC5C,MAAI,SAAS,WAAW,GAAG;AACzB,UAAM,IAAI,gBAAgB,4BAA4B;AAAA,EACxD;AACA,MAAI,CAAC,IAAI,qBAAqB;AAC5B,UAAM,IAAI,gBAAgB,0CAA0C;AAAA,EACtE;AAEA,QAAM,OAAgB,KAAK,QAAQ,CAAC;AACpC,QAAM,YAAY,KAAK,SAAS;AAChC,QAAM,MAAM,KAAK,QAAQ,MAAM,KAAK,IAAI;AACxC,QAAM,SAAS,KAAK;AACpB,QAAM,YAAY,IAAI;AAEtB,QAAM,OAAgB,KAAK,QAAQ;AACnC,QAAM,SAAS,KAAK,UAAU,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,GAAG;AACzE,QAAM,gBAAgB,eAAe,UAAU,MAAM;AACrD,QAAM,QAAQ,KAAK,MAAM,MAAM,aAAa;AAI5C,MAAI,MAAM,QAAQ,aAAa,aAAa;AAC1C,UAAM,SAAS,MAAM,SAAS,UAAU,KAAK,MAAM,IAAI;AACvD,QAAI,OAAO,UAAU,QAAQ,OAAO,SAAS,MAAM;AACjD,YAAM,IAAI,cAAc,4BAA4B,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IAC7E;AACA,UAAM,OAAO,KAAK;AAClB,WAAO,OAAO;AAAA,EAChB;AAGA,MAAI,sBAAsB,MAAM,QAAQ,UAAU,GAAG,GAAG;AACtD,YAAQ,OAAO,4BAA4B,EAAE,UAAU,MAAM,QAAQ,SAAS,CAAC;AAE/E,UAAM,SAAS,MAAM,SAAS,UAAU,KAAK,MAAM,IAAI;AACvD,QAAI,OAAO,UAAU,QAAQ,OAAO,SAAS,MAAM;AACjD,YAAM,IAAI,cAAc,4BAA4B,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IAC7E;AACA,UAAM,OAAO,KAAK;AAClB,WAAO,OAAO;AAAA,EAChB;AAEA,QAAM,MAAM,sBAAsB,MAAM,QAAQ,OAAO,UAAU,MAAM,KAAK,IAAI;AAEhF,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,UAAU,IAAI,KAAK;AAAA,MAClC,QAAQ;AAAA,MACR,SAAS,IAAI;AAAA,MACb,MAAM,IAAI;AAAA;AAAA;AAAA,MAGV,QAAQ,KAAK,UAAU,YAAY,QAAQ,GAAM;AAAA,IACnD,CAAC;AAAA,EACH,SAAS,GAAG;AACV,QAAI,aAAa,gBAAgB,EAAE,SAAS,cAAc;AACxD,YAAM,IAAI,cAAc,oBAAoB;AAAA,QAC1C,UAAU,MAAM,QAAQ;AAAA,QACxB,OAAO,MAAM,QAAQ;AAAA,MACvB,CAAC;AAAA,IACH;AACA,UAAM,IAAI,cAAc,2BAA2B;AAAA,MACjD,SAAS,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,IACpD,CAAC;AAAA,EACH;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,UAAM,YAAY,sBAAsB,SAAS,MAAM;AACvD,QAAI,aAAa,SAAS,WAAW,KAAK;AACxC,8BAAwB,MAAM,QAAQ,UAAU,GAAG;AAAA,IACrD;AAEA,UAAM,SAAS,MAAM,SAAS,UAAU,KAAK,MAAM,IAAI;AACvD,QAAI,OAAO,UAAU,QAAQ,OAAO,SAAS,MAAM;AACjD,YAAM,IAAI,cAAc,4BAA4B;AAAA,QAClD,aAAa,GAAG,MAAM,QAAQ,QAAQ,IAAI,OAAO,SAAS,MAAM,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,QACxF,OAAO,OAAO;AAAA,MAChB,CAAC;AAAA,IACH;AACA,UAAM,OAAO,KAAK;AAClB,WAAO,OAAO;AAAA,EAChB;AAEA,MAAI,CAAC,SAAS,MAAM;AAClB,UAAM,IAAI,cAAc,oCAAoC;AAAA,MAC1D,UAAU,MAAM,QAAQ;AAAA,IAC1B,CAAC;AAAA,EACH;AAGA,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,kBAAkB;AACtB,MAAI,cAAc;AAClB,MAAI,eAAe;AACnB,MAAI,YAAY;AAChB,MAAI,aAAa;AACjB,MAAI;AACJ,QAAM,mBAAuC,SAAS,QAAQ,IAAI,mBAAmB,KAAK;AAE1F,QAAM,SAAS,SAAS,KAAK,UAAU;AACvC,MAAI,SAAS;AAEb,MAAI;AACF,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AACV,gBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAGhD,YAAM,QAAQ,OAAO,MAAM,IAAI;AAE/B,eAAS,MAAM,IAAI,KAAK;AAExB,iBAAW,QAAQ,OAAO;AACxB,YAAI,CAAC,KAAK,WAAW,QAAQ,EAAG;AAChC,cAAM,OAAO,KAAK,MAAM,CAAC,EAAE,KAAK;AAChC,YAAI,SAAS,SAAU;AACvB,YAAI;AACJ,YAAI;AACF,kBAAQ,KAAK,MAAM,IAAI;AAAA,QACzB,QAAQ;AACN;AAAA,QACF;AAEA,gBAAQ,MAAM,MAAM;AAAA,UAClB,KAAK;AACH,0BAAc,MAAM,SAAS,OAAO,gBAAgB;AACpD,wBAAY,MAAM,SAAS,OAAO,2BAA2B;AAC7D,yBAAa,MAAM,SAAS,OAAO,+BAA+B;AAClE,wBAAY,MAAM,SAAS;AAC3B;AAAA,UACF,KAAK;AACH,gBAAI,MAAM,OAAO,SAAS,gBAAgB,OAAO,MAAM,MAAM,SAAS,UAAU;AAC9E,iCAAmB,MAAM,MAAM;AAC/B,oBAAM,MAAM,MAAM;AAAA,YACpB;AACA;AAAA,UACF,KAAK;AACH,2BAAe,MAAM,OAAO,iBAAiB;AAC7C;AAAA,UACF;AACE;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AAEA,wBAAsB,MAAM,QAAQ,QAAQ;AAC5C,UAAQ,OAAO,wBAAwB;AAAA,IACrC,UAAU,MAAM,QAAQ;AAAA,IACxB,OAAO,MAAM,QAAQ;AAAA,IACrB;AAAA,IACA;AAAA,IACA,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,OAAO,KAAK;AAAA,EACd,CAAC;AAED,SAAO;AAAA,IACL,SAAS;AAAA,IACT,UAAU,MAAM,QAAQ;AAAA,IACxB,OAAO,aAAa,MAAM,QAAQ;AAAA,IAClC;AAAA,IACA,QAAQ,EAAE,OAAO,aAAa,QAAQ,cAAc,WAAW,WAAW;AAAA,IAC1E,SAAS,IAAI,IAAI;AAAA,IACjB,UAAU;AAAA,IACV;AAAA,EACF;AACF;AA4BO,SAAS,gBAAgB,UAAkB,SAA4B;AAC5E,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,SAAS;AACf,QAAM,iBAAiB,SAAS,MAAM,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAEvE,MAAI,eAAe,SAAS,OAAQ,QAAO;AAG3C,QAAM,eAAe,oBAAI,IAAY;AACrC,aAAW,UAAU,SAAS;AAC5B,UAAM,SAAS,OAAO,MAAM,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7D,aAAS,IAAI,GAAG,KAAK,OAAO,SAAS,QAAQ,KAAK;AAChD,YAAM,QAAQ,OAAO,MAAM,GAAG,IAAI,MAAM,EAAE,KAAK,GAAG;AAClD,mBAAa,IAAI,KAAK;AAAA,IACxB;AAAA,EACF;AAEA,MAAI,aAAa,SAAS,EAAG,QAAO;AAGpC,WAAS,IAAI,GAAG,KAAK,eAAe,SAAS,QAAQ,KAAK;AACxD,UAAM,QAAQ,eAAe,MAAM,GAAG,IAAI,MAAM,EAAE,KAAK,GAAG;AAC1D,QAAI,aAAa,IAAI,KAAK,EAAG,QAAO;AAAA,EACtC;AAEA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n InternalError,\n RateLimitError,\n ValidationError,\n toErrorResponse,\n type FactoryResponse,\n} from '@latimer-woods-tech/errors';\nimport type { Logger } from '@latimer-woods-tech/logger';\n\n/**\n * Single chat message exchanged with an LLM provider.\n */\nexport interface LLMMessage {\n role: 'user' | 'assistant' | 'system';\n content: string;\n}\n\n/**\n * Quality tier selected by the caller. Routing is workload-split:\n * - `fast` → Grok 4.3 with Anthropic Haiku fallback (routine drafts/small jobs)\n * - `balanced` → Anthropic Sonnet (default)\n * - `smart` → Anthropic Opus OR Gemini 2.5 Pro if input is long-context (>150k tokens estimated)\n * - `verifier` → Groq Llama (cheap second opinion; only used from verifier code path)\n * - `workbench` → DeepSeek Chat with Groq fallback (boring, reviewable, non-sensitive batch work)\n */\nexport type LLMTier = 'fast' | 'balanced' | 'smart' | 'verifier' | 'workbench';\n\n/**\n * Options that influence LLM completion behaviour.\n */\nexport interface LLMOptions {\n /** Quality tier; see {@link LLMTier}. Defaults to `balanced`. */\n tier?: LLMTier;\n /** Explicit model override. Takes precedence over tier. */\n model?: string;\n maxTokens?: number;\n temperature?: number;\n system?: string;\n /** Token budget above which we force long-context routing (Gemini). */\n longContextThreshold?: number;\n /** Per-call cancellation signal. Aborts the in-flight provider request. */\n signal?: AbortSignal;\n /** Optional run identifier stamped on ledger rows + logs. */\n runId?: string;\n /** Optional project identifier stamped on ledger rows + logs. */\n project?: string;\n /** Optional actor identifier (supervisor / worker / human). */\n actor?: string;\n /** Optional workload label used in logs and cost-policy call sites. */\n workload?: string;\n /** Grok reasoning effort. Defaults to `none` for cost-controlled fast/draft calls. */\n reasoningEffort?: 'none' | 'low' | 'medium' | 'high';\n /** Anthropic prompt-cache control. Defaults to `true` for `system` prompts ≥ 1024 tokens. */\n promptCache?: boolean;\n /**\n * Maximum estimated cost in USD for this completion.\n * This cap is enforced after the provider returns because it uses actual\n * response token counts to compute the final cost.\n * If the post-call estimated cost exceeds this cap, `complete` returns a\n * {@link RateLimitError} with code `LLM_COST_CAP_EXCEEDED` and\n * `completionStream` throws the same error.\n * Pricing is based on {@link MODEL_PRICE_PER_1M}; unknown models default to\n * Opus rates (conservative upper bound).\n */\n maxCostUsd?: number;\n /**\n * Org-level daily cost cap in USD. Requires `env.LLM_COST_KV` to be set.\n * When today's cumulative spend read from KV is >= this value, `complete`\n * returns a {@link RateLimitError} with code `LLM_DAILY_CAP_EXCEEDED`\n * without making any provider call. After a successful call the daily\n * accumulator is updated in KV (TTL: 48 h).\n */\n dailyCapUsd?: number;\n /**\n * Org-level monthly cost cap in USD. Requires `env.LLM_COST_KV` to be set.\n * Same enforcement pattern as {@link dailyCapUsd} but keyed by YYYY-MM.\n * KV TTL: 40 days.\n */\n monthlyCapUsd?: number;\n /**\n * Metering context. When supplied and `deps.onRecord` is set, a {@link LLMRecordRow}\n * is emitted after every successful completion. Errors are swallowed.\n */\n ledger?: LLMRecordContext;\n}\n\n/**\n * Provider that produced an LLM response.\n */\nexport type LLMProvider = 'anthropic' | 'gemini' | 'groq' | 'grok' | 'deepseek';\n\n/**\n * Result returned by a successful completion.\n */\nexport interface LLMResult {\n content: string;\n provider: LLMProvider;\n model: string;\n tier: LLMTier;\n tokens: { input: number; output: number; cacheRead?: number; cacheWrite?: number };\n latency: number;\n /** Number of attempts before success (1 = primary succeeded). */\n attempts: number;\n /** Monotonic request id from AI Gateway, if present in headers. */\n gatewayRequestId?: string;\n}\n\n/**\n * Environment bindings required by {@link complete}.\n *\n * `AI_GATEWAY_BASE_URL` is REQUIRED in 0.3.0. All provider calls flow through the\n * Cloudflare AI Gateway for unified logging, rate limiting, and cost telemetry.\n * In test/dev the caller may pass a custom fetch impl that short-circuits this.\n */\nexport interface LLMEnv {\n AI_GATEWAY_BASE_URL: string;\n ANTHROPIC_API_KEY: string;\n GROQ_API_KEY: string;\n /** Optional — only required for `{ tier: 'workbench' }` or `deepseek-*` model overrides. */\n DEEPSEEK_API_KEY?: string;\n /** Optional — only required when caller passes `{ model: 'grok-*' }` override. */\n GROK_API_KEY?: string;\n /**\n * Google Cloud short-lived access token with `aiplatform.endpoints.predict`.\n * Callers mint this via the JWT-bearer flow (service account → token exchange);\n * see `docs/runbooks/rotate-gcp-sa.md`. Token must be valid for ≥ 5 minutes.\n */\n VERTEX_ACCESS_TOKEN: string;\n VERTEX_PROJECT: string;\n VERTEX_LOCATION: string;\n /**\n * Optional KV store for org-level daily/monthly cost tracking and enforcement.\n * When provided alongside {@link LLMOptions.dailyCapUsd} or {@link LLMOptions.monthlyCapUsd},\n * `complete` will block calls that would exceed the declared cap.\n * Any KV-like store satisfying `get`/`put` works (e.g. Cloudflare KV, in-memory stub).\n */\n LLM_COST_KV?: CostKvStore;\n}\n\n/**\n * Minimal KV store interface for org-level LLM cost tracking.\n * Cloudflare KV satisfies this. An in-memory stub is sufficient for tests.\n */\nexport interface CostKvStore {\n get(key: string): Promise<string | null>;\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n}\n\n/**\n * Caller-supplied context stamped on every metering row.\n * Mirrors the `LLMRecordContext` in `@latimer-woods-tech/llm-meter`; kept inline\n * to avoid a circular dependency (llm-meter imports llm).\n */\nexport interface LLMRecordContext {\n project: string;\n actor: string;\n runId?: string;\n workload?: string;\n tenantId?: string;\n}\n\n/**\n * Row shape passed to the optional {@link LLMDeps.onRecord} callback.\n * Callers can wire this directly to `recordCall` from `@latimer-woods-tech/llm-meter`.\n */\nexport interface LLMRecordRow extends LLMRecordContext {\n model: string;\n provider: LLMProvider;\n tier: LLMTier;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n latencyMs: number;\n costUsd: number;\n yyyyMm: string;\n}\n\n/**\n * Optional dependencies for {@link complete}.\n */\nexport interface LLMDeps {\n fetch?: typeof fetch;\n logger?: Logger;\n now?: () => number;\n /**\n * Optional metering callback. Called after every successful completion.\n * Errors are swallowed so metering never blocks the caller.\n * Wire to `recordCall` from `@latimer-woods-tech/llm-meter`.\n */\n onRecord?: (row: LLMRecordRow) => Promise<void>;\n}\n\n// Model catalogue — keep in sync with docs/architecture/FACTORY_V1.md § LLM substrate.\nconst MODELS = {\n anthropic: {\n fast: 'claude-haiku-4-20250514',\n balanced: 'claude-sonnet-4-6',\n smart: 'claude-opus-4-7',\n },\n gemini: {\n smart: 'gemini-2.5-pro',\n },\n groq: {\n verifier: 'llama-4-maverick',\n },\n grok: {\n fast: 'grok-4.3',\n },\n deepseek: {\n workbench: 'deepseek-chat',\n },\n} as const;\n\nconst DEFAULT_MAX_TOKENS = 1024;\nconst DEFAULT_TEMPERATURE = 0.7;\nconst DEFAULT_LONG_CONTEXT_THRESHOLD = 150_000; // tokens\n\n// ─── Per-provider exponential backoff constants ────────────────────────────\n/** Base delay in ms for the first retry. */\nconst BACKOFF_BASE_MS = 500;\n/** Maximum backoff cap in ms. */\nconst BACKOFF_CAP_MS = 8_000;\n/** Max random jitter added to each backoff delay, in ms. */\nconst BACKOFF_JITTER_MAX_MS = 250;\n/** Maximum number of attempts per provider (1 initial + 2 retries). */\nconst PER_PROVIDER_MAX_ATTEMPTS = 3;\n\n// ─── Per-provider cooldown state (module-level) ────────────────────────────\n/**\n * Tracks when a provider's cooldown period expires.\n * Keyed by {@link LLMProvider}; value is the `Date.now()` epoch ms at which\n * the cooldown expires. Absent key means \"not cooling down\".\n */\nconst providerCooldownUntil: Map<LLMProvider, number> = new Map();\n\n/** Cooldown duration in ms after a provider exhausts all retries. */\nconst PROVIDER_COOLDOWN_MS = 30_000;\n\n/**\n * Returns `true` if the provider is currently in its cooldown window.\n * Uses the injected `now` function (or `Date.now`) for testability.\n */\nfunction isProviderCoolingDown(provider: LLMProvider, now: () => number = Date.now): boolean {\n const until = providerCooldownUntil.get(provider);\n if (until === undefined) return false;\n return now() < until;\n}\n\n/** Returns `YYYY-MM-DD` from a Unix timestamp (ms). Used for daily KV cost keys. */\nfunction isoDate(nowMs: number): string {\n return new Date(nowMs).toISOString().slice(0, 10);\n}\n\n/**\n * Record actual call spend in org-level daily/monthly KV buckets.\n * This is intentionally best-effort: Cloudflare KV does not provide an atomic\n * compare-and-swap, so concurrent requests can race and undercount spend.\n */\nasync function recordOrgCostUsage(\n kv: CostKvStore,\n todayKey: string,\n monthKey: string,\n costUsd: number,\n opts: LLMOptions,\n): Promise<void> {\n if (opts.dailyCapUsd !== undefined) {\n const raw = await kv.get(todayKey).catch(() => null);\n const spent = parseFloat(raw ?? '0');\n await kv.put(todayKey, String(spent + costUsd), { expirationTtl: 172_800 /* 48 h */ }).catch(() => undefined);\n }\n if (opts.monthlyCapUsd !== undefined) {\n const raw = await kv.get(monthKey).catch(() => null);\n const spent = parseFloat(raw ?? '0');\n await kv.put(monthKey, String(spent + costUsd), { expirationTtl: 3_456_000 /* 40 d */ }).catch(() => undefined);\n }\n}\n\n/**\n * USD cost per 1 million tokens for each model.\n * Source: Anthropic / Google / xAI pricing pages as of 2026-05.\n * Keep these model names in sync with the default routing constants in\n * {@link MODELS}; unknown models fall back to Opus rates (conservative upper bound).\n *\n * CANONICAL pricing source for the platform. `@latimer-woods-tech/llm-meter`\n * derives its cents-denominated rates from this table and a drift-guard test\n * there fails CI if they diverge — make all rate changes here.\n */\nexport const MODEL_PRICE_PER_1M: Record<string, { input: number; output: number; cacheRead: number; cacheWrite: number }> = {\n // Anthropic Haiku 4\n 'claude-haiku-4-20250514': { input: 0.80, output: 4.00, cacheRead: 0.08, cacheWrite: 1.00 },\n 'claude-haiku-4-5-20251001': { input: 0.80, output: 4.00, cacheRead: 0.08, cacheWrite: 1.00 },\n // Anthropic Sonnet 4\n 'claude-sonnet-4-20250514': { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },\n 'claude-sonnet-4-6': { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },\n // Anthropic Opus 4\n 'claude-opus-4-20250514': { input: 15.00, output: 75.00, cacheRead: 1.50, cacheWrite: 18.75 },\n 'claude-opus-4-7': { input: 15.00, output: 75.00, cacheRead: 1.50, cacheWrite: 18.75 },\n // Gemini 2.5 Pro\n 'gemini-2.5-pro': { input: 1.25, output: 10.00, cacheRead: 0.31, cacheWrite: 4.50 },\n // Groq Llama 4 Maverick\n 'llama-4-maverick': { input: 0.50, output: 0.77, cacheRead: 0.05, cacheWrite: 0.50 },\n // Grok 4.3\n 'grok-4.3': { input: 1.25, output: 2.50, cacheRead: 0.00, cacheWrite: 0.00 },\n // DeepSeek API pricing as of 2026-05: cache-write conservatively uses cache-miss input pricing.\n 'deepseek-chat': { input: 0.27, output: 1.10, cacheRead: 0.07, cacheWrite: 0.27 },\n 'deepseek-reasoner': { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 },\n // Deprecated aliases retained for historical ledger rows.\n 'grok-4-fast': { input: 1.25, output: 2.50, cacheRead: 0.00, cacheWrite: 0.00 },\n 'grok-3-mini-latest': { input: 1.25, output: 2.50, cacheRead: 0.00, cacheWrite: 0.00 },\n};\n\n/** Fallback pricing used for unrecognised models (Opus rates — conservative upper bound). */\nconst PRICE_FALLBACK = MODEL_PRICE_PER_1M['claude-opus-4-7']!;\n\n/**\n * Estimates the USD cost of a single LLM completion from token counts.\n * Returns 0 for zero-token results. Uses {@link MODEL_PRICE_PER_1M} with\n * {@link PRICE_FALLBACK} for unknown models.\n */\nfunction estimateCostUsd(\n tokens: { input: number; output: number; cacheRead?: number; cacheWrite?: number },\n model: string,\n): number {\n const price = MODEL_PRICE_PER_1M[model] ?? PRICE_FALLBACK;\n return (\n (tokens.input * price.input +\n tokens.output * price.output +\n (tokens.cacheRead ?? 0) * price.cacheRead +\n (tokens.cacheWrite ?? 0) * price.cacheWrite) /\n 1_000_000\n );\n}\n\n/** Returns `YYYY-MM` from a Unix timestamp (ms). Used for monthly KV cost keys. */\nfunction isoMonth(nowMs: number): string {\n return new Date(nowMs).toISOString().slice(0, 7);\n}\n\n/**\n * Marks a provider as cooling down for {@link PROVIDER_COOLDOWN_MS} milliseconds.\n */\nfunction markProviderCoolingDown(provider: LLMProvider, now: () => number = Date.now): void {\n providerCooldownUntil.set(provider, now() + PROVIDER_COOLDOWN_MS);\n}\n\n/**\n * Clears the cooldown state for a provider after a successful call.\n */\nfunction clearProviderCooldown(provider: LLMProvider): void {\n providerCooldownUntil.delete(provider);\n}\n\n// ─── Legacy backoff constant (kept for the existing callWithBackoff signature) ─\nconst BASE_BACKOFF_MS = 250;\n\ninterface ProviderError {\n provider: LLMProvider;\n status: number;\n retryable: boolean;\n message: string;\n}\n\n/**\n * Returns `true` for status codes that should trigger a retry.\n * Only 429 and 5xx (transient server errors) qualify; other 4xx are terminal.\n */\nfunction isRetryableForBackoff(status: number): boolean {\n return status === 429 || (status >= 500 && status < 600);\n}\n\nfunction estimateTokens(messages: LLMMessage[], system?: string): number {\n // Cheap estimator: ~4 chars/token. Good enough for threshold routing.\n let chars = system?.length ?? 0;\n for (const m of messages) chars += m.content.length;\n return Math.ceil(chars / 4);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const t = setTimeout(resolve, ms);\n if (signal) {\n const onAbort = () => {\n clearTimeout(t);\n reject(new DOMException('Aborted', 'AbortError'));\n };\n if (signal.aborted) onAbort();\n else signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n}\n\n/**\n * Computes the exponential backoff delay for a given attempt with jitter.\n *\n * Formula: `Math.min(base * 2^attempt + jitter, cap)`\n * where `jitter` is a random value in `[0, BACKOFF_JITTER_MAX_MS)`.\n *\n * @param attempt - Zero-based attempt index (0 = first retry after initial failure).\n */\nfunction computeBackoffMs(attempt: number): number {\n const jitter = Math.floor(Math.random() * BACKOFF_JITTER_MAX_MS);\n return Math.min(BACKOFF_BASE_MS * Math.pow(2, attempt) + jitter, BACKOFF_CAP_MS);\n}\n\n// ─── Provider request builders ─────────────────────────────────────────────\n\nfunction buildAnthropicRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n streaming = false,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const filtered = messages.filter((m) => m.role !== 'system');\n const body: Record<string, unknown> = {\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: filtered.map((m) => ({ role: m.role, content: m.content })),\n };\n if (streaming) {\n body.stream = true;\n }\n if (sys) {\n const cache = opts.promptCache ?? sys.length >= 4096;\n body.system = cache\n ? [{ type: 'text', text: sys, cache_control: { type: 'ephemeral' } }]\n : sys;\n }\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/anthropic/v1/messages`,\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': env.ANTHROPIC_API_KEY,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n };\n}\n\nfunction buildGeminiRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const contents = messages\n .filter((m) => m.role !== 'system')\n .map((m) => ({\n role: m.role === 'assistant' ? 'model' : 'user',\n parts: [{ text: m.content }],\n }));\n const body: Record<string, unknown> = {\n contents,\n generationConfig: {\n maxOutputTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n },\n };\n if (sys) {\n body.systemInstruction = { parts: [{ text: sys }] };\n }\n const path = `v1/projects/${env.VERTEX_PROJECT}/locations/${env.VERTEX_LOCATION}/publishers/google/models/${model}:generateContent`;\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/google-vertex-ai/${path}`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.VERTEX_ACCESS_TOKEN}`,\n },\n body: JSON.stringify(body),\n };\n}\n\nfunction buildGroqRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const merged: LLMMessage[] = [];\n if (sys) merged.push({ role: 'system', content: sys });\n for (const m of messages) if (m.role !== 'system') merged.push(m);\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/groq/openai/v1/chat/completions`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.GROQ_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: merged,\n }),\n };\n}\n\nfunction buildGrokRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n if (!env.GROK_API_KEY) {\n throw new ValidationError('GROK_API_KEY required for grok-* model override');\n }\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const merged: LLMMessage[] = [];\n if (sys) merged.push({ role: 'system', content: sys });\n for (const m of messages) if (m.role !== 'system') merged.push(m);\n const body: Record<string, unknown> = {\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: merged,\n };\n if (model === MODELS.grok.fast) {\n body.reasoning_effort = opts.reasoningEffort ?? 'none';\n }\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/grok/v1/chat/completions`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.GROK_API_KEY}`,\n },\n body: JSON.stringify(body),\n };\n}\n\nfunction buildDeepSeekRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n if (!env.DEEPSEEK_API_KEY) {\n throw new ValidationError('DEEPSEEK_API_KEY required for workbench tier or deepseek-* model override');\n }\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const merged: LLMMessage[] = [];\n if (sys) merged.push({ role: 'system', content: sys });\n for (const m of messages) if (m.role !== 'system') merged.push(m);\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/deepseek/chat/completions`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: merged,\n }),\n };\n}\n\n// ─── Response parsers ──────────────────────────────────────────────────────\n\ninterface AnthropicResponse {\n content?: Array<{ type: string; text?: string }>;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n model?: string;\n}\n\ninterface GeminiResponse {\n candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>;\n usageMetadata?: {\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n };\n}\n\ninterface GroqResponse {\n choices?: Array<{ message?: { content?: string } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n model?: string;\n}\n\nfunction parseAnthropic(\n json: unknown,\n): { content: string; input: number; output: number; cacheRead: number; cacheWrite: number; model?: string } {\n const r = json as AnthropicResponse;\n return {\n content: r.content?.find((c) => c.type === 'text')?.text ?? '',\n input: r.usage?.input_tokens ?? 0,\n output: r.usage?.output_tokens ?? 0,\n cacheRead: r.usage?.cache_read_input_tokens ?? 0,\n cacheWrite: r.usage?.cache_creation_input_tokens ?? 0,\n model: r.model,\n };\n}\n\nfunction parseGemini(json: unknown): { content: string; input: number; output: number } {\n const r = json as GeminiResponse;\n const text =\n r.candidates?.[0]?.content?.parts?.map((p) => p.text ?? '').join('') ?? '';\n return {\n content: text,\n input: r.usageMetadata?.promptTokenCount ?? 0,\n output: r.usageMetadata?.candidatesTokenCount ?? 0,\n };\n}\n\nfunction parseGroq(json: unknown): { content: string; input: number; output: number; model?: string } {\n const r = json as GroqResponse;\n return {\n content: r.choices?.[0]?.message?.content ?? '',\n input: r.usage?.prompt_tokens ?? 0,\n output: r.usage?.completion_tokens ?? 0,\n model: r.model,\n };\n}\n\n// ─── Core call with backoff ────────────────────────────────────────────────\n\n/**\n * Calls a provider with per-provider exponential backoff.\n *\n * Retries up to {@link PER_PROVIDER_MAX_ATTEMPTS} times on 429 or transient 5xx.\n * Other 4xx codes are treated as terminal and not retried.\n * AbortError is never retried — it bubbles immediately.\n *\n * @param provider - Provider name, used for error tagging.\n * @param request - Pre-built HTTP request descriptor.\n * @param fetchImpl - Fetch implementation (injectable for tests).\n * @param signal - Optional AbortSignal for cancellation.\n * @param logger - Optional logger for per-attempt warnings.\n * @param nowFn - Optional clock injection for testability.\n * @returns Parsed JSON body, optional AI Gateway request ID, and attempt count.\n */\nasync function callWithBackoff(\n provider: LLMProvider,\n request: { url: string; headers: Record<string, string>; body: string },\n fetchImpl: typeof fetch,\n signal: AbortSignal | undefined,\n logger: Logger | undefined,\n nowFn?: () => number,\n): Promise<{ json: unknown; gatewayRequestId?: string; attempts: number }> {\n /**\n * Helper: mark provider cooling down and then throw the error.\n * Called whenever we determine we've exhausted all retries for the provider.\n * AbortError is never counted as a provider exhaustion — it bypasses this.\n */\n function exhaustAndThrow(err: ProviderError): never {\n markProviderCoolingDown(provider, nowFn ?? Date.now);\n throw err;\n }\n\n let lastErr: ProviderError | undefined;\n for (let attempt = 1; attempt <= PER_PROVIDER_MAX_ATTEMPTS; attempt++) {\n try {\n const response = await fetchImpl(request.url, {\n method: 'POST',\n headers: request.headers,\n body: request.body,\n signal,\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n const retryable = isRetryableForBackoff(response.status);\n const err: ProviderError = {\n provider,\n status: response.status,\n retryable,\n message: `${provider} ${String(response.status)}: ${text.slice(0, 300)}`,\n };\n logger?.warn?.('llm.provider.error', { provider, status: response.status, attempt });\n if (!err.retryable || attempt === PER_PROVIDER_MAX_ATTEMPTS) {\n if (err.retryable) exhaustAndThrow(err); // retryable but exhausted\n throw err; // terminal non-retryable error — no cooldown\n }\n lastErr = err;\n } else {\n const gatewayRequestId = response.headers.get('cf-aig-request-id') ?? undefined;\n clearProviderCooldown(provider);\n return { json: await response.json(), gatewayRequestId, attempts: attempt };\n }\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n if (typeof e === 'object' && e !== null && 'retryable' in e) {\n const err = e as ProviderError;\n if (!err.retryable || attempt === PER_PROVIDER_MAX_ATTEMPTS) {\n if (err.retryable) exhaustAndThrow(err); // retryable but exhausted\n throw err; // terminal — no cooldown\n }\n lastErr = err;\n } else {\n const err: ProviderError = {\n provider,\n status: 0,\n retryable: true,\n message: e instanceof Error ? e.message : String(e),\n };\n if (attempt === PER_PROVIDER_MAX_ATTEMPTS) exhaustAndThrow(err);\n lastErr = err;\n }\n }\n // Exponential backoff with jitter: base=500ms, cap=8000ms, jitter up to 250ms\n const backoffMs = computeBackoffMs(attempt - 1);\n await sleep(backoffMs, signal);\n }\n // Fallthrough — should not be reached, but mark cooling down defensively.\n markProviderCoolingDown(provider, nowFn ?? Date.now);\n throw lastErr ?? ({ provider, status: 0, retryable: false, message: 'exhausted' } as ProviderError);\n}\n\nfunction isProviderError(err: unknown): err is ProviderError {\n return (\n typeof err === 'object' &&\n err !== null &&\n typeof (err as { status?: unknown }).status === 'number' &&\n typeof (err as { message?: unknown }).message === 'string' &&\n typeof (err as { provider?: unknown }).provider === 'string'\n );\n}\n\n// ─── Routing ───────────────────────────────────────────────────────────────\n\ninterface RoutePlan {\n primary: { provider: LLMProvider; model: string };\n fallback?: { provider: LLMProvider; model: string };\n}\n\nfunction plan(tier: LLMTier, opts: LLMOptions, tokenEstimate: number): RoutePlan {\n if (opts.model) {\n // Explicit override — best-effort provider detection.\n const m = opts.model;\n if (m.startsWith('claude')) return { primary: { provider: 'anthropic', model: m } };\n if (m.startsWith('gemini')) return { primary: { provider: 'gemini', model: m } };\n if (m.startsWith('grok')) return { primary: { provider: 'grok', model: m } };\n if (m.startsWith('deepseek')) return { primary: { provider: 'deepseek', model: m } };\n return { primary: { provider: 'groq', model: m } };\n }\n const longContext = tokenEstimate >= (opts.longContextThreshold ?? DEFAULT_LONG_CONTEXT_THRESHOLD);\n switch (tier) {\n case 'workbench':\n return {\n primary: { provider: 'deepseek', model: MODELS.deepseek.workbench },\n fallback: { provider: 'groq', model: MODELS.groq.verifier },\n };\n case 'verifier':\n return { primary: { provider: 'groq', model: MODELS.groq.verifier } };\n case 'smart':\n return longContext\n ? {\n primary: { provider: 'gemini', model: MODELS.gemini.smart },\n fallback: { provider: 'anthropic', model: MODELS.anthropic.smart },\n }\n : {\n primary: { provider: 'anthropic', model: MODELS.anthropic.smart },\n fallback: { provider: 'gemini', model: MODELS.gemini.smart },\n };\n case 'fast':\n return {\n primary: { provider: 'grok', model: MODELS.grok.fast },\n fallback: { provider: 'anthropic', model: MODELS.anthropic.fast },\n };\n case 'balanced':\n default:\n return longContext\n ? {\n primary: { provider: 'gemini', model: MODELS.gemini.smart },\n fallback: { provider: 'anthropic', model: MODELS.anthropic.balanced },\n }\n : {\n primary: { provider: 'anthropic', model: MODELS.anthropic.balanced },\n fallback: { provider: 'gemini', model: MODELS.gemini.smart },\n };\n }\n}\n\n/**\n * Build the `cf-aig-metadata` header value for the Cloudflare AI Gateway.\n *\n * Carries caller attribution (project / workload / actor / runId) so a single\n * shared gateway can be sliced per-app and per-feature in the AI Gateway\n * dashboard and logs. This replaces the per-app-gateway convention: rather than\n * one gateway per app (which has to be provisioned and silently 401s when it\n * isn't), one gateway tags every request with who made it.\n *\n * Returns `undefined` when no attribution fields are set (header omitted).\n * The CF AI Gateway accepts a JSON object of string/number/boolean values.\n */\nfunction buildAigMetadata(opts: LLMOptions): string | undefined {\n const meta: Record<string, string> = {};\n if (opts.project) meta.project = opts.project;\n if (opts.workload) meta.workload = opts.workload;\n if (opts.actor) meta.actor = opts.actor;\n if (opts.runId) meta.runId = opts.runId;\n return Object.keys(meta).length > 0 ? JSON.stringify(meta) : undefined;\n}\n\nasync function callOne(\n leg: { provider: LLMProvider; model: string },\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n fetchImpl: typeof fetch,\n logger: Logger | undefined,\n nowFn?: () => number,\n): Promise<{ parsed: { content: string; input: number; output: number; cacheRead?: number; cacheWrite?: number; model?: string }; gatewayRequestId?: string; attempts: number }> {\n let req: { url: string; headers: Record<string, string>; body: string };\n switch (leg.provider) {\n case 'anthropic':\n req = buildAnthropicRequest(leg.model, messages, opts, env);\n break;\n case 'gemini':\n req = buildGeminiRequest(leg.model, messages, opts, env);\n break;\n case 'groq':\n req = buildGroqRequest(leg.model, messages, opts, env);\n break;\n case 'grok':\n req = buildGrokRequest(leg.model, messages, opts, env);\n break;\n case 'deepseek':\n req = buildDeepSeekRequest(leg.model, messages, opts, env);\n break;\n }\n // Attribution for the shared AI Gateway — one gateway, sliced per-app/feature.\n const aigMetadata = buildAigMetadata(opts);\n if (aigMetadata) req.headers['cf-aig-metadata'] = aigMetadata;\n const { json, gatewayRequestId, attempts } = await callWithBackoff(\n leg.provider,\n req,\n fetchImpl,\n opts.signal,\n logger,\n nowFn,\n );\n switch (leg.provider) {\n case 'anthropic':\n return { parsed: parseAnthropic(json), gatewayRequestId, attempts };\n case 'gemini':\n return { parsed: parseGemini(json), gatewayRequestId, attempts };\n case 'groq':\n return { parsed: parseGroq(json), gatewayRequestId, attempts };\n case 'grok':\n return { parsed: parseGroq(json), gatewayRequestId, attempts };\n case 'deepseek':\n return { parsed: parseGroq(json), gatewayRequestId, attempts };\n }\n}\n\n/**\n * Run a completion through the routing plan for the requested tier.\n *\n * Routing summary (0.3.0):\n * - `fast` → Grok 4.3; Anthropic Haiku fallback when Grok is unavailable\n * - `balanced` → Anthropic Sonnet; Gemini 2.5 Pro if `longContextThreshold` exceeded\n * - `smart` → Anthropic Opus; Gemini 2.5 Pro if long-context\n * - `verifier` → Groq Llama 3.3 70B (no fallback — verifier is inherently cheap/best-effort)\n * - `workbench` → DeepSeek Chat; Groq fallback for boring/reviewable internal batch jobs\n *\n * All provider traffic flows through Cloudflare AI Gateway at `AI_GATEWAY_BASE_URL`.\n *\n * Per-provider reliability guarantees (0.4.0):\n * - Exponential backoff with jitter on 429 / 5xx (base 500ms, cap 8s, up to 2 retries).\n * - Provider cooldown: after exhausting retries the provider is marked cooling down\n * for 30 seconds; subsequent calls skip it and go straight to the fallback leg.\n *\n * @param messages - Ordered chat history.\n * @param env - API key + gateway bindings.\n * @param opts - Optional tier/model/parameters override.\n * @param deps - Optional fetch/logger/clock injection (for testing).\n * @returns A {@link FactoryResponse} carrying either an {@link LLMResult} or\n * an error (`LLM_ALL_PROVIDERS_FAILED`, `LLM_RATE_LIMITED`, or `INTERNAL_ERROR`).\n */\nexport async function complete(\n messages: LLMMessage[],\n env: LLMEnv,\n opts: LLMOptions = {},\n deps: LLMDeps = {},\n): Promise<FactoryResponse<LLMResult>> {\n if (messages.length === 0) {\n throw new ValidationError('messages must not be empty');\n }\n if (!env.AI_GATEWAY_BASE_URL) {\n throw new ValidationError('AI_GATEWAY_BASE_URL is required in 0.3.0');\n }\n const fetchImpl = deps.fetch ?? fetch;\n const now = deps.now ?? (() => Date.now());\n const logger = deps.logger;\n const startedAt = now();\n\n const tier: LLMTier = opts.tier ?? 'balanced';\n const system = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const tokenEstimate = estimateTokens(messages, system);\n const route = plan(tier, opts, tokenEstimate);\n\n // ── Org-level daily / monthly cap pre-check ──────────────────────────────\n const kv = env.LLM_COST_KV;\n const todayKey = `llm:daily-cost:${isoDate(now())}`;\n const monthKey = `llm:monthly-cost:${isoMonth(now())}`;\n if (kv) {\n if (opts.dailyCapUsd !== undefined) {\n const raw = await kv.get(todayKey).catch(() => null);\n const spent = parseFloat(raw ?? '0');\n if (spent >= opts.dailyCapUsd) {\n return toErrorResponse(\n new RateLimitError('LLM_DAILY_CAP_EXCEEDED', {\n spentUsd: spent,\n dailyCapUsd: opts.dailyCapUsd,\n }),\n );\n }\n }\n if (opts.monthlyCapUsd !== undefined) {\n const raw = await kv.get(monthKey).catch(() => null);\n const spent = parseFloat(raw ?? '0');\n if (spent >= opts.monthlyCapUsd) {\n return toErrorResponse(\n new RateLimitError('LLM_MONTHLY_CAP_EXCEEDED', {\n spentUsd: spent,\n monthlyCapUsd: opts.monthlyCapUsd,\n }),\n );\n }\n }\n }\n\n const attemptLog: Array<{ provider: LLMProvider; status?: number; message: string }> = [];\n\n const routeLegs = [route.primary, route.fallback].filter(Boolean) as Array<{ provider: LLMProvider; model: string }>;\n for (const [legIndex, leg] of routeLegs.entries()) {\n // Skip providers that are currently in their cooldown window.\n if (isProviderCoolingDown(leg.provider, now)) {\n logger?.warn?.('llm.provider.coolingDown', { provider: leg.provider });\n attemptLog.push({ provider: leg.provider, message: 'skipped: cooling down' });\n continue;\n }\n if (opts.signal?.aborted) {\n return toErrorResponse(\n new InternalError('llm call aborted', { provider: leg.provider, model: leg.model }),\n );\n }\n try {\n const result = await callOne(leg, messages, opts, env, fetchImpl, logger, now);\n if (!result.parsed.content) {\n throw { provider: leg.provider, status: 200, retryable: false, message: 'empty content' } satisfies ProviderError;\n }\n logger?.info?.('llm.complete', {\n provider: leg.provider,\n model: leg.model,\n tier,\n tokenEstimate,\n attempts: result.attempts,\n runId: opts.runId,\n project: opts.project,\n actor: opts.actor,\n workload: opts.workload,\n });\n const llmResult: LLMResult = {\n content: result.parsed.content,\n provider: leg.provider,\n model: result.parsed.model ?? leg.model,\n tier,\n tokens: {\n input: result.parsed.input,\n output: result.parsed.output,\n cacheRead: result.parsed.cacheRead,\n cacheWrite: result.parsed.cacheWrite,\n },\n latency: now() - startedAt,\n attempts: result.attempts,\n gatewayRequestId: result.gatewayRequestId,\n };\n const costUsd = estimateCostUsd(llmResult.tokens, llmResult.model);\n if (opts.maxCostUsd !== undefined && costUsd > opts.maxCostUsd) {\n if (kv && (opts.dailyCapUsd !== undefined || opts.monthlyCapUsd !== undefined)) {\n await recordOrgCostUsage(kv, todayKey, monthKey, costUsd, opts);\n }\n return toErrorResponse(\n new RateLimitError('LLM_COST_CAP_EXCEEDED', {\n costUsd,\n maxCostUsd: opts.maxCostUsd,\n model: llmResult.model,\n tokens: llmResult.tokens,\n }),\n );\n }\n // ── Update org-level cost accumulators in KV ─────────────────────────\n // The KV writes are best-effort. Cloudflare KV does not support atomic\n // compare-and-swap, so concurrent increments may undercount spend.\n if (kv && (opts.dailyCapUsd !== undefined || opts.monthlyCapUsd !== undefined)) {\n await recordOrgCostUsage(kv, todayKey, monthKey, costUsd, opts);\n }\n // ── Metering callback ────────────────────────────────────────────────\n if (deps.onRecord && opts.ledger) {\n const row: LLMRecordRow = {\n ...opts.ledger,\n model: llmResult.model,\n provider: llmResult.provider,\n tier: llmResult.tier,\n inputTokens: llmResult.tokens.input,\n outputTokens: llmResult.tokens.output,\n cacheReadTokens: llmResult.tokens.cacheRead ?? 0,\n cacheWriteTokens: llmResult.tokens.cacheWrite ?? 0,\n latencyMs: llmResult.latency,\n costUsd,\n yyyyMm: isoMonth(now()),\n };\n deps.onRecord(row).catch((e: unknown) => {\n logger?.warn?.('llm.onRecord.error', { message: e instanceof Error ? e.message : String(e) });\n });\n }\n return { data: llmResult, error: null };\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') {\n return toErrorResponse(\n new InternalError('llm call aborted', { provider: leg.provider, model: leg.model }),\n );\n }\n if (isProviderError(e)) {\n attemptLog.push({ provider: e.provider, status: e.status, message: e.message });\n if (e.status === 429 && legIndex === routeLegs.length - 1) {\n return toErrorResponse(\n new RateLimitError(`llm rate limited on ${e.provider}`, { attempts: attemptLog }),\n );\n }\n logger?.warn?.('llm.leg.failed', { provider: leg.provider, status: e.status });\n continue;\n }\n attemptLog.push({ provider: leg.provider, message: e instanceof Error ? e.message : String(e) });\n }\n }\n\n return toErrorResponse(\n new InternalError('LLM_ALL_PROVIDERS_FAILED', { attempts: attemptLog, tier, tokenEstimate }),\n );\n}\n\n// ─── Streaming ────────────────────────────────────────────────────────────\n\n/**\n * Anthropic server-sent event shapes used by the streaming parser.\n * Only the fields we consume are typed; the rest are ignored.\n */\ninterface AnthropicStreamEvent {\n type: string;\n index?: number;\n delta?: { type?: string; text?: string };\n message?: {\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n model?: string;\n };\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n };\n}\n\n/**\n * Streams a completion from the primary Anthropic provider, yielding text chunks\n * as they arrive. Falls back to the non-streaming {@link complete} function when\n * the provider does not support streaming (i.e. a non-Anthropic primary is selected).\n *\n * The generator's **return value** (accessible via `gen.return()` or by consuming\n * the full iteration) is an {@link LLMResult} with the same shape as {@link complete}.\n *\n * Usage pattern:\n * ```ts\n * const gen = completionStream(messages, env, opts);\n * for await (const chunk of gen) {\n * // stream chunk to client\n * }\n * const result = (await gen.return(undefined)).value; // LLMResult\n * ```\n *\n * @param messages - Ordered chat history.\n * @param env - API key + gateway bindings.\n * @param opts - Optional tier/model/parameters override. Accepts `deps` as nested field.\n * @returns An async generator that yields `string` chunks and returns an {@link LLMResult}.\n */\nexport async function* completionStream(\n messages: LLMMessage[],\n env: LLMEnv,\n opts: LLMOptions & { deps?: LLMDeps } = {},\n): AsyncGenerator<string, LLMResult, unknown> {\n if (messages.length === 0) {\n throw new ValidationError('messages must not be empty');\n }\n if (!env.AI_GATEWAY_BASE_URL) {\n throw new ValidationError('AI_GATEWAY_BASE_URL is required in 0.3.0');\n }\n\n const deps: LLMDeps = opts.deps ?? {};\n const fetchImpl = deps.fetch ?? fetch;\n const now = deps.now ?? (() => Date.now());\n const logger = deps.logger;\n const startedAt = now();\n\n const tier: LLMTier = opts.tier ?? 'balanced';\n const system = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const tokenEstimate = estimateTokens(messages, system);\n const route = plan(tier, opts, tokenEstimate);\n const streamLeg =\n route.primary.provider === 'grok' && !env.GROK_API_KEY && route.fallback?.provider === 'anthropic'\n ? route.fallback\n : route.primary;\n\n // Only Anthropic supports streaming in the current implementation.\n // For all other primaries, fall back to non-streaming complete().\n if (streamLeg.provider !== 'anthropic') {\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', { error: result.error });\n }\n yield result.data.content;\n return result.data;\n }\n\n // Check cooldown before attempting the streaming call.\n if (isProviderCoolingDown(streamLeg.provider, now)) {\n logger?.warn?.('llm.provider.coolingDown', { provider: streamLeg.provider });\n // Fall back to non-streaming complete() which will handle the fallback leg.\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', { error: result.error });\n }\n yield result.data.content;\n return result.data;\n }\n\n const req = buildAnthropicRequest(streamLeg.model, messages, opts, env, true);\n // Attribution for the shared AI Gateway (matches the non-streaming path).\n const streamAigMetadata = buildAigMetadata(opts);\n if (streamAigMetadata) req.headers['cf-aig-metadata'] = streamAigMetadata;\n\n let response: Response;\n try {\n response = await fetchImpl(req.url, {\n method: 'POST',\n headers: req.headers,\n body: req.body,\n // Fall back to a 60 s default when the caller provides no signal — prevents\n // a hung provider connection from consuming the Worker's wall-clock budget.\n signal: opts.signal ?? AbortSignal.timeout(60_000),\n });\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') {\n throw new InternalError('llm call aborted', {\n provider: streamLeg.provider,\n model: streamLeg.model,\n });\n }\n throw new InternalError('llm stream fetch failed', {\n message: e instanceof Error ? e.message : String(e),\n });\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n const retryable = isRetryableForBackoff(response.status);\n if (retryable && response.status === 429) {\n markProviderCoolingDown(streamLeg.provider, now);\n }\n // Fall back to non-streaming complete() which will try the fallback leg.\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', {\n streamError: `${streamLeg.provider} ${String(response.status)}: ${text.slice(0, 300)}`,\n error: result.error,\n });\n }\n yield result.data.content;\n return result.data;\n }\n\n if (!response.body) {\n throw new InternalError('llm stream response body is null', {\n provider: streamLeg.provider,\n });\n }\n\n // Stream SSE events from Anthropic.\n const decoder = new TextDecoder();\n let accumulatedText = '';\n let inputTokens = 0;\n let outputTokens = 0;\n let cacheRead = 0;\n let cacheWrite = 0;\n let modelName: string | undefined;\n const gatewayRequestId: string | undefined = response.headers.get('cf-aig-request-id') ?? undefined;\n\n const reader = response.body.getReader();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // SSE lines are delimited by '\\n'. Events are separated by '\\n\\n'.\n const lines = buffer.split('\\n');\n // Keep the last (potentially incomplete) line in the buffer.\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const data = line.slice(6).trim();\n if (data === '[DONE]') break;\n let event: AnthropicStreamEvent;\n try {\n event = JSON.parse(data) as AnthropicStreamEvent;\n } catch {\n continue; // Skip malformed SSE lines.\n }\n\n switch (event.type) {\n case 'message_start':\n inputTokens = event.message?.usage?.input_tokens ?? 0;\n cacheRead = event.message?.usage?.cache_read_input_tokens ?? 0;\n cacheWrite = event.message?.usage?.cache_creation_input_tokens ?? 0;\n modelName = event.message?.model;\n break;\n case 'content_block_delta':\n if (event.delta?.type === 'text_delta' && typeof event.delta.text === 'string') {\n accumulatedText += event.delta.text;\n yield event.delta.text;\n }\n break;\n case 'message_delta':\n outputTokens = event.usage?.output_tokens ?? outputTokens;\n break;\n default:\n break;\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n clearProviderCooldown(streamLeg.provider);\n logger?.info?.('llm.completionStream', {\n provider: streamLeg.provider,\n model: streamLeg.model,\n tier,\n tokenEstimate,\n runId: opts.runId,\n project: opts.project,\n actor: opts.actor,\n workload: opts.workload,\n });\n\n return {\n content: accumulatedText,\n provider: streamLeg.provider,\n model: modelName ?? streamLeg.model,\n tier,\n tokens: { input: inputTokens, output: outputTokens, cacheRead, cacheWrite },\n latency: now() - startedAt,\n attempts: 1,\n gatewayRequestId,\n };\n}\n\n// ─── Grounding assertion ───────────────────────────────────────────────────\n\n/**\n * Returns `true` if `response` contains at least one verbatim phrase of at\n * least 5 consecutive whitespace-delimited tokens that also appears in one of\n * the `sources` strings.\n *\n * Returns `true` unconditionally when `sources` is empty (no grounding\n * documents means grounding cannot be violated).\n *\n * This is a lightweight guard for RAG pipelines — it detects obvious\n * hallucinations where the model generates content not present in any\n * retrieved source. It is NOT a semantic similarity check.\n *\n * @param response - The LLM-generated text to inspect.\n * @param sources - Retrieved source documents to check against.\n * @returns `true` if the response is grounded, `false` if hallucination detected.\n *\n * @example\n * ```ts\n * const grounded = assertGrounding(llmAnswer, retrievedDocs);\n * if (!grounded) {\n * // flag or re-rank the response\n * }\n * ```\n */\nexport function assertGrounding(response: string, sources: string[]): boolean {\n if (sources.length === 0) return true;\n\n const WINDOW = 5;\n const responseTokens = response.split(/\\s+/).filter((t) => t.length > 0);\n\n if (responseTokens.length < WINDOW) return false;\n\n // Build a set of all 5-token ngrams from each source for O(n) lookup.\n const sourceNgrams = new Set<string>();\n for (const source of sources) {\n const tokens = source.split(/\\s+/).filter((t) => t.length > 0);\n for (let i = 0; i <= tokens.length - WINDOW; i++) {\n const ngram = tokens.slice(i, i + WINDOW).join(' ');\n sourceNgrams.add(ngram);\n }\n }\n\n if (sourceNgrams.size === 0) return false;\n\n // Slide a window of WINDOW tokens over the response and check for a match.\n for (let i = 0; i <= responseTokens.length - WINDOW; i++) {\n const ngram = responseTokens.slice(i, i + WINDOW).join(' ');\n if (sourceNgrams.has(ngram)) return true;\n }\n\n return false;\n}\n\n// ─── Exported helpers (kept for existing consumers) ───────────────────────\n\nexport { MODELS, isProviderCoolingDown, markProviderCoolingDown, clearProviderCooldown, PROVIDER_COOLDOWN_MS };\nexport { BASE_BACKOFF_MS };\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@latimer-woods-tech/llm",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Latimer-Woods-Tech/Factory.git",
8
8
  "directory": "packages/llm"
9
9
  },
10
- "publishConfig": {},
10
+ "publishConfig": {
11
+ "registry": "https://npm.pkg.github.com"
12
+ },
11
13
  "main": "./dist/index.mjs",
12
14
  "module": "./dist/index.mjs",
13
15
  "types": "./dist/index.d.mts",
@@ -26,7 +28,8 @@
26
28
  "build": "tsup src/index.ts --format esm --dts",
27
29
  "test": "vitest run --coverage",
28
30
  "lint": "eslint src --max-warnings 0",
29
- "typecheck": "tsc --noEmit"
31
+ "typecheck": "tsc --noEmit",
32
+ "prepublish": "npm run lint && npm run typecheck && npm run test && npm run build"
30
33
  },
31
34
  "dependencies": {
32
35
  "@latimer-woods-tech/errors": "^0.2.0",