@latimer-woods-tech/llm 0.4.1 → 0.4.3
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 +49 -0
- package/dist/index.d.mts +69 -2
- package/dist/index.mjs +161 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.3 — 2026-06-03
|
|
4
|
+
|
|
5
|
+
### Added — tool-calling for OpenAI-style providers (Phase 1b: Grok, DeepSeek)
|
|
6
|
+
|
|
7
|
+
- **Grok and DeepSeek now support tool-calling.** Requests carry OpenAI-format
|
|
8
|
+
`tools` + `tool_choice`; responses parse `tool_calls` + `finish_reason` into
|
|
9
|
+
the same normalized `LLMResult.toolCalls` / `stopReason` as Anthropic.
|
|
10
|
+
- Anthropic-shaped tool blocks are converted to OpenAI wire format:
|
|
11
|
+
`tool_use` → an assistant message with `tool_calls`; `tool_result` → a
|
|
12
|
+
standalone `tool` message keyed by `tool_call_id`.
|
|
13
|
+
- **`TOOL_CAPABLE_PROVIDERS`** widened to `anthropic, grok, deepseek`, so the
|
|
14
|
+
`fast` (Grok→Haiku) and `workbench` (DeepSeek) tiers support tool loops while
|
|
15
|
+
still failing closed for `verifier` (Groq Llama).
|
|
16
|
+
- Malformed tool-call argument JSON is tolerated (billed as `{}`, never throws).
|
|
17
|
+
|
|
18
|
+
### Not yet
|
|
19
|
+
|
|
20
|
+
- **Gemini** tool-calling is deferred to a focused follow-up — its
|
|
21
|
+
`tool_use_id`↔function-name correlation and schema constraints need dedicated
|
|
22
|
+
handling. Gemini stays out of `TOOL_CAPABLE_PROVIDERS` until then.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 0.4.2 — 2026-06-03
|
|
27
|
+
|
|
28
|
+
### Added — tool-calling (Agent Runtime Phase 1a; Anthropic)
|
|
29
|
+
|
|
30
|
+
- **`LLMOptions.tools`** (`LLMTool[]`) and **`LLMOptions.toolChoice`**
|
|
31
|
+
(`'auto' | 'none' | { name }`). When `tools` is set, routing **fails closed**
|
|
32
|
+
to tool-capable providers — failover never silently falls back to one that
|
|
33
|
+
can't honour the tool schema (1a: Anthropic only; others land in 1b).
|
|
34
|
+
- **`LLMResult.toolCalls`** (`LLMToolCall[]`) and **`LLMResult.stopReason`**
|
|
35
|
+
(`'end' | 'tool_use' | 'max_tokens' | 'other'`), normalized across providers.
|
|
36
|
+
- **`LLMMessage.content`** now accepts `string | LLMContentBlock[]` — structured
|
|
37
|
+
`text` / `tool_use` / `tool_result` blocks for multi-turn tool conversations.
|
|
38
|
+
Backwards-compatible: existing `string` content is unchanged; providers without
|
|
39
|
+
tool support receive the text projection.
|
|
40
|
+
- New exported types: `LLMTool`, `LLMToolCall`, `LLMContentBlock`.
|
|
41
|
+
|
|
42
|
+
### Notes
|
|
43
|
+
|
|
44
|
+
- A `tool_use` response with no text content is no longer treated as an empty
|
|
45
|
+
(failed) completion.
|
|
46
|
+
- Anthropic `tool_use` blocks pass through unchanged (the block shapes mirror the
|
|
47
|
+
Messages wire format). Other providers' tool formats are normalized in 1a's
|
|
48
|
+
follow-ups (1b: Grok/DeepSeek/Gemini; 1c: streaming tool-call accumulation).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
3
52
|
## 0.4.1 — 2026-06-03
|
|
4
53
|
|
|
5
54
|
### Added (no breaking changes)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,56 @@
|
|
|
1
1
|
import { FactoryResponse } from '@latimer-woods-tech/errors';
|
|
2
2
|
import { Logger } from '@latimer-woods-tech/logger';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* A tool the model may call. `parameters` is a JSON Schema object describing
|
|
6
|
+
* the tool's input. Provider-agnostic; normalized per provider at request time.
|
|
7
|
+
*/
|
|
8
|
+
interface LLMTool {
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
/** JSON Schema for the tool's input arguments. */
|
|
12
|
+
parameters: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A tool invocation requested by the model, normalized across providers.
|
|
16
|
+
*/
|
|
17
|
+
interface LLMToolCall {
|
|
18
|
+
/** Provider-assigned call id; echo it back in the matching tool_result. */
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
/** Parsed argument object the model passed to the tool. */
|
|
22
|
+
arguments: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Structured content block for tool-calling conversations. The field shapes
|
|
26
|
+
* mirror the Anthropic Messages wire format so they pass through unchanged.
|
|
27
|
+
*/
|
|
28
|
+
type LLMContentBlock = {
|
|
29
|
+
type: 'text';
|
|
30
|
+
text: string;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'tool_use';
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
input: Record<string, unknown>;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'tool_result';
|
|
38
|
+
tool_use_id: string;
|
|
39
|
+
content: string;
|
|
40
|
+
is_error?: boolean;
|
|
41
|
+
};
|
|
4
42
|
/**
|
|
5
43
|
* Single chat message exchanged with an LLM provider.
|
|
44
|
+
*
|
|
45
|
+
* `content` is a plain string in the common case. For tool-calling
|
|
46
|
+
* conversations it may be an array of {@link LLMContentBlock}s (e.g. an
|
|
47
|
+
* assistant turn carrying `tool_use` blocks, or a user turn carrying
|
|
48
|
+
* `tool_result` blocks). Providers that don't support tool-calling receive
|
|
49
|
+
* the text projection of the content (see `contentToText`).
|
|
6
50
|
*/
|
|
7
51
|
interface LLMMessage {
|
|
8
52
|
role: 'user' | 'assistant' | 'system';
|
|
9
|
-
content: string;
|
|
53
|
+
content: string | LLMContentBlock[];
|
|
10
54
|
}
|
|
11
55
|
/**
|
|
12
56
|
* Quality tier selected by the caller. Routing is workload-split:
|
|
@@ -74,6 +118,19 @@ interface LLMOptions {
|
|
|
74
118
|
* is emitted after every successful completion. Errors are swallowed.
|
|
75
119
|
*/
|
|
76
120
|
ledger?: LLMRecordContext;
|
|
121
|
+
/**
|
|
122
|
+
* Tools the model may call. When present, routing **fails closed** to
|
|
123
|
+
* tool-capable providers — failover never falls back to a provider that
|
|
124
|
+
* can't honour the tool schema. See {@link LLMResult.toolCalls}.
|
|
125
|
+
*/
|
|
126
|
+
tools?: LLMTool[];
|
|
127
|
+
/**
|
|
128
|
+
* Tool-selection policy. `'auto'` (default when `tools` is set) lets the
|
|
129
|
+
* model decide; `'none'` forbids tool use; `{ name }` forces a specific tool.
|
|
130
|
+
*/
|
|
131
|
+
toolChoice?: 'auto' | 'none' | {
|
|
132
|
+
name: string;
|
|
133
|
+
};
|
|
77
134
|
}
|
|
78
135
|
/**
|
|
79
136
|
* Provider that produced an LLM response.
|
|
@@ -98,6 +155,16 @@ interface LLMResult {
|
|
|
98
155
|
attempts: number;
|
|
99
156
|
/** Monotonic request id from AI Gateway, if present in headers. */
|
|
100
157
|
gatewayRequestId?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Why generation stopped, normalized across providers. `'tool_use'` means
|
|
160
|
+
* the model is requesting one or more tool calls (see {@link toolCalls}).
|
|
161
|
+
*/
|
|
162
|
+
stopReason?: 'end' | 'tool_use' | 'max_tokens' | 'other';
|
|
163
|
+
/**
|
|
164
|
+
* Tool calls the model requested, normalized across providers. Present
|
|
165
|
+
* (non-empty) when `stopReason === 'tool_use'`.
|
|
166
|
+
*/
|
|
167
|
+
toolCalls?: LLMToolCall[];
|
|
101
168
|
}
|
|
102
169
|
/**
|
|
103
170
|
* Environment bindings required by {@link complete}.
|
|
@@ -309,4 +376,4 @@ declare function completionStream(messages: LLMMessage[], env: LLMEnv, opts?: LL
|
|
|
309
376
|
*/
|
|
310
377
|
declare function assertGrounding(response: string, sources: string[]): boolean;
|
|
311
378
|
|
|
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 };
|
|
379
|
+
export { BASE_BACKOFF_MS, type CostKvStore, type LLMContentBlock, type LLMDeps, type LLMEnv, type LLMMessage, type LLMOptions, type LLMProvider, type LLMRecordContext, type LLMRecordRow, type LLMResult, type LLMTier, type LLMTool, type LLMToolCall, MODELS, MODEL_PRICE_PER_1M, PROVIDER_COOLDOWN_MS, assertGrounding, clearProviderCooldown, complete, completionStream, isProviderCoolingDown, markProviderCoolingDown };
|
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,15 @@ import {
|
|
|
5
5
|
ValidationError,
|
|
6
6
|
toErrorResponse
|
|
7
7
|
} from "@latimer-woods-tech/errors";
|
|
8
|
+
function contentToText(content) {
|
|
9
|
+
if (typeof content === "string") return content;
|
|
10
|
+
return content.map((b) => b.type === "text" ? b.text : b.type === "tool_result" ? b.content : "").join("");
|
|
11
|
+
}
|
|
12
|
+
function systemText(opts, messages) {
|
|
13
|
+
if (opts.system !== void 0) return opts.system;
|
|
14
|
+
const c = messages.find((m) => m.role === "system")?.content;
|
|
15
|
+
return c === void 0 ? void 0 : contentToText(c);
|
|
16
|
+
}
|
|
8
17
|
var MODELS = {
|
|
9
18
|
anthropic: {
|
|
10
19
|
fast: "claude-haiku-4-20250514",
|
|
@@ -102,7 +111,7 @@ function isRetryableForBackoff(status) {
|
|
|
102
111
|
}
|
|
103
112
|
function estimateTokens(messages, system) {
|
|
104
113
|
let chars = system?.length ?? 0;
|
|
105
|
-
for (const m of messages) chars += m.content.length;
|
|
114
|
+
for (const m of messages) chars += contentToText(m.content).length;
|
|
106
115
|
return Math.ceil(chars / 4);
|
|
107
116
|
}
|
|
108
117
|
function sleep(ms, signal) {
|
|
@@ -123,7 +132,7 @@ function computeBackoffMs(attempt) {
|
|
|
123
132
|
return Math.min(BACKOFF_BASE_MS * Math.pow(2, attempt) + jitter, BACKOFF_CAP_MS);
|
|
124
133
|
}
|
|
125
134
|
function buildAnthropicRequest(model, messages, opts, env, streaming = false) {
|
|
126
|
-
const sys = opts
|
|
135
|
+
const sys = systemText(opts, messages);
|
|
127
136
|
const filtered = messages.filter((m) => m.role !== "system");
|
|
128
137
|
const body = {
|
|
129
138
|
model,
|
|
@@ -138,6 +147,15 @@ function buildAnthropicRequest(model, messages, opts, env, streaming = false) {
|
|
|
138
147
|
const cache = opts.promptCache ?? sys.length >= 4096;
|
|
139
148
|
body.system = cache ? [{ type: "text", text: sys, cache_control: { type: "ephemeral" } }] : sys;
|
|
140
149
|
}
|
|
150
|
+
if (opts.tools && opts.tools.length > 0) {
|
|
151
|
+
body.tools = opts.tools.map((t) => ({
|
|
152
|
+
name: t.name,
|
|
153
|
+
description: t.description ?? "",
|
|
154
|
+
input_schema: t.parameters
|
|
155
|
+
}));
|
|
156
|
+
const tc = opts.toolChoice ?? "auto";
|
|
157
|
+
body.tool_choice = tc === "auto" ? { type: "auto" } : tc === "none" ? { type: "none" } : { type: "tool", name: tc.name };
|
|
158
|
+
}
|
|
141
159
|
return {
|
|
142
160
|
url: `${env.AI_GATEWAY_BASE_URL}/anthropic/v1/messages`,
|
|
143
161
|
headers: {
|
|
@@ -150,10 +168,10 @@ function buildAnthropicRequest(model, messages, opts, env, streaming = false) {
|
|
|
150
168
|
};
|
|
151
169
|
}
|
|
152
170
|
function buildGeminiRequest(model, messages, opts, env) {
|
|
153
|
-
const sys = opts
|
|
171
|
+
const sys = systemText(opts, messages);
|
|
154
172
|
const contents = messages.filter((m) => m.role !== "system").map((m) => ({
|
|
155
173
|
role: m.role === "assistant" ? "model" : "user",
|
|
156
|
-
parts: [{ text: m.content }]
|
|
174
|
+
parts: [{ text: contentToText(m.content) }]
|
|
157
175
|
}));
|
|
158
176
|
const body = {
|
|
159
177
|
contents,
|
|
@@ -175,11 +193,52 @@ function buildGeminiRequest(model, messages, opts, env) {
|
|
|
175
193
|
body: JSON.stringify(body)
|
|
176
194
|
};
|
|
177
195
|
}
|
|
196
|
+
function toOpenAiMessages(messages, sys) {
|
|
197
|
+
const out = [];
|
|
198
|
+
if (sys) out.push({ role: "system", content: sys });
|
|
199
|
+
for (const m of messages) {
|
|
200
|
+
if (m.role === "system") continue;
|
|
201
|
+
if (typeof m.content === "string") {
|
|
202
|
+
out.push({ role: m.role, content: m.content });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let text = "";
|
|
206
|
+
const toolCalls = [];
|
|
207
|
+
const results = [];
|
|
208
|
+
for (const b of m.content) {
|
|
209
|
+
if (b.type === "text") text += b.text;
|
|
210
|
+
else if (b.type === "tool_use")
|
|
211
|
+
toolCalls.push({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input) } });
|
|
212
|
+
else if (b.type === "tool_result") results.push({ tool_use_id: b.tool_use_id, content: b.content });
|
|
213
|
+
}
|
|
214
|
+
if (results.length > 0) {
|
|
215
|
+
for (const r of results) out.push({ role: "tool", tool_call_id: r.tool_use_id, content: r.content });
|
|
216
|
+
if (text) out.push({ role: "user", content: text });
|
|
217
|
+
} else if (toolCalls.length > 0) {
|
|
218
|
+
out.push({ role: "assistant", content: text || null, tool_calls: toolCalls });
|
|
219
|
+
} else {
|
|
220
|
+
out.push({ role: m.role, content: text });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
function openAiTools(opts) {
|
|
226
|
+
if (!opts.tools || opts.tools.length === 0) return void 0;
|
|
227
|
+
return opts.tools.map((t) => ({
|
|
228
|
+
type: "function",
|
|
229
|
+
function: { name: t.name, description: t.description ?? "", parameters: t.parameters }
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
function openAiToolChoice(tc) {
|
|
233
|
+
if (tc === void 0) return void 0;
|
|
234
|
+
if (tc === "auto" || tc === "none") return tc;
|
|
235
|
+
return { type: "function", function: { name: tc.name } };
|
|
236
|
+
}
|
|
178
237
|
function buildGroqRequest(model, messages, opts, env) {
|
|
179
|
-
const sys = opts
|
|
238
|
+
const sys = systemText(opts, messages);
|
|
180
239
|
const merged = [];
|
|
181
240
|
if (sys) merged.push({ role: "system", content: sys });
|
|
182
|
-
for (const m of messages) if (m.role !== "system") merged.push(m);
|
|
241
|
+
for (const m of messages) if (m.role !== "system") merged.push({ role: m.role, content: contentToText(m.content) });
|
|
183
242
|
return {
|
|
184
243
|
url: `${env.AI_GATEWAY_BASE_URL}/groq/openai/v1/chat/completions`,
|
|
185
244
|
headers: {
|
|
@@ -198,16 +257,19 @@ function buildGrokRequest(model, messages, opts, env) {
|
|
|
198
257
|
if (!env.GROK_API_KEY) {
|
|
199
258
|
throw new ValidationError("GROK_API_KEY required for grok-* model override");
|
|
200
259
|
}
|
|
201
|
-
const sys = opts
|
|
202
|
-
const merged = [];
|
|
203
|
-
if (sys) merged.push({ role: "system", content: sys });
|
|
204
|
-
for (const m of messages) if (m.role !== "system") merged.push(m);
|
|
260
|
+
const sys = systemText(opts, messages);
|
|
205
261
|
const body = {
|
|
206
262
|
model,
|
|
207
263
|
max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
208
264
|
temperature: opts.temperature ?? DEFAULT_TEMPERATURE,
|
|
209
|
-
messages:
|
|
265
|
+
messages: toOpenAiMessages(messages, sys)
|
|
210
266
|
};
|
|
267
|
+
const tools = openAiTools(opts);
|
|
268
|
+
if (tools) {
|
|
269
|
+
body.tools = tools;
|
|
270
|
+
const tc = openAiToolChoice(opts.toolChoice ?? "auto");
|
|
271
|
+
if (tc !== void 0) body.tool_choice = tc;
|
|
272
|
+
}
|
|
211
273
|
if (model === MODELS.grok.fast) {
|
|
212
274
|
body.reasoning_effort = opts.reasoningEffort ?? "none";
|
|
213
275
|
}
|
|
@@ -224,33 +286,53 @@ function buildDeepSeekRequest(model, messages, opts, env) {
|
|
|
224
286
|
if (!env.DEEPSEEK_API_KEY) {
|
|
225
287
|
throw new ValidationError("DEEPSEEK_API_KEY required for workbench tier or deepseek-* model override");
|
|
226
288
|
}
|
|
227
|
-
const sys = opts
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
289
|
+
const sys = systemText(opts, messages);
|
|
290
|
+
const body = {
|
|
291
|
+
model,
|
|
292
|
+
max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
293
|
+
temperature: opts.temperature ?? DEFAULT_TEMPERATURE,
|
|
294
|
+
messages: toOpenAiMessages(messages, sys)
|
|
295
|
+
};
|
|
296
|
+
const tools = openAiTools(opts);
|
|
297
|
+
if (tools) {
|
|
298
|
+
body.tools = tools;
|
|
299
|
+
const tc = openAiToolChoice(opts.toolChoice ?? "auto");
|
|
300
|
+
if (tc !== void 0) body.tool_choice = tc;
|
|
301
|
+
}
|
|
231
302
|
return {
|
|
232
303
|
url: `${env.AI_GATEWAY_BASE_URL}/deepseek/chat/completions`,
|
|
233
304
|
headers: {
|
|
234
305
|
"content-type": "application/json",
|
|
235
306
|
authorization: `Bearer ${env.DEEPSEEK_API_KEY}`
|
|
236
307
|
},
|
|
237
|
-
body: JSON.stringify(
|
|
238
|
-
model,
|
|
239
|
-
max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
240
|
-
temperature: opts.temperature ?? DEFAULT_TEMPERATURE,
|
|
241
|
-
messages: merged
|
|
242
|
-
})
|
|
308
|
+
body: JSON.stringify(body)
|
|
243
309
|
};
|
|
244
310
|
}
|
|
311
|
+
function normalizeAnthropicStop(reason) {
|
|
312
|
+
switch (reason) {
|
|
313
|
+
case "end_turn":
|
|
314
|
+
case "stop_sequence":
|
|
315
|
+
return "end";
|
|
316
|
+
case "tool_use":
|
|
317
|
+
return "tool_use";
|
|
318
|
+
case "max_tokens":
|
|
319
|
+
return "max_tokens";
|
|
320
|
+
default:
|
|
321
|
+
return reason ? "other" : void 0;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
245
324
|
function parseAnthropic(json) {
|
|
246
325
|
const r = json;
|
|
326
|
+
const toolCalls = (r.content ?? []).filter((c) => c.type === "tool_use" && typeof c.id === "string" && typeof c.name === "string").map((c) => ({ id: c.id, name: c.name, arguments: c.input ?? {} }));
|
|
247
327
|
return {
|
|
248
328
|
content: r.content?.find((c) => c.type === "text")?.text ?? "",
|
|
249
329
|
input: r.usage?.input_tokens ?? 0,
|
|
250
330
|
output: r.usage?.output_tokens ?? 0,
|
|
251
331
|
cacheRead: r.usage?.cache_read_input_tokens ?? 0,
|
|
252
332
|
cacheWrite: r.usage?.cache_creation_input_tokens ?? 0,
|
|
253
|
-
model: r.model
|
|
333
|
+
model: r.model,
|
|
334
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
335
|
+
stopReason: normalizeAnthropicStop(r.stop_reason)
|
|
254
336
|
};
|
|
255
337
|
}
|
|
256
338
|
function parseGemini(json) {
|
|
@@ -271,6 +353,45 @@ function parseGroq(json) {
|
|
|
271
353
|
model: r.model
|
|
272
354
|
};
|
|
273
355
|
}
|
|
356
|
+
function normalizeOpenAiStop(reason) {
|
|
357
|
+
switch (reason) {
|
|
358
|
+
case "stop":
|
|
359
|
+
return "end";
|
|
360
|
+
case "tool_calls":
|
|
361
|
+
case "function_call":
|
|
362
|
+
return "tool_use";
|
|
363
|
+
case "length":
|
|
364
|
+
return "max_tokens";
|
|
365
|
+
default:
|
|
366
|
+
return reason ? "other" : void 0;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function parseToolArgs(raw) {
|
|
370
|
+
if (!raw) return {};
|
|
371
|
+
try {
|
|
372
|
+
const v = JSON.parse(raw);
|
|
373
|
+
return typeof v === "object" && v !== null ? v : {};
|
|
374
|
+
} catch {
|
|
375
|
+
return {};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function parseOpenAi(json) {
|
|
379
|
+
const r = json;
|
|
380
|
+
const choice = r.choices?.[0];
|
|
381
|
+
const toolCalls = (choice?.message?.tool_calls ?? []).filter((c) => typeof c.function?.name === "string").map((c, i) => ({
|
|
382
|
+
id: c.id ?? `call_${i}`,
|
|
383
|
+
name: c.function.name,
|
|
384
|
+
arguments: parseToolArgs(c.function?.arguments)
|
|
385
|
+
}));
|
|
386
|
+
return {
|
|
387
|
+
content: choice?.message?.content ?? "",
|
|
388
|
+
input: r.usage?.prompt_tokens ?? 0,
|
|
389
|
+
output: r.usage?.completion_tokens ?? 0,
|
|
390
|
+
model: r.model,
|
|
391
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
392
|
+
stopReason: normalizeOpenAiStop(choice?.finish_reason)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
274
395
|
async function callWithBackoff(provider, request, fetchImpl, signal, logger, nowFn) {
|
|
275
396
|
function exhaustAndThrow(err) {
|
|
276
397
|
markProviderCoolingDown(provider, nowFn ?? Date.now);
|
|
@@ -334,6 +455,7 @@ async function callWithBackoff(provider, request, fetchImpl, signal, logger, now
|
|
|
334
455
|
function isProviderError(err) {
|
|
335
456
|
return typeof err === "object" && err !== null && typeof err.status === "number" && typeof err.message === "string" && typeof err.provider === "string";
|
|
336
457
|
}
|
|
458
|
+
var TOOL_CAPABLE_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "grok", "deepseek"]);
|
|
337
459
|
function plan(tier, opts, tokenEstimate) {
|
|
338
460
|
if (opts.model) {
|
|
339
461
|
const m = opts.model;
|
|
@@ -421,9 +543,9 @@ async function callOne(leg, messages, opts, env, fetchImpl, logger, nowFn) {
|
|
|
421
543
|
case "groq":
|
|
422
544
|
return { parsed: parseGroq(json), gatewayRequestId, attempts };
|
|
423
545
|
case "grok":
|
|
424
|
-
return { parsed:
|
|
546
|
+
return { parsed: parseOpenAi(json), gatewayRequestId, attempts };
|
|
425
547
|
case "deepseek":
|
|
426
|
-
return { parsed:
|
|
548
|
+
return { parsed: parseOpenAi(json), gatewayRequestId, attempts };
|
|
427
549
|
}
|
|
428
550
|
}
|
|
429
551
|
async function complete(messages, env, opts = {}, deps = {}) {
|
|
@@ -438,7 +560,7 @@ async function complete(messages, env, opts = {}, deps = {}) {
|
|
|
438
560
|
const logger = deps.logger;
|
|
439
561
|
const startedAt = now();
|
|
440
562
|
const tier = opts.tier ?? "balanced";
|
|
441
|
-
const system = opts
|
|
563
|
+
const system = systemText(opts, messages);
|
|
442
564
|
const tokenEstimate = estimateTokens(messages, system);
|
|
443
565
|
const route = plan(tier, opts, tokenEstimate);
|
|
444
566
|
const kv = env.LLM_COST_KV;
|
|
@@ -471,7 +593,15 @@ async function complete(messages, env, opts = {}, deps = {}) {
|
|
|
471
593
|
}
|
|
472
594
|
}
|
|
473
595
|
const attemptLog = [];
|
|
474
|
-
|
|
596
|
+
let routeLegs = [route.primary, route.fallback].filter(Boolean);
|
|
597
|
+
if (opts.tools && opts.tools.length > 0) {
|
|
598
|
+
routeLegs = routeLegs.filter((l) => TOOL_CAPABLE_PROVIDERS.has(l.provider));
|
|
599
|
+
if (routeLegs.length === 0) {
|
|
600
|
+
throw new ValidationError(
|
|
601
|
+
`tool-calling requires a tool-capable provider (${[...TOOL_CAPABLE_PROVIDERS].join(", ")}); tier '${tier}' has none \u2014 use tier fast/balanced/smart or a claude-* model override`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
475
605
|
for (const [legIndex, leg] of routeLegs.entries()) {
|
|
476
606
|
if (isProviderCoolingDown(leg.provider, now)) {
|
|
477
607
|
logger?.warn?.("llm.provider.coolingDown", { provider: leg.provider });
|
|
@@ -485,7 +615,7 @@ async function complete(messages, env, opts = {}, deps = {}) {
|
|
|
485
615
|
}
|
|
486
616
|
try {
|
|
487
617
|
const result = await callOne(leg, messages, opts, env, fetchImpl, logger, now);
|
|
488
|
-
if (!result.parsed.content) {
|
|
618
|
+
if (!result.parsed.content && !(result.parsed.toolCalls && result.parsed.toolCalls.length > 0)) {
|
|
489
619
|
throw { provider: leg.provider, status: 200, retryable: false, message: "empty content" };
|
|
490
620
|
}
|
|
491
621
|
logger?.info?.("llm.complete", {
|
|
@@ -512,7 +642,9 @@ async function complete(messages, env, opts = {}, deps = {}) {
|
|
|
512
642
|
},
|
|
513
643
|
latency: now() - startedAt,
|
|
514
644
|
attempts: result.attempts,
|
|
515
|
-
gatewayRequestId: result.gatewayRequestId
|
|
645
|
+
gatewayRequestId: result.gatewayRequestId,
|
|
646
|
+
stopReason: result.parsed.stopReason,
|
|
647
|
+
toolCalls: result.parsed.toolCalls
|
|
516
648
|
};
|
|
517
649
|
const costUsd = estimateCostUsd(llmResult.tokens, llmResult.model);
|
|
518
650
|
if (opts.maxCostUsd !== void 0 && costUsd > opts.maxCostUsd) {
|
|
@@ -586,7 +718,7 @@ async function* completionStream(messages, env, opts = {}) {
|
|
|
586
718
|
const logger = deps.logger;
|
|
587
719
|
const startedAt = now();
|
|
588
720
|
const tier = opts.tier ?? "balanced";
|
|
589
|
-
const system = opts
|
|
721
|
+
const system = systemText(opts, messages);
|
|
590
722
|
const tokenEstimate = estimateTokens(messages, system);
|
|
591
723
|
const route = plan(tier, opts, tokenEstimate);
|
|
592
724
|
const streamLeg = route.primary.provider === "grok" && !env.GROK_API_KEY && route.fallback?.provider === "anthropic" ? route.fallback : route.primary;
|
package/dist/index.mjs.map
CHANGED
|
@@ -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` → 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":[]}
|
|
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 * A tool the model may call. `parameters` is a JSON Schema object describing\n * the tool's input. Provider-agnostic; normalized per provider at request time.\n */\nexport interface LLMTool {\n name: string;\n description?: string;\n /** JSON Schema for the tool's input arguments. */\n parameters: Record<string, unknown>;\n}\n\n/**\n * A tool invocation requested by the model, normalized across providers.\n */\nexport interface LLMToolCall {\n /** Provider-assigned call id; echo it back in the matching tool_result. */\n id: string;\n name: string;\n /** Parsed argument object the model passed to the tool. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Structured content block for tool-calling conversations. The field shapes\n * mirror the Anthropic Messages wire format so they pass through unchanged.\n */\nexport type LLMContentBlock =\n | { type: 'text'; text: string }\n | { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> }\n | { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean };\n\n/**\n * Single chat message exchanged with an LLM provider.\n *\n * `content` is a plain string in the common case. For tool-calling\n * conversations it may be an array of {@link LLMContentBlock}s (e.g. an\n * assistant turn carrying `tool_use` blocks, or a user turn carrying\n * `tool_result` blocks). Providers that don't support tool-calling receive\n * the text projection of the content (see `contentToText`).\n */\nexport interface LLMMessage {\n role: 'user' | 'assistant' | 'system';\n content: string | LLMContentBlock[];\n}\n\n/**\n * Flattens message content to plain text for providers/paths that only handle\n * strings. `tool_use` blocks contribute nothing; `tool_result` blocks\n * contribute their textual content.\n */\nfunction contentToText(content: string | LLMContentBlock[]): string {\n if (typeof content === 'string') return content;\n return content\n .map((b) => (b.type === 'text' ? b.text : b.type === 'tool_result' ? b.content : ''))\n .join('');\n}\n\n/**\n * Resolves the system prompt: explicit `opts.system` wins, else the first\n * `system` message, flattened to text. Returns `undefined` when neither is set.\n */\nfunction systemText(opts: LLMOptions, messages: LLMMessage[]): string | undefined {\n if (opts.system !== undefined) return opts.system;\n const c = messages.find((m) => m.role === 'system')?.content;\n return c === undefined ? undefined : contentToText(c);\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 * Tools the model may call. When present, routing **fails closed** to\n * tool-capable providers — failover never falls back to a provider that\n * can't honour the tool schema. See {@link LLMResult.toolCalls}.\n */\n tools?: LLMTool[];\n /**\n * Tool-selection policy. `'auto'` (default when `tools` is set) lets the\n * model decide; `'none'` forbids tool use; `{ name }` forces a specific tool.\n */\n toolChoice?: 'auto' | 'none' | { name: string };\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 * Why generation stopped, normalized across providers. `'tool_use'` means\n * the model is requesting one or more tool calls (see {@link toolCalls}).\n */\n stopReason?: 'end' | 'tool_use' | 'max_tokens' | 'other';\n /**\n * Tool calls the model requested, normalized across providers. Present\n * (non-empty) when `stopReason === 'tool_use'`.\n */\n toolCalls?: LLMToolCall[];\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 += contentToText(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 = systemText(opts, messages);\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 if (opts.tools && opts.tools.length > 0) {\n body.tools = opts.tools.map((t) => ({\n name: t.name,\n description: t.description ?? '',\n input_schema: t.parameters,\n }));\n const tc = opts.toolChoice ?? 'auto';\n body.tool_choice =\n tc === 'auto'\n ? { type: 'auto' }\n : tc === 'none'\n ? { type: 'none' }\n : { type: 'tool', name: tc.name };\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 = systemText(opts, messages);\n const contents = messages\n .filter((m) => m.role !== 'system')\n .map((m) => ({\n role: m.role === 'assistant' ? 'model' : 'user',\n parts: [{ text: contentToText(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\n// ─── OpenAI-style (Grok / DeepSeek / Groq) tool-calling helpers ──────────────\n\n/**\n * Converts provider-agnostic messages to OpenAI chat-completions format,\n * translating the Anthropic-shaped tool blocks: `tool_use` → an assistant\n * message with `tool_calls`; `tool_result` → a standalone `tool` message keyed\n * by `tool_call_id`. Plain-string content passes through unchanged.\n */\nfunction toOpenAiMessages(messages: LLMMessage[], sys: string | undefined): Array<Record<string, unknown>> {\n const out: Array<Record<string, unknown>> = [];\n if (sys) out.push({ role: 'system', content: sys });\n for (const m of messages) {\n if (m.role === 'system') continue;\n if (typeof m.content === 'string') {\n out.push({ role: m.role, content: m.content });\n continue;\n }\n let text = '';\n const toolCalls: Array<Record<string, unknown>> = [];\n const results: Array<{ tool_use_id: string; content: string }> = [];\n for (const b of m.content) {\n if (b.type === 'text') text += b.text;\n else if (b.type === 'tool_use')\n toolCalls.push({ id: b.id, type: 'function', function: { name: b.name, arguments: JSON.stringify(b.input) } });\n else if (b.type === 'tool_result') results.push({ tool_use_id: b.tool_use_id, content: b.content });\n }\n if (results.length > 0) {\n for (const r of results) out.push({ role: 'tool', tool_call_id: r.tool_use_id, content: r.content });\n if (text) out.push({ role: 'user', content: text });\n } else if (toolCalls.length > 0) {\n out.push({ role: 'assistant', content: text || null, tool_calls: toolCalls });\n } else {\n out.push({ role: m.role, content: text });\n }\n }\n return out;\n}\n\n/** Builds the OpenAI `tools` array from {@link LLMOptions.tools}, or undefined. */\nfunction openAiTools(opts: LLMOptions): Array<Record<string, unknown>> | undefined {\n if (!opts.tools || opts.tools.length === 0) return undefined;\n return opts.tools.map((t) => ({\n type: 'function',\n function: { name: t.name, description: t.description ?? '', parameters: t.parameters },\n }));\n}\n\n/** Maps {@link LLMOptions.toolChoice} to the OpenAI `tool_choice` value. */\nfunction openAiToolChoice(tc: LLMOptions['toolChoice']): unknown {\n if (tc === undefined) return undefined;\n if (tc === 'auto' || tc === 'none') return tc;\n return { type: 'function', function: { name: tc.name } };\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 = systemText(opts, messages);\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({ role: m.role, content: contentToText(m.content) });\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 = systemText(opts, messages);\n const body: Record<string, unknown> = {\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: toOpenAiMessages(messages, sys),\n };\n const tools = openAiTools(opts);\n if (tools) {\n body.tools = tools;\n const tc = openAiToolChoice(opts.toolChoice ?? 'auto');\n if (tc !== undefined) body.tool_choice = tc;\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 = systemText(opts, messages);\n const body: Record<string, unknown> = {\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: toOpenAiMessages(messages, sys),\n };\n const tools = openAiTools(opts);\n if (tools) {\n body.tools = tools;\n const tc = openAiToolChoice(opts.toolChoice ?? 'auto');\n if (tc !== undefined) body.tool_choice = tc;\n }\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(body),\n };\n}\n\n// ─── Response parsers ──────────────────────────────────────────────────────\n\ninterface AnthropicResponse {\n content?: Array<{\n type: string;\n text?: string;\n id?: string;\n name?: string;\n input?: Record<string, unknown>;\n }>;\n stop_reason?: 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\n/** Maps a provider stop reason to the normalized {@link LLMResult.stopReason}. */\nfunction normalizeAnthropicStop(reason: string | undefined): LLMResult['stopReason'] {\n switch (reason) {\n case 'end_turn':\n case 'stop_sequence':\n return 'end';\n case 'tool_use':\n return 'tool_use';\n case 'max_tokens':\n return 'max_tokens';\n default:\n return reason ? 'other' : undefined;\n }\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): {\n content: string;\n input: number;\n output: number;\n cacheRead: number;\n cacheWrite: number;\n model?: string;\n toolCalls?: LLMToolCall[];\n stopReason?: LLMResult['stopReason'];\n} {\n const r = json as AnthropicResponse;\n const toolCalls: LLMToolCall[] = (r.content ?? [])\n .filter((c) => c.type === 'tool_use' && typeof c.id === 'string' && typeof c.name === 'string')\n .map((c) => ({ id: c.id!, name: c.name!, arguments: c.input ?? {} }));\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 toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n stopReason: normalizeAnthropicStop(r.stop_reason),\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\ninterface OpenAiResponse {\n choices?: Array<{\n message?: {\n content?: string | null;\n tool_calls?: Array<{ id?: string; function?: { name?: string; arguments?: string } }>;\n };\n finish_reason?: string;\n }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n model?: string;\n}\n\n/** Maps an OpenAI `finish_reason` to the normalized {@link LLMResult.stopReason}. */\nfunction normalizeOpenAiStop(reason: string | undefined): LLMResult['stopReason'] {\n switch (reason) {\n case 'stop':\n return 'end';\n case 'tool_calls':\n case 'function_call':\n return 'tool_use';\n case 'length':\n return 'max_tokens';\n default:\n return reason ? 'other' : undefined;\n }\n}\n\n/** Best-effort parse of an OpenAI tool-call arguments string; `{}` on failure. */\nfunction parseToolArgs(raw: string | undefined): Record<string, unknown> {\n if (!raw) return {};\n try {\n const v = JSON.parse(raw) as unknown;\n return typeof v === 'object' && v !== null ? (v as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\n/**\n * Parses an OpenAI chat-completions response (Grok, DeepSeek), extracting\n * normalized tool calls and a stop reason in addition to text + tokens.\n */\nfunction parseOpenAi(json: unknown): {\n content: string;\n input: number;\n output: number;\n model?: string;\n toolCalls?: LLMToolCall[];\n stopReason?: LLMResult['stopReason'];\n} {\n const r = json as OpenAiResponse;\n const choice = r.choices?.[0];\n const toolCalls: LLMToolCall[] = (choice?.message?.tool_calls ?? [])\n .filter((c) => typeof c.function?.name === 'string')\n .map((c, i) => ({\n id: c.id ?? `call_${i}`,\n name: c.function!.name!,\n arguments: parseToolArgs(c.function?.arguments),\n }));\n return {\n content: choice?.message?.content ?? '',\n input: r.usage?.prompt_tokens ?? 0,\n output: r.usage?.completion_tokens ?? 0,\n model: r.model,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n stopReason: normalizeOpenAiStop(choice?.finish_reason),\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\n/**\n * Providers whose request builder + response parser support tool-calling.\n * When `opts.tools` is set, routing is restricted to this set and **fails\n * closed** rather than silently calling a provider that would ignore the\n * tools. Expanded in 1b as the other providers' tool formats are normalized.\n */\nconst TOOL_CAPABLE_PROVIDERS = new Set<LLMProvider>(['anthropic', 'grok', 'deepseek']);\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; toolCalls?: LLMToolCall[]; stopReason?: LLMResult['stopReason'] }; 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: parseOpenAi(json), gatewayRequestId, attempts };\n case 'deepseek':\n return { parsed: parseOpenAi(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 = systemText(opts, messages);\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 let routeLegs = [route.primary, route.fallback].filter(Boolean) as Array<{ provider: LLMProvider; model: string }>;\n // Tool-calling fails closed: never fall back to a provider that can't honour\n // the tool schema. Narrow the route to tool-capable providers when tools are set.\n if (opts.tools && opts.tools.length > 0) {\n routeLegs = routeLegs.filter((l) => TOOL_CAPABLE_PROVIDERS.has(l.provider));\n if (routeLegs.length === 0) {\n throw new ValidationError(\n `tool-calling requires a tool-capable provider (${[...TOOL_CAPABLE_PROVIDERS].join(', ')}); tier '${tier}' has none — use tier fast/balanced/smart or a claude-* model override`,\n );\n }\n }\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 // A tool_use turn legitimately has no text content — only treat a\n // genuinely empty response (no text AND no tool calls) as a failure.\n if (!result.parsed.content && !(result.parsed.toolCalls && result.parsed.toolCalls.length > 0)) {\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 stopReason: result.parsed.stopReason,\n toolCalls: result.parsed.toolCalls,\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 = systemText(opts, messages);\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,15 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latimer-woods-tech/llm",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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": {
|
|
11
|
-
"registry": "https://npm.pkg.github.com"
|
|
12
|
-
},
|
|
10
|
+
"publishConfig": {},
|
|
13
11
|
"main": "./dist/index.mjs",
|
|
14
12
|
"module": "./dist/index.mjs",
|
|
15
13
|
"types": "./dist/index.d.mts",
|