@latimer-woods-tech/llm 0.3.1 → 0.4.2
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 +109 -0
- package/README.md +8 -1
- package/package.json +3 -2
- package/dist/index.d.mts +0 -206
- package/dist/index.mjs +0 -583
- package/dist/index.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,114 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 — 2026-06-03
|
|
4
|
+
|
|
5
|
+
### Added — tool-calling (Agent Runtime Phase 1a; Anthropic)
|
|
6
|
+
|
|
7
|
+
- **`LLMOptions.tools`** (`LLMTool[]`) and **`LLMOptions.toolChoice`**
|
|
8
|
+
(`'auto' | 'none' | { name }`). When `tools` is set, routing **fails closed**
|
|
9
|
+
to tool-capable providers — failover never silently falls back to one that
|
|
10
|
+
can't honour the tool schema (1a: Anthropic only; others land in 1b).
|
|
11
|
+
- **`LLMResult.toolCalls`** (`LLMToolCall[]`) and **`LLMResult.stopReason`**
|
|
12
|
+
(`'end' | 'tool_use' | 'max_tokens' | 'other'`), normalized across providers.
|
|
13
|
+
- **`LLMMessage.content`** now accepts `string | LLMContentBlock[]` — structured
|
|
14
|
+
`text` / `tool_use` / `tool_result` blocks for multi-turn tool conversations.
|
|
15
|
+
Backwards-compatible: existing `string` content is unchanged; providers without
|
|
16
|
+
tool support receive the text projection.
|
|
17
|
+
- New exported types: `LLMTool`, `LLMToolCall`, `LLMContentBlock`.
|
|
18
|
+
|
|
19
|
+
### Notes
|
|
20
|
+
|
|
21
|
+
- A `tool_use` response with no text content is no longer treated as an empty
|
|
22
|
+
(failed) completion.
|
|
23
|
+
- Anthropic `tool_use` blocks pass through unchanged (the block shapes mirror the
|
|
24
|
+
Messages wire format). Other providers' tool formats are normalized in 1a's
|
|
25
|
+
follow-ups (1b: Grok/DeepSeek/Gemini; 1c: streaming tool-call accumulation).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 0.4.1 — 2026-06-03
|
|
30
|
+
|
|
31
|
+
### Added (no breaking changes)
|
|
32
|
+
|
|
33
|
+
- Export `MODEL_PRICE_PER_1M` — the canonical USD-per-1M-tokens rate table. This
|
|
34
|
+
makes it the single source of truth for pricing across the platform;
|
|
35
|
+
`@latimer-woods-tech/llm-meter` now derives its cents table from it and enforces
|
|
36
|
+
parity with a drift-guard test. Make all rate changes here.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 0.3.4 — 2026-05-28
|
|
41
|
+
|
|
42
|
+
### Added (no breaking changes)
|
|
43
|
+
|
|
44
|
+
- **`workbench` tier** routes to `deepseek-chat` with Groq fallback for boring,
|
|
45
|
+
reviewable, non-sensitive internal batch work.
|
|
46
|
+
- **`DEEPSEEK_API_KEY`** added as an optional `LLMEnv` binding. It is required only
|
|
47
|
+
for `tier: 'workbench'` or explicit `deepseek-*` model overrides.
|
|
48
|
+
- **DeepSeek pricing entries** for `deepseek-chat` and `deepseek-reasoner` so cost
|
|
49
|
+
caps and ledger rows use known rates instead of the conservative Opus fallback.
|
|
50
|
+
|
|
51
|
+
### Guardrail
|
|
52
|
+
|
|
53
|
+
- `workbench` is for docs summaries, changelog drafts, issue triage, and classification.
|
|
54
|
+
Do not route secrets, customer PII, billing data, production ops, or final
|
|
55
|
+
customer-facing answers through DeepSeek.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 0.3.3 — 2026-05-27
|
|
60
|
+
|
|
61
|
+
### Changed (no breaking changes)
|
|
62
|
+
|
|
63
|
+
- **`fast` tier now routes to Grok 4.3 (primary) → Anthropic Haiku (fallback).**
|
|
64
|
+
When `GROK_API_KEY` is present in `LLMEnv`, the `fast` tier sends completions to
|
|
65
|
+
`grok-4.3` first and falls back to `claude-haiku-4-20250514` if Grok is unavailable
|
|
66
|
+
or returns an error. When `GROK_API_KEY` is absent, the request goes directly to
|
|
67
|
+
Anthropic Haiku (same behaviour as before). Callers that already set `tier: 'fast'`
|
|
68
|
+
pick up Grok routing automatically; no code change needed.
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **`LLMOptions.reasoningEffort`** — `'none' | 'low' | 'medium' | 'high'`
|
|
73
|
+
(optional, defaults to `'none'`). Forwarded to the Grok `reasoning_effort` parameter
|
|
74
|
+
for `grok-4.3`; ignored for all other providers.
|
|
75
|
+
- **`MODELS.grok.fast`** updated from `'grok-4-fast'` to `'grok-4.3'`. Old alias
|
|
76
|
+
`'grok-4-fast'` retained as deprecated with updated pricing so historical ledger
|
|
77
|
+
rows remain accurate.
|
|
78
|
+
- **Grok pricing update**: `grok-4.3` at $1.25/$2.50 per MTok (in/out).
|
|
79
|
+
Old `grok-4-fast` and `grok-3-mini-latest` re-priced to the same $1.25/$2.50 rate.
|
|
80
|
+
- **`for (const [legIndex, leg] of routeLegs.entries())`** — renamed loop variable so
|
|
81
|
+
`legIndex` is accessible for the last-leg 429 rate-limit short-circuit.
|
|
82
|
+
|
|
83
|
+
### Consumers
|
|
84
|
+
|
|
85
|
+
- To opt-in to Grok 4.3: add `GROK_API_KEY` to your `LLMEnv` binding. No other code
|
|
86
|
+
change required for `tier: 'fast'` callers.
|
|
87
|
+
- `GROK_API_KEY` is optional. If absent, fast-tier routing is identical to 0.3.2
|
|
88
|
+
(Anthropic Haiku only).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 0.3.2 — 2026-05-27
|
|
93
|
+
|
|
94
|
+
### Added (no breaking changes)
|
|
95
|
+
|
|
96
|
+
- **`LLMOptions.workload`** — optional string label (`'insights'`, `'copy'`, `'lead-qualification'`, …)
|
|
97
|
+
forwarded to cost-recording calls for per-workload cost attribution in dashboards.
|
|
98
|
+
- **Missing model pricing entries** in `MODEL_PRICE_PER_1M`:
|
|
99
|
+
`claude-haiku-4-5-20251001`, `claude-sonnet-4-20250514`, `claude-opus-4-20250514`
|
|
100
|
+
(aliases for variants that share pricing with their shorthand names; prevents
|
|
101
|
+
`estimateCostUsd` from silently returning `$0` for these model IDs).
|
|
102
|
+
- **`workload` forwarded** to `recordOrgCostUsage` so per-workload breakdowns appear
|
|
103
|
+
in the cost-tracking KV store.
|
|
104
|
+
|
|
105
|
+
### Consumers
|
|
106
|
+
|
|
107
|
+
- Existing callers do not need to set `workload`; it is optional and defaults to `undefined`.
|
|
108
|
+
- The `recordOrgCostUsage` signature is unchanged; `workload` is an additive internal field.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
3
112
|
## 0.3.1 — 2026-05-02 PM
|
|
4
113
|
|
|
5
114
|
### Added (no breaking changes)
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @latimer-woods-tech/llm
|
|
2
2
|
|
|
3
3
|
Tier-routed LLM orchestration for the Factory platform, with Cloudflare AI Gateway, Anthropic
|
|
4
|
-
primary, Gemini 2.5 Pro long-context fallback, and
|
|
4
|
+
primary, Gemini 2.5 Pro long-context fallback, Groq verifier, and DeepSeek workbench routing.
|
|
5
5
|
|
|
6
6
|
## Routing (0.3.0)
|
|
7
7
|
|
|
@@ -11,6 +11,7 @@ primary, Gemini 2.5 Pro long-context fallback, and Groq verifier.
|
|
|
11
11
|
| `balanced` *(default)* | Claude Sonnet 4 | Gemini 2.5 Pro | swaps to Gemini when est. tokens ≥ 150k |
|
|
12
12
|
| `smart` | Claude Opus 4 | Gemini 2.5 Pro | ditto; tools, long reasoning |
|
|
13
13
|
| `verifier` | Groq Llama 3.3 70B | — | cheap second opinion; no fallback |
|
|
14
|
+
| `workbench` | DeepSeek Chat | Groq Llama | boring, reviewable, non-sensitive batch work |
|
|
14
15
|
|
|
15
16
|
All traffic flows through `AI_GATEWAY_BASE_URL`. The gateway handles caching, rate-limit shedding,
|
|
16
17
|
and per-project cost telemetry.
|
|
@@ -29,6 +30,7 @@ const res = await complete(
|
|
|
29
30
|
AI_GATEWAY_BASE_URL: env.AI_GATEWAY_BASE_URL,
|
|
30
31
|
ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY,
|
|
31
32
|
GROQ_API_KEY: env.GROQ_API_KEY,
|
|
33
|
+
DEEPSEEK_API_KEY: env.DEEPSEEK_API_KEY,
|
|
32
34
|
VERTEX_ACCESS_TOKEN: env.VERTEX_ACCESS_TOKEN,
|
|
33
35
|
VERTEX_PROJECT: env.VERTEX_PROJECT,
|
|
34
36
|
VERTEX_LOCATION: env.VERTEX_LOCATION,
|
|
@@ -47,6 +49,11 @@ if (res.ok) {
|
|
|
47
49
|
}
|
|
48
50
|
```
|
|
49
51
|
|
|
52
|
+
Use `tier: 'workbench'` only for low-risk internal jobs: docs summaries, changelog drafts,
|
|
53
|
+
classification, issue triage, and other outputs a human or higher-trust model can review. Do not
|
|
54
|
+
send secrets, customer PII, production ops requests, billing data, or final customer-facing answers
|
|
55
|
+
through this lane.
|
|
56
|
+
|
|
50
57
|
## Vertex access token
|
|
51
58
|
|
|
52
59
|
Minted via the JWT-bearer flow from a GCP service account. See
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latimer-woods-tech/llm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"build": "tsup src/index.ts --format esm --dts",
|
|
27
27
|
"test": "vitest run --coverage",
|
|
28
28
|
"lint": "eslint src --max-warnings 0",
|
|
29
|
-
"typecheck": "tsc --noEmit"
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"prepublish": "npm run lint && npm run typecheck && npm run test && npm run build"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"@latimer-woods-tech/errors": "^0.2.0",
|
package/dist/index.d.mts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { FactoryResponse } from '@latimer-woods-tech/errors';
|
|
2
|
-
import { Logger } from '@latimer-woods-tech/logger';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Single chat message exchanged with an LLM provider.
|
|
6
|
-
*/
|
|
7
|
-
interface LLMMessage {
|
|
8
|
-
role: 'user' | 'assistant' | 'system';
|
|
9
|
-
content: string;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Quality tier selected by the caller. Routing is workload-split:
|
|
13
|
-
* - `fast` → Anthropic Haiku (short, latency-sensitive)
|
|
14
|
-
* - `balanced` → Anthropic Sonnet (default)
|
|
15
|
-
* - `smart` → Anthropic Opus OR Gemini 2.5 Pro if input is long-context (>150k tokens estimated)
|
|
16
|
-
* - `verifier` → Groq Llama (cheap second opinion; only used from verifier code path)
|
|
17
|
-
*/
|
|
18
|
-
type LLMTier = 'fast' | 'balanced' | 'smart' | 'verifier';
|
|
19
|
-
/**
|
|
20
|
-
* Options that influence LLM completion behaviour.
|
|
21
|
-
*/
|
|
22
|
-
interface LLMOptions {
|
|
23
|
-
/** Quality tier; see {@link LLMTier}. Defaults to `balanced`. */
|
|
24
|
-
tier?: LLMTier;
|
|
25
|
-
/** Explicit model override. Takes precedence over tier. */
|
|
26
|
-
model?: string;
|
|
27
|
-
maxTokens?: number;
|
|
28
|
-
temperature?: number;
|
|
29
|
-
system?: string;
|
|
30
|
-
/** Token budget above which we force long-context routing (Gemini). */
|
|
31
|
-
longContextThreshold?: number;
|
|
32
|
-
/** Per-call cancellation signal. Aborts the in-flight provider request. */
|
|
33
|
-
signal?: AbortSignal;
|
|
34
|
-
/** Optional run identifier stamped on ledger rows + logs. */
|
|
35
|
-
runId?: string;
|
|
36
|
-
/** Optional project identifier stamped on ledger rows + logs. */
|
|
37
|
-
project?: string;
|
|
38
|
-
/** Optional actor identifier (supervisor / worker / human). */
|
|
39
|
-
actor?: string;
|
|
40
|
-
/** Anthropic prompt-cache control. Defaults to `true` for `system` prompts ≥ 1024 tokens. */
|
|
41
|
-
promptCache?: boolean;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Provider that produced an LLM response. `grok` removed in 0.3.0.
|
|
45
|
-
*/
|
|
46
|
-
type LLMProvider = 'anthropic' | 'gemini' | 'groq' | 'grok';
|
|
47
|
-
/**
|
|
48
|
-
* Result returned by a successful completion.
|
|
49
|
-
*/
|
|
50
|
-
interface LLMResult {
|
|
51
|
-
content: string;
|
|
52
|
-
provider: LLMProvider;
|
|
53
|
-
model: string;
|
|
54
|
-
tier: LLMTier;
|
|
55
|
-
tokens: {
|
|
56
|
-
input: number;
|
|
57
|
-
output: number;
|
|
58
|
-
cacheRead?: number;
|
|
59
|
-
cacheWrite?: number;
|
|
60
|
-
};
|
|
61
|
-
latency: number;
|
|
62
|
-
/** Number of attempts before success (1 = primary succeeded). */
|
|
63
|
-
attempts: number;
|
|
64
|
-
/** Monotonic request id from AI Gateway, if present in headers. */
|
|
65
|
-
gatewayRequestId?: string;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Environment bindings required by {@link complete}.
|
|
69
|
-
*
|
|
70
|
-
* `AI_GATEWAY_BASE_URL` is REQUIRED in 0.3.0. All provider calls flow through the
|
|
71
|
-
* Cloudflare AI Gateway for unified logging, rate limiting, and cost telemetry.
|
|
72
|
-
* In test/dev the caller may pass a custom fetch impl that short-circuits this.
|
|
73
|
-
*/
|
|
74
|
-
interface LLMEnv {
|
|
75
|
-
AI_GATEWAY_BASE_URL: string;
|
|
76
|
-
ANTHROPIC_API_KEY: string;
|
|
77
|
-
GROQ_API_KEY: string;
|
|
78
|
-
/** Optional — only required when caller passes `{ model: 'grok-*' }` override. */
|
|
79
|
-
GROK_API_KEY?: string;
|
|
80
|
-
/**
|
|
81
|
-
* Google Cloud short-lived access token with `aiplatform.endpoints.predict`.
|
|
82
|
-
* Callers mint this via the JWT-bearer flow (service account → token exchange);
|
|
83
|
-
* see `docs/runbooks/rotate-gcp-sa.md`. Token must be valid for ≥ 5 minutes.
|
|
84
|
-
*/
|
|
85
|
-
VERTEX_ACCESS_TOKEN: string;
|
|
86
|
-
VERTEX_PROJECT: string;
|
|
87
|
-
VERTEX_LOCATION: string;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Optional dependencies for {@link complete}.
|
|
91
|
-
*/
|
|
92
|
-
interface LLMDeps {
|
|
93
|
-
fetch?: typeof fetch;
|
|
94
|
-
logger?: Logger;
|
|
95
|
-
now?: () => number;
|
|
96
|
-
}
|
|
97
|
-
declare const MODELS: {
|
|
98
|
-
readonly anthropic: {
|
|
99
|
-
readonly fast: "claude-haiku-4-20250514";
|
|
100
|
-
readonly balanced: "claude-sonnet-4-6";
|
|
101
|
-
readonly smart: "claude-opus-4-7";
|
|
102
|
-
};
|
|
103
|
-
readonly gemini: {
|
|
104
|
-
readonly smart: "gemini-2.5-pro";
|
|
105
|
-
};
|
|
106
|
-
readonly groq: {
|
|
107
|
-
readonly verifier: "llama-3.3-70b-versatile";
|
|
108
|
-
};
|
|
109
|
-
readonly grok: {
|
|
110
|
-
/** Opt-in only via `{ model: 'grok-*' }`. Not in default tier routing. */
|
|
111
|
-
readonly fast: "grok-4-fast";
|
|
112
|
-
readonly mini: "grok-3-mini-latest";
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
/** Cooldown duration in ms after a provider exhausts all retries. */
|
|
116
|
-
declare const PROVIDER_COOLDOWN_MS = 30000;
|
|
117
|
-
/**
|
|
118
|
-
* Returns `true` if the provider is currently in its cooldown window.
|
|
119
|
-
* Uses the injected `now` function (or `Date.now`) for testability.
|
|
120
|
-
*/
|
|
121
|
-
declare function isProviderCoolingDown(provider: LLMProvider, now?: () => number): boolean;
|
|
122
|
-
/**
|
|
123
|
-
* Marks a provider as cooling down for {@link PROVIDER_COOLDOWN_MS} milliseconds.
|
|
124
|
-
*/
|
|
125
|
-
declare function markProviderCoolingDown(provider: LLMProvider, now?: () => number): void;
|
|
126
|
-
/**
|
|
127
|
-
* Clears the cooldown state for a provider after a successful call.
|
|
128
|
-
*/
|
|
129
|
-
declare function clearProviderCooldown(provider: LLMProvider): void;
|
|
130
|
-
declare const BASE_BACKOFF_MS = 250;
|
|
131
|
-
/**
|
|
132
|
-
* Run a completion through the routing plan for the requested tier.
|
|
133
|
-
*
|
|
134
|
-
* Routing summary (0.3.0):
|
|
135
|
-
* - `fast` → Anthropic Haiku
|
|
136
|
-
* - `balanced` → Anthropic Sonnet; Gemini 2.5 Pro if `longContextThreshold` exceeded
|
|
137
|
-
* - `smart` → Anthropic Opus; Gemini 2.5 Pro if long-context
|
|
138
|
-
* - `verifier` → Groq Llama 3.3 70B (no fallback — verifier is inherently cheap/best-effort)
|
|
139
|
-
*
|
|
140
|
-
* All provider traffic flows through Cloudflare AI Gateway at `AI_GATEWAY_BASE_URL`.
|
|
141
|
-
*
|
|
142
|
-
* Per-provider reliability guarantees (0.4.0):
|
|
143
|
-
* - Exponential backoff with jitter on 429 / 5xx (base 500ms, cap 8s, up to 2 retries).
|
|
144
|
-
* - Provider cooldown: after exhausting retries the provider is marked cooling down
|
|
145
|
-
* for 30 seconds; subsequent calls skip it and go straight to the fallback leg.
|
|
146
|
-
*
|
|
147
|
-
* @param messages - Ordered chat history.
|
|
148
|
-
* @param env - API key + gateway bindings.
|
|
149
|
-
* @param opts - Optional tier/model/parameters override.
|
|
150
|
-
* @param deps - Optional fetch/logger/clock injection (for testing).
|
|
151
|
-
* @returns A {@link FactoryResponse} carrying either an {@link LLMResult} or
|
|
152
|
-
* an error (`LLM_ALL_PROVIDERS_FAILED`, `LLM_RATE_LIMITED`, or `INTERNAL_ERROR`).
|
|
153
|
-
*/
|
|
154
|
-
declare function complete(messages: LLMMessage[], env: LLMEnv, opts?: LLMOptions, deps?: LLMDeps): Promise<FactoryResponse<LLMResult>>;
|
|
155
|
-
/**
|
|
156
|
-
* Streams a completion from the primary Anthropic provider, yielding text chunks
|
|
157
|
-
* as they arrive. Falls back to the non-streaming {@link complete} function when
|
|
158
|
-
* the provider does not support streaming (i.e. a non-Anthropic primary is selected).
|
|
159
|
-
*
|
|
160
|
-
* The generator's **return value** (accessible via `gen.return()` or by consuming
|
|
161
|
-
* the full iteration) is an {@link LLMResult} with the same shape as {@link complete}.
|
|
162
|
-
*
|
|
163
|
-
* Usage pattern:
|
|
164
|
-
* ```ts
|
|
165
|
-
* const gen = completionStream(messages, env, opts);
|
|
166
|
-
* for await (const chunk of gen) {
|
|
167
|
-
* // stream chunk to client
|
|
168
|
-
* }
|
|
169
|
-
* const result = (await gen.return(undefined)).value; // LLMResult
|
|
170
|
-
* ```
|
|
171
|
-
*
|
|
172
|
-
* @param messages - Ordered chat history.
|
|
173
|
-
* @param env - API key + gateway bindings.
|
|
174
|
-
* @param opts - Optional tier/model/parameters override. Accepts `deps` as nested field.
|
|
175
|
-
* @returns An async generator that yields `string` chunks and returns an {@link LLMResult}.
|
|
176
|
-
*/
|
|
177
|
-
declare function completionStream(messages: LLMMessage[], env: LLMEnv, opts?: LLMOptions & {
|
|
178
|
-
deps?: LLMDeps;
|
|
179
|
-
}): AsyncGenerator<string, LLMResult, unknown>;
|
|
180
|
-
/**
|
|
181
|
-
* Returns `true` if `response` contains at least one verbatim phrase of at
|
|
182
|
-
* least 5 consecutive whitespace-delimited tokens that also appears in one of
|
|
183
|
-
* the `sources` strings.
|
|
184
|
-
*
|
|
185
|
-
* Returns `true` unconditionally when `sources` is empty (no grounding
|
|
186
|
-
* documents means grounding cannot be violated).
|
|
187
|
-
*
|
|
188
|
-
* This is a lightweight guard for RAG pipelines — it detects obvious
|
|
189
|
-
* hallucinations where the model generates content not present in any
|
|
190
|
-
* retrieved source. It is NOT a semantic similarity check.
|
|
191
|
-
*
|
|
192
|
-
* @param response - The LLM-generated text to inspect.
|
|
193
|
-
* @param sources - Retrieved source documents to check against.
|
|
194
|
-
* @returns `true` if the response is grounded, `false` if hallucination detected.
|
|
195
|
-
*
|
|
196
|
-
* @example
|
|
197
|
-
* ```ts
|
|
198
|
-
* const grounded = assertGrounding(llmAnswer, retrievedDocs);
|
|
199
|
-
* if (!grounded) {
|
|
200
|
-
* // flag or re-rank the response
|
|
201
|
-
* }
|
|
202
|
-
* ```
|
|
203
|
-
*/
|
|
204
|
-
declare function assertGrounding(response: string, sources: string[]): boolean;
|
|
205
|
-
|
|
206
|
-
export { BASE_BACKOFF_MS, type LLMDeps, type LLMEnv, type LLMMessage, type LLMOptions, type LLMProvider, type LLMResult, type LLMTier, MODELS, PROVIDER_COOLDOWN_MS, assertGrounding, clearProviderCooldown, complete, completionStream, isProviderCoolingDown, markProviderCoolingDown };
|
package/dist/index.mjs
DELETED
|
@@ -1,583 +0,0 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
import {
|
|
3
|
-
InternalError,
|
|
4
|
-
RateLimitError,
|
|
5
|
-
ValidationError,
|
|
6
|
-
toErrorResponse
|
|
7
|
-
} from "@latimer-woods-tech/errors";
|
|
8
|
-
var MODELS = {
|
|
9
|
-
anthropic: {
|
|
10
|
-
fast: "claude-haiku-4-20250514",
|
|
11
|
-
balanced: "claude-sonnet-4-6",
|
|
12
|
-
smart: "claude-opus-4-7"
|
|
13
|
-
},
|
|
14
|
-
gemini: {
|
|
15
|
-
smart: "gemini-2.5-pro"
|
|
16
|
-
},
|
|
17
|
-
groq: {
|
|
18
|
-
verifier: "llama-3.3-70b-versatile"
|
|
19
|
-
},
|
|
20
|
-
grok: {
|
|
21
|
-
/** Opt-in only via `{ model: 'grok-*' }`. Not in default tier routing. */
|
|
22
|
-
fast: "grok-4-fast",
|
|
23
|
-
mini: "grok-3-mini-latest"
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
var DEFAULT_MAX_TOKENS = 1024;
|
|
27
|
-
var DEFAULT_TEMPERATURE = 0.7;
|
|
28
|
-
var DEFAULT_LONG_CONTEXT_THRESHOLD = 15e4;
|
|
29
|
-
var BACKOFF_BASE_MS = 500;
|
|
30
|
-
var BACKOFF_CAP_MS = 8e3;
|
|
31
|
-
var BACKOFF_JITTER_MAX_MS = 250;
|
|
32
|
-
var PER_PROVIDER_MAX_ATTEMPTS = 3;
|
|
33
|
-
var providerCooldownUntil = /* @__PURE__ */ new Map();
|
|
34
|
-
var PROVIDER_COOLDOWN_MS = 3e4;
|
|
35
|
-
function isProviderCoolingDown(provider, now = Date.now) {
|
|
36
|
-
const until = providerCooldownUntil.get(provider);
|
|
37
|
-
if (until === void 0) return false;
|
|
38
|
-
return now() < until;
|
|
39
|
-
}
|
|
40
|
-
function markProviderCoolingDown(provider, now = Date.now) {
|
|
41
|
-
providerCooldownUntil.set(provider, now() + PROVIDER_COOLDOWN_MS);
|
|
42
|
-
}
|
|
43
|
-
function clearProviderCooldown(provider) {
|
|
44
|
-
providerCooldownUntil.delete(provider);
|
|
45
|
-
}
|
|
46
|
-
var BASE_BACKOFF_MS = 250;
|
|
47
|
-
function isRetryableForBackoff(status) {
|
|
48
|
-
return status === 429 || status >= 500 && status < 600;
|
|
49
|
-
}
|
|
50
|
-
function estimateTokens(messages, system) {
|
|
51
|
-
let chars = system?.length ?? 0;
|
|
52
|
-
for (const m of messages) chars += m.content.length;
|
|
53
|
-
return Math.ceil(chars / 4);
|
|
54
|
-
}
|
|
55
|
-
function sleep(ms, signal) {
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
const t = setTimeout(resolve, ms);
|
|
58
|
-
if (signal) {
|
|
59
|
-
const onAbort = () => {
|
|
60
|
-
clearTimeout(t);
|
|
61
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
62
|
-
};
|
|
63
|
-
if (signal.aborted) onAbort();
|
|
64
|
-
else signal.addEventListener("abort", onAbort, { once: true });
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
function computeBackoffMs(attempt) {
|
|
69
|
-
const jitter = Math.floor(Math.random() * BACKOFF_JITTER_MAX_MS);
|
|
70
|
-
return Math.min(BACKOFF_BASE_MS * Math.pow(2, attempt) + jitter, BACKOFF_CAP_MS);
|
|
71
|
-
}
|
|
72
|
-
function buildAnthropicRequest(model, messages, opts, env, streaming = false) {
|
|
73
|
-
const sys = opts.system ?? messages.find((m) => m.role === "system")?.content;
|
|
74
|
-
const filtered = messages.filter((m) => m.role !== "system");
|
|
75
|
-
const body = {
|
|
76
|
-
model,
|
|
77
|
-
max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
78
|
-
temperature: opts.temperature ?? DEFAULT_TEMPERATURE,
|
|
79
|
-
messages: filtered.map((m) => ({ role: m.role, content: m.content }))
|
|
80
|
-
};
|
|
81
|
-
if (streaming) {
|
|
82
|
-
body.stream = true;
|
|
83
|
-
}
|
|
84
|
-
if (sys) {
|
|
85
|
-
const cache = opts.promptCache ?? sys.length >= 4096;
|
|
86
|
-
body.system = cache ? [{ type: "text", text: sys, cache_control: { type: "ephemeral" } }] : sys;
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
url: `${env.AI_GATEWAY_BASE_URL}/anthropic/v1/messages`,
|
|
90
|
-
headers: {
|
|
91
|
-
"content-type": "application/json",
|
|
92
|
-
"x-api-key": env.ANTHROPIC_API_KEY,
|
|
93
|
-
"anthropic-version": "2023-06-01",
|
|
94
|
-
"anthropic-beta": "prompt-caching-2024-07-31"
|
|
95
|
-
},
|
|
96
|
-
body: JSON.stringify(body)
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function buildGeminiRequest(model, messages, opts, env) {
|
|
100
|
-
const sys = opts.system ?? messages.find((m) => m.role === "system")?.content;
|
|
101
|
-
const contents = messages.filter((m) => m.role !== "system").map((m) => ({
|
|
102
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
103
|
-
parts: [{ text: m.content }]
|
|
104
|
-
}));
|
|
105
|
-
const body = {
|
|
106
|
-
contents,
|
|
107
|
-
generationConfig: {
|
|
108
|
-
maxOutputTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
109
|
-
temperature: opts.temperature ?? DEFAULT_TEMPERATURE
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
if (sys) {
|
|
113
|
-
body.systemInstruction = { parts: [{ text: sys }] };
|
|
114
|
-
}
|
|
115
|
-
const path = `v1/projects/${env.VERTEX_PROJECT}/locations/${env.VERTEX_LOCATION}/publishers/google/models/${model}:generateContent`;
|
|
116
|
-
return {
|
|
117
|
-
url: `${env.AI_GATEWAY_BASE_URL}/google-vertex-ai/${path}`,
|
|
118
|
-
headers: {
|
|
119
|
-
"content-type": "application/json",
|
|
120
|
-
authorization: `Bearer ${env.VERTEX_ACCESS_TOKEN}`
|
|
121
|
-
},
|
|
122
|
-
body: JSON.stringify(body)
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function buildGroqRequest(model, messages, opts, env) {
|
|
126
|
-
const sys = opts.system ?? messages.find((m) => m.role === "system")?.content;
|
|
127
|
-
const merged = [];
|
|
128
|
-
if (sys) merged.push({ role: "system", content: sys });
|
|
129
|
-
for (const m of messages) if (m.role !== "system") merged.push(m);
|
|
130
|
-
return {
|
|
131
|
-
url: `${env.AI_GATEWAY_BASE_URL}/groq/openai/v1/chat/completions`,
|
|
132
|
-
headers: {
|
|
133
|
-
"content-type": "application/json",
|
|
134
|
-
authorization: `Bearer ${env.GROQ_API_KEY}`
|
|
135
|
-
},
|
|
136
|
-
body: JSON.stringify({
|
|
137
|
-
model,
|
|
138
|
-
max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
139
|
-
temperature: opts.temperature ?? DEFAULT_TEMPERATURE,
|
|
140
|
-
messages: merged
|
|
141
|
-
})
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function buildGrokRequest(model, messages, opts, env) {
|
|
145
|
-
if (!env.GROK_API_KEY) {
|
|
146
|
-
throw new ValidationError("GROK_API_KEY required for grok-* model override");
|
|
147
|
-
}
|
|
148
|
-
const sys = opts.system ?? messages.find((m) => m.role === "system")?.content;
|
|
149
|
-
const merged = [];
|
|
150
|
-
if (sys) merged.push({ role: "system", content: sys });
|
|
151
|
-
for (const m of messages) if (m.role !== "system") merged.push(m);
|
|
152
|
-
return {
|
|
153
|
-
url: `${env.AI_GATEWAY_BASE_URL}/grok/v1/chat/completions`,
|
|
154
|
-
headers: {
|
|
155
|
-
"content-type": "application/json",
|
|
156
|
-
authorization: `Bearer ${env.GROK_API_KEY}`
|
|
157
|
-
},
|
|
158
|
-
body: JSON.stringify({
|
|
159
|
-
model,
|
|
160
|
-
max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
161
|
-
temperature: opts.temperature ?? DEFAULT_TEMPERATURE,
|
|
162
|
-
messages: merged
|
|
163
|
-
})
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
function parseAnthropic(json) {
|
|
167
|
-
const r = json;
|
|
168
|
-
return {
|
|
169
|
-
content: r.content?.find((c) => c.type === "text")?.text ?? "",
|
|
170
|
-
input: r.usage?.input_tokens ?? 0,
|
|
171
|
-
output: r.usage?.output_tokens ?? 0,
|
|
172
|
-
cacheRead: r.usage?.cache_read_input_tokens ?? 0,
|
|
173
|
-
cacheWrite: r.usage?.cache_creation_input_tokens ?? 0,
|
|
174
|
-
model: r.model
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
function parseGemini(json) {
|
|
178
|
-
const r = json;
|
|
179
|
-
const text = r.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
|
|
180
|
-
return {
|
|
181
|
-
content: text,
|
|
182
|
-
input: r.usageMetadata?.promptTokenCount ?? 0,
|
|
183
|
-
output: r.usageMetadata?.candidatesTokenCount ?? 0
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
function parseGroq(json) {
|
|
187
|
-
const r = json;
|
|
188
|
-
return {
|
|
189
|
-
content: r.choices?.[0]?.message?.content ?? "",
|
|
190
|
-
input: r.usage?.prompt_tokens ?? 0,
|
|
191
|
-
output: r.usage?.completion_tokens ?? 0,
|
|
192
|
-
model: r.model
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
async function callWithBackoff(provider, request, fetchImpl, signal, logger, nowFn) {
|
|
196
|
-
function exhaustAndThrow(err) {
|
|
197
|
-
markProviderCoolingDown(provider, nowFn ?? Date.now);
|
|
198
|
-
throw err;
|
|
199
|
-
}
|
|
200
|
-
let lastErr;
|
|
201
|
-
for (let attempt = 1; attempt <= PER_PROVIDER_MAX_ATTEMPTS; attempt++) {
|
|
202
|
-
try {
|
|
203
|
-
const response = await fetchImpl(request.url, {
|
|
204
|
-
method: "POST",
|
|
205
|
-
headers: request.headers,
|
|
206
|
-
body: request.body,
|
|
207
|
-
signal
|
|
208
|
-
});
|
|
209
|
-
if (!response.ok) {
|
|
210
|
-
const text = await response.text().catch(() => "");
|
|
211
|
-
const retryable = isRetryableForBackoff(response.status);
|
|
212
|
-
const err = {
|
|
213
|
-
provider,
|
|
214
|
-
status: response.status,
|
|
215
|
-
retryable,
|
|
216
|
-
message: `${provider} ${String(response.status)}: ${text.slice(0, 300)}`
|
|
217
|
-
};
|
|
218
|
-
logger?.warn?.("llm.provider.error", { provider, status: response.status, attempt });
|
|
219
|
-
if (!err.retryable || attempt === PER_PROVIDER_MAX_ATTEMPTS) {
|
|
220
|
-
if (err.retryable) exhaustAndThrow(err);
|
|
221
|
-
throw err;
|
|
222
|
-
}
|
|
223
|
-
lastErr = err;
|
|
224
|
-
} else {
|
|
225
|
-
const gatewayRequestId = response.headers.get("cf-aig-request-id") ?? void 0;
|
|
226
|
-
clearProviderCooldown(provider);
|
|
227
|
-
return { json: await response.json(), gatewayRequestId, attempts: attempt };
|
|
228
|
-
}
|
|
229
|
-
} catch (e) {
|
|
230
|
-
if (e instanceof DOMException && e.name === "AbortError") throw e;
|
|
231
|
-
if (typeof e === "object" && e !== null && "retryable" in e) {
|
|
232
|
-
const err = e;
|
|
233
|
-
if (!err.retryable || attempt === PER_PROVIDER_MAX_ATTEMPTS) {
|
|
234
|
-
if (err.retryable) exhaustAndThrow(err);
|
|
235
|
-
throw err;
|
|
236
|
-
}
|
|
237
|
-
lastErr = err;
|
|
238
|
-
} else {
|
|
239
|
-
const err = {
|
|
240
|
-
provider,
|
|
241
|
-
status: 0,
|
|
242
|
-
retryable: true,
|
|
243
|
-
message: e instanceof Error ? e.message : String(e)
|
|
244
|
-
};
|
|
245
|
-
if (attempt === PER_PROVIDER_MAX_ATTEMPTS) exhaustAndThrow(err);
|
|
246
|
-
lastErr = err;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
const backoffMs = computeBackoffMs(attempt - 1);
|
|
250
|
-
await sleep(backoffMs, signal);
|
|
251
|
-
}
|
|
252
|
-
markProviderCoolingDown(provider, nowFn ?? Date.now);
|
|
253
|
-
throw lastErr ?? { provider, status: 0, retryable: false, message: "exhausted" };
|
|
254
|
-
}
|
|
255
|
-
function isProviderError(err) {
|
|
256
|
-
return typeof err === "object" && err !== null && typeof err.status === "number" && typeof err.message === "string" && typeof err.provider === "string";
|
|
257
|
-
}
|
|
258
|
-
function plan(tier, opts, tokenEstimate) {
|
|
259
|
-
if (opts.model) {
|
|
260
|
-
const m = opts.model;
|
|
261
|
-
if (m.startsWith("claude")) return { primary: { provider: "anthropic", model: m } };
|
|
262
|
-
if (m.startsWith("gemini")) return { primary: { provider: "gemini", model: m } };
|
|
263
|
-
if (m.startsWith("grok")) return { primary: { provider: "grok", model: m } };
|
|
264
|
-
return { primary: { provider: "groq", model: m } };
|
|
265
|
-
}
|
|
266
|
-
const longContext = tokenEstimate >= (opts.longContextThreshold ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
|
|
267
|
-
switch (tier) {
|
|
268
|
-
case "verifier":
|
|
269
|
-
return { primary: { provider: "groq", model: MODELS.groq.verifier } };
|
|
270
|
-
case "smart":
|
|
271
|
-
return longContext ? {
|
|
272
|
-
primary: { provider: "gemini", model: MODELS.gemini.smart },
|
|
273
|
-
fallback: { provider: "anthropic", model: MODELS.anthropic.smart }
|
|
274
|
-
} : {
|
|
275
|
-
primary: { provider: "anthropic", model: MODELS.anthropic.smart },
|
|
276
|
-
fallback: { provider: "gemini", model: MODELS.gemini.smart }
|
|
277
|
-
};
|
|
278
|
-
case "fast":
|
|
279
|
-
return { primary: { provider: "anthropic", model: MODELS.anthropic.fast } };
|
|
280
|
-
case "balanced":
|
|
281
|
-
default:
|
|
282
|
-
return longContext ? {
|
|
283
|
-
primary: { provider: "gemini", model: MODELS.gemini.smart },
|
|
284
|
-
fallback: { provider: "anthropic", model: MODELS.anthropic.balanced }
|
|
285
|
-
} : {
|
|
286
|
-
primary: { provider: "anthropic", model: MODELS.anthropic.balanced },
|
|
287
|
-
fallback: { provider: "gemini", model: MODELS.gemini.smart }
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
async function callOne(leg, messages, opts, env, fetchImpl, logger, nowFn) {
|
|
292
|
-
let req;
|
|
293
|
-
switch (leg.provider) {
|
|
294
|
-
case "anthropic":
|
|
295
|
-
req = buildAnthropicRequest(leg.model, messages, opts, env);
|
|
296
|
-
break;
|
|
297
|
-
case "gemini":
|
|
298
|
-
req = buildGeminiRequest(leg.model, messages, opts, env);
|
|
299
|
-
break;
|
|
300
|
-
case "groq":
|
|
301
|
-
req = buildGroqRequest(leg.model, messages, opts, env);
|
|
302
|
-
break;
|
|
303
|
-
case "grok":
|
|
304
|
-
req = buildGrokRequest(leg.model, messages, opts, env);
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
const { json, gatewayRequestId, attempts } = await callWithBackoff(
|
|
308
|
-
leg.provider,
|
|
309
|
-
req,
|
|
310
|
-
fetchImpl,
|
|
311
|
-
opts.signal,
|
|
312
|
-
logger,
|
|
313
|
-
nowFn
|
|
314
|
-
);
|
|
315
|
-
switch (leg.provider) {
|
|
316
|
-
case "anthropic":
|
|
317
|
-
return { parsed: parseAnthropic(json), gatewayRequestId, attempts };
|
|
318
|
-
case "gemini":
|
|
319
|
-
return { parsed: parseGemini(json), gatewayRequestId, attempts };
|
|
320
|
-
case "groq":
|
|
321
|
-
return { parsed: parseGroq(json), gatewayRequestId, attempts };
|
|
322
|
-
case "grok":
|
|
323
|
-
return { parsed: parseGroq(json), gatewayRequestId, attempts };
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
async function complete(messages, env, opts = {}, deps = {}) {
|
|
327
|
-
if (messages.length === 0) {
|
|
328
|
-
throw new ValidationError("messages must not be empty");
|
|
329
|
-
}
|
|
330
|
-
if (!env.AI_GATEWAY_BASE_URL) {
|
|
331
|
-
throw new ValidationError("AI_GATEWAY_BASE_URL is required in 0.3.0");
|
|
332
|
-
}
|
|
333
|
-
const fetchImpl = deps.fetch ?? fetch;
|
|
334
|
-
const now = deps.now ?? (() => Date.now());
|
|
335
|
-
const logger = deps.logger;
|
|
336
|
-
const startedAt = now();
|
|
337
|
-
const tier = opts.tier ?? "balanced";
|
|
338
|
-
const system = opts.system ?? messages.find((m) => m.role === "system")?.content;
|
|
339
|
-
const tokenEstimate = estimateTokens(messages, system);
|
|
340
|
-
const route = plan(tier, opts, tokenEstimate);
|
|
341
|
-
const attemptLog = [];
|
|
342
|
-
for (const leg of [route.primary, route.fallback].filter(Boolean)) {
|
|
343
|
-
if (isProviderCoolingDown(leg.provider, now)) {
|
|
344
|
-
logger?.warn?.("llm.provider.coolingDown", { provider: leg.provider });
|
|
345
|
-
attemptLog.push({ provider: leg.provider, message: "skipped: cooling down" });
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
|
-
const result = await callOne(leg, messages, opts, env, fetchImpl, logger, now);
|
|
350
|
-
if (!result.parsed.content) {
|
|
351
|
-
throw { provider: leg.provider, status: 200, retryable: false, message: "empty content" };
|
|
352
|
-
}
|
|
353
|
-
logger?.info?.("llm.complete", {
|
|
354
|
-
provider: leg.provider,
|
|
355
|
-
model: leg.model,
|
|
356
|
-
tier,
|
|
357
|
-
tokenEstimate,
|
|
358
|
-
attempts: result.attempts,
|
|
359
|
-
runId: opts.runId,
|
|
360
|
-
project: opts.project,
|
|
361
|
-
actor: opts.actor
|
|
362
|
-
});
|
|
363
|
-
return {
|
|
364
|
-
data: {
|
|
365
|
-
content: result.parsed.content,
|
|
366
|
-
provider: leg.provider,
|
|
367
|
-
model: result.parsed.model ?? leg.model,
|
|
368
|
-
tier,
|
|
369
|
-
tokens: {
|
|
370
|
-
input: result.parsed.input,
|
|
371
|
-
output: result.parsed.output,
|
|
372
|
-
cacheRead: result.parsed.cacheRead,
|
|
373
|
-
cacheWrite: result.parsed.cacheWrite
|
|
374
|
-
},
|
|
375
|
-
latency: now() - startedAt,
|
|
376
|
-
attempts: result.attempts,
|
|
377
|
-
gatewayRequestId: result.gatewayRequestId
|
|
378
|
-
},
|
|
379
|
-
error: null
|
|
380
|
-
};
|
|
381
|
-
} catch (e) {
|
|
382
|
-
if (e instanceof DOMException && e.name === "AbortError") {
|
|
383
|
-
return toErrorResponse(
|
|
384
|
-
new InternalError("llm call aborted", { provider: leg.provider, model: leg.model })
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
if (isProviderError(e)) {
|
|
388
|
-
attemptLog.push({ provider: e.provider, status: e.status, message: e.message });
|
|
389
|
-
if (e.status === 429 && !route.fallback) {
|
|
390
|
-
return toErrorResponse(
|
|
391
|
-
new RateLimitError(`llm rate limited on ${e.provider}`, { attempts: attemptLog })
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
logger?.warn?.("llm.leg.failed", { provider: leg.provider, status: e.status });
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
attemptLog.push({ provider: leg.provider, message: e instanceof Error ? e.message : String(e) });
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return toErrorResponse(
|
|
401
|
-
new InternalError("LLM_ALL_PROVIDERS_FAILED", { attempts: attemptLog, tier, tokenEstimate })
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
async function* completionStream(messages, env, opts = {}) {
|
|
405
|
-
if (messages.length === 0) {
|
|
406
|
-
throw new ValidationError("messages must not be empty");
|
|
407
|
-
}
|
|
408
|
-
if (!env.AI_GATEWAY_BASE_URL) {
|
|
409
|
-
throw new ValidationError("AI_GATEWAY_BASE_URL is required in 0.3.0");
|
|
410
|
-
}
|
|
411
|
-
const deps = opts.deps ?? {};
|
|
412
|
-
const fetchImpl = deps.fetch ?? fetch;
|
|
413
|
-
const now = deps.now ?? (() => Date.now());
|
|
414
|
-
const logger = deps.logger;
|
|
415
|
-
const startedAt = now();
|
|
416
|
-
const tier = opts.tier ?? "balanced";
|
|
417
|
-
const system = opts.system ?? messages.find((m) => m.role === "system")?.content;
|
|
418
|
-
const tokenEstimate = estimateTokens(messages, system);
|
|
419
|
-
const route = plan(tier, opts, tokenEstimate);
|
|
420
|
-
if (route.primary.provider !== "anthropic") {
|
|
421
|
-
const result = await complete(messages, env, opts, deps);
|
|
422
|
-
if (result.error !== null || result.data === null) {
|
|
423
|
-
throw new InternalError("LLM_ALL_PROVIDERS_FAILED", { error: result.error });
|
|
424
|
-
}
|
|
425
|
-
yield result.data.content;
|
|
426
|
-
return result.data;
|
|
427
|
-
}
|
|
428
|
-
if (isProviderCoolingDown(route.primary.provider, now)) {
|
|
429
|
-
logger?.warn?.("llm.provider.coolingDown", { provider: route.primary.provider });
|
|
430
|
-
const result = await complete(messages, env, opts, deps);
|
|
431
|
-
if (result.error !== null || result.data === null) {
|
|
432
|
-
throw new InternalError("LLM_ALL_PROVIDERS_FAILED", { error: result.error });
|
|
433
|
-
}
|
|
434
|
-
yield result.data.content;
|
|
435
|
-
return result.data;
|
|
436
|
-
}
|
|
437
|
-
const req = buildAnthropicRequest(route.primary.model, messages, opts, env, true);
|
|
438
|
-
let response;
|
|
439
|
-
try {
|
|
440
|
-
response = await fetchImpl(req.url, {
|
|
441
|
-
method: "POST",
|
|
442
|
-
headers: req.headers,
|
|
443
|
-
body: req.body,
|
|
444
|
-
// Fall back to a 60 s default when the caller provides no signal — prevents
|
|
445
|
-
// a hung provider connection from consuming the Worker's wall-clock budget.
|
|
446
|
-
signal: opts.signal ?? AbortSignal.timeout(6e4)
|
|
447
|
-
});
|
|
448
|
-
} catch (e) {
|
|
449
|
-
if (e instanceof DOMException && e.name === "AbortError") {
|
|
450
|
-
throw new InternalError("llm call aborted", {
|
|
451
|
-
provider: route.primary.provider,
|
|
452
|
-
model: route.primary.model
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
throw new InternalError("llm stream fetch failed", {
|
|
456
|
-
message: e instanceof Error ? e.message : String(e)
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
if (!response.ok) {
|
|
460
|
-
const text = await response.text().catch(() => "");
|
|
461
|
-
const retryable = isRetryableForBackoff(response.status);
|
|
462
|
-
if (retryable && response.status === 429) {
|
|
463
|
-
markProviderCoolingDown(route.primary.provider, now);
|
|
464
|
-
}
|
|
465
|
-
const result = await complete(messages, env, opts, deps);
|
|
466
|
-
if (result.error !== null || result.data === null) {
|
|
467
|
-
throw new InternalError("LLM_ALL_PROVIDERS_FAILED", {
|
|
468
|
-
streamError: `${route.primary.provider} ${String(response.status)}: ${text.slice(0, 300)}`,
|
|
469
|
-
error: result.error
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
yield result.data.content;
|
|
473
|
-
return result.data;
|
|
474
|
-
}
|
|
475
|
-
if (!response.body) {
|
|
476
|
-
throw new InternalError("llm stream response body is null", {
|
|
477
|
-
provider: route.primary.provider
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
const decoder = new TextDecoder();
|
|
481
|
-
let accumulatedText = "";
|
|
482
|
-
let inputTokens = 0;
|
|
483
|
-
let outputTokens = 0;
|
|
484
|
-
let cacheRead = 0;
|
|
485
|
-
let cacheWrite = 0;
|
|
486
|
-
let modelName;
|
|
487
|
-
const gatewayRequestId = response.headers.get("cf-aig-request-id") ?? void 0;
|
|
488
|
-
const reader = response.body.getReader();
|
|
489
|
-
let buffer = "";
|
|
490
|
-
try {
|
|
491
|
-
while (true) {
|
|
492
|
-
const { done, value } = await reader.read();
|
|
493
|
-
if (done) break;
|
|
494
|
-
buffer += decoder.decode(value, { stream: true });
|
|
495
|
-
const lines = buffer.split("\n");
|
|
496
|
-
buffer = lines.pop() ?? "";
|
|
497
|
-
for (const line of lines) {
|
|
498
|
-
if (!line.startsWith("data: ")) continue;
|
|
499
|
-
const data = line.slice(6).trim();
|
|
500
|
-
if (data === "[DONE]") break;
|
|
501
|
-
let event;
|
|
502
|
-
try {
|
|
503
|
-
event = JSON.parse(data);
|
|
504
|
-
} catch {
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
switch (event.type) {
|
|
508
|
-
case "message_start":
|
|
509
|
-
inputTokens = event.message?.usage?.input_tokens ?? 0;
|
|
510
|
-
cacheRead = event.message?.usage?.cache_read_input_tokens ?? 0;
|
|
511
|
-
cacheWrite = event.message?.usage?.cache_creation_input_tokens ?? 0;
|
|
512
|
-
modelName = event.message?.model;
|
|
513
|
-
break;
|
|
514
|
-
case "content_block_delta":
|
|
515
|
-
if (event.delta?.type === "text_delta" && typeof event.delta.text === "string") {
|
|
516
|
-
accumulatedText += event.delta.text;
|
|
517
|
-
yield event.delta.text;
|
|
518
|
-
}
|
|
519
|
-
break;
|
|
520
|
-
case "message_delta":
|
|
521
|
-
outputTokens = event.usage?.output_tokens ?? outputTokens;
|
|
522
|
-
break;
|
|
523
|
-
default:
|
|
524
|
-
break;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
} finally {
|
|
529
|
-
reader.releaseLock();
|
|
530
|
-
}
|
|
531
|
-
clearProviderCooldown(route.primary.provider);
|
|
532
|
-
logger?.info?.("llm.completionStream", {
|
|
533
|
-
provider: route.primary.provider,
|
|
534
|
-
model: route.primary.model,
|
|
535
|
-
tier,
|
|
536
|
-
tokenEstimate,
|
|
537
|
-
runId: opts.runId,
|
|
538
|
-
project: opts.project,
|
|
539
|
-
actor: opts.actor
|
|
540
|
-
});
|
|
541
|
-
return {
|
|
542
|
-
content: accumulatedText,
|
|
543
|
-
provider: route.primary.provider,
|
|
544
|
-
model: modelName ?? route.primary.model,
|
|
545
|
-
tier,
|
|
546
|
-
tokens: { input: inputTokens, output: outputTokens, cacheRead, cacheWrite },
|
|
547
|
-
latency: now() - startedAt,
|
|
548
|
-
attempts: 1,
|
|
549
|
-
gatewayRequestId
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
function assertGrounding(response, sources) {
|
|
553
|
-
if (sources.length === 0) return true;
|
|
554
|
-
const WINDOW = 5;
|
|
555
|
-
const responseTokens = response.split(/\s+/).filter((t) => t.length > 0);
|
|
556
|
-
if (responseTokens.length < WINDOW) return false;
|
|
557
|
-
const sourceNgrams = /* @__PURE__ */ new Set();
|
|
558
|
-
for (const source of sources) {
|
|
559
|
-
const tokens = source.split(/\s+/).filter((t) => t.length > 0);
|
|
560
|
-
for (let i = 0; i <= tokens.length - WINDOW; i++) {
|
|
561
|
-
const ngram = tokens.slice(i, i + WINDOW).join(" ");
|
|
562
|
-
sourceNgrams.add(ngram);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
if (sourceNgrams.size === 0) return false;
|
|
566
|
-
for (let i = 0; i <= responseTokens.length - WINDOW; i++) {
|
|
567
|
-
const ngram = responseTokens.slice(i, i + WINDOW).join(" ");
|
|
568
|
-
if (sourceNgrams.has(ngram)) return true;
|
|
569
|
-
}
|
|
570
|
-
return false;
|
|
571
|
-
}
|
|
572
|
-
export {
|
|
573
|
-
BASE_BACKOFF_MS,
|
|
574
|
-
MODELS,
|
|
575
|
-
PROVIDER_COOLDOWN_MS,
|
|
576
|
-
assertGrounding,
|
|
577
|
-
clearProviderCooldown,
|
|
578
|
-
complete,
|
|
579
|
-
completionStream,
|
|
580
|
-
isProviderCoolingDown,
|
|
581
|
-
markProviderCoolingDown
|
|
582
|
-
};
|
|
583
|
-
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n InternalError,\n RateLimitError,\n ValidationError,\n toErrorResponse,\n type FactoryResponse,\n} from '@latimer-woods-tech/errors';\nimport type { Logger } from '@latimer-woods-tech/logger';\n\n/**\n * Single chat message exchanged with an LLM provider.\n */\nexport interface LLMMessage {\n role: 'user' | 'assistant' | 'system';\n content: string;\n}\n\n/**\n * Quality tier selected by the caller. Routing is workload-split:\n * - `fast` → Anthropic Haiku (short, latency-sensitive)\n * - `balanced` → Anthropic Sonnet (default)\n * - `smart` → Anthropic Opus OR Gemini 2.5 Pro if input is long-context (>150k tokens estimated)\n * - `verifier` → Groq Llama (cheap second opinion; only used from verifier code path)\n */\nexport type LLMTier = 'fast' | 'balanced' | 'smart' | 'verifier';\n\n/**\n * Options that influence LLM completion behaviour.\n */\nexport interface LLMOptions {\n /** Quality tier; see {@link LLMTier}. Defaults to `balanced`. */\n tier?: LLMTier;\n /** Explicit model override. Takes precedence over tier. */\n model?: string;\n maxTokens?: number;\n temperature?: number;\n system?: string;\n /** Token budget above which we force long-context routing (Gemini). */\n longContextThreshold?: number;\n /** Per-call cancellation signal. Aborts the in-flight provider request. */\n signal?: AbortSignal;\n /** Optional run identifier stamped on ledger rows + logs. */\n runId?: string;\n /** Optional project identifier stamped on ledger rows + logs. */\n project?: string;\n /** Optional actor identifier (supervisor / worker / human). */\n actor?: string;\n /** Anthropic prompt-cache control. Defaults to `true` for `system` prompts ≥ 1024 tokens. */\n promptCache?: boolean;\n}\n\n/**\n * Provider that produced an LLM response. `grok` removed in 0.3.0.\n */\nexport type LLMProvider = 'anthropic' | 'gemini' | 'groq' | 'grok';\n\n/**\n * Result returned by a successful completion.\n */\nexport interface LLMResult {\n content: string;\n provider: LLMProvider;\n model: string;\n tier: LLMTier;\n tokens: { input: number; output: number; cacheRead?: number; cacheWrite?: number };\n latency: number;\n /** Number of attempts before success (1 = primary succeeded). */\n attempts: number;\n /** Monotonic request id from AI Gateway, if present in headers. */\n gatewayRequestId?: string;\n}\n\n/**\n * Environment bindings required by {@link complete}.\n *\n * `AI_GATEWAY_BASE_URL` is REQUIRED in 0.3.0. All provider calls flow through the\n * Cloudflare AI Gateway for unified logging, rate limiting, and cost telemetry.\n * In test/dev the caller may pass a custom fetch impl that short-circuits this.\n */\nexport interface LLMEnv {\n AI_GATEWAY_BASE_URL: string;\n ANTHROPIC_API_KEY: string;\n GROQ_API_KEY: string;\n /** Optional — only required when caller passes `{ model: 'grok-*' }` override. */\n GROK_API_KEY?: string;\n /**\n * Google Cloud short-lived access token with `aiplatform.endpoints.predict`.\n * Callers mint this via the JWT-bearer flow (service account → token exchange);\n * see `docs/runbooks/rotate-gcp-sa.md`. Token must be valid for ≥ 5 minutes.\n */\n VERTEX_ACCESS_TOKEN: string;\n VERTEX_PROJECT: string;\n VERTEX_LOCATION: string;\n}\n\n/**\n * Optional dependencies for {@link complete}.\n */\nexport interface LLMDeps {\n fetch?: typeof fetch;\n logger?: Logger;\n now?: () => number;\n}\n\n// Model catalogue — keep in sync with docs/architecture/FACTORY_V1.md § LLM substrate.\nconst MODELS = {\n anthropic: {\n fast: 'claude-haiku-4-20250514',\n balanced: 'claude-sonnet-4-6',\n smart: 'claude-opus-4-7',\n },\n gemini: {\n smart: 'gemini-2.5-pro',\n },\n groq: {\n verifier: 'llama-3.3-70b-versatile',\n },\n grok: {\n /** Opt-in only via `{ model: 'grok-*' }`. Not in default tier routing. */\n fast: 'grok-4-fast',\n mini: 'grok-3-mini-latest',\n },\n} as const;\n\nconst DEFAULT_MAX_TOKENS = 1024;\nconst DEFAULT_TEMPERATURE = 0.7;\nconst DEFAULT_LONG_CONTEXT_THRESHOLD = 150_000; // tokens\n\n// ─── Per-provider exponential backoff constants ────────────────────────────\n/** Base delay in ms for the first retry. */\nconst BACKOFF_BASE_MS = 500;\n/** Maximum backoff cap in ms. */\nconst BACKOFF_CAP_MS = 8_000;\n/** Max random jitter added to each backoff delay, in ms. */\nconst BACKOFF_JITTER_MAX_MS = 250;\n/** Maximum number of attempts per provider (1 initial + 2 retries). */\nconst PER_PROVIDER_MAX_ATTEMPTS = 3;\n\n// ─── Per-provider cooldown state (module-level) ────────────────────────────\n/**\n * Tracks when a provider's cooldown period expires.\n * Keyed by {@link LLMProvider}; value is the `Date.now()` epoch ms at which\n * the cooldown expires. Absent key means \"not cooling down\".\n */\nconst providerCooldownUntil: Map<LLMProvider, number> = new Map();\n\n/** Cooldown duration in ms after a provider exhausts all retries. */\nconst PROVIDER_COOLDOWN_MS = 30_000;\n\n/**\n * Returns `true` if the provider is currently in its cooldown window.\n * Uses the injected `now` function (or `Date.now`) for testability.\n */\nfunction isProviderCoolingDown(provider: LLMProvider, now: () => number = Date.now): boolean {\n const until = providerCooldownUntil.get(provider);\n if (until === undefined) return false;\n return now() < until;\n}\n\n/**\n * Marks a provider as cooling down for {@link PROVIDER_COOLDOWN_MS} milliseconds.\n */\nfunction markProviderCoolingDown(provider: LLMProvider, now: () => number = Date.now): void {\n providerCooldownUntil.set(provider, now() + PROVIDER_COOLDOWN_MS);\n}\n\n/**\n * Clears the cooldown state for a provider after a successful call.\n */\nfunction clearProviderCooldown(provider: LLMProvider): void {\n providerCooldownUntil.delete(provider);\n}\n\n// ─── Legacy backoff constant (kept for the existing callWithBackoff signature) ─\nconst BASE_BACKOFF_MS = 250;\n\ninterface ProviderError {\n provider: LLMProvider;\n status: number;\n retryable: boolean;\n message: string;\n}\n\n/**\n * Returns `true` for status codes that should trigger a retry.\n * Only 429 and 5xx (transient server errors) qualify; other 4xx are terminal.\n */\nfunction isRetryableForBackoff(status: number): boolean {\n return status === 429 || (status >= 500 && status < 600);\n}\n\nfunction estimateTokens(messages: LLMMessage[], system?: string): number {\n // Cheap estimator: ~4 chars/token. Good enough for threshold routing.\n let chars = system?.length ?? 0;\n for (const m of messages) chars += m.content.length;\n return Math.ceil(chars / 4);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const t = setTimeout(resolve, ms);\n if (signal) {\n const onAbort = () => {\n clearTimeout(t);\n reject(new DOMException('Aborted', 'AbortError'));\n };\n if (signal.aborted) onAbort();\n else signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n}\n\n/**\n * Computes the exponential backoff delay for a given attempt with jitter.\n *\n * Formula: `Math.min(base * 2^attempt + jitter, cap)`\n * where `jitter` is a random value in `[0, BACKOFF_JITTER_MAX_MS)`.\n *\n * @param attempt - Zero-based attempt index (0 = first retry after initial failure).\n */\nfunction computeBackoffMs(attempt: number): number {\n const jitter = Math.floor(Math.random() * BACKOFF_JITTER_MAX_MS);\n return Math.min(BACKOFF_BASE_MS * Math.pow(2, attempt) + jitter, BACKOFF_CAP_MS);\n}\n\n// ─── Provider request builders ─────────────────────────────────────────────\n\nfunction buildAnthropicRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n streaming = false,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const filtered = messages.filter((m) => m.role !== 'system');\n const body: Record<string, unknown> = {\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: filtered.map((m) => ({ role: m.role, content: m.content })),\n };\n if (streaming) {\n body.stream = true;\n }\n if (sys) {\n const cache = opts.promptCache ?? sys.length >= 4096;\n body.system = cache\n ? [{ type: 'text', text: sys, cache_control: { type: 'ephemeral' } }]\n : sys;\n }\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/anthropic/v1/messages`,\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': env.ANTHROPIC_API_KEY,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n };\n}\n\nfunction buildGeminiRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const contents = messages\n .filter((m) => m.role !== 'system')\n .map((m) => ({\n role: m.role === 'assistant' ? 'model' : 'user',\n parts: [{ text: m.content }],\n }));\n const body: Record<string, unknown> = {\n contents,\n generationConfig: {\n maxOutputTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n },\n };\n if (sys) {\n body.systemInstruction = { parts: [{ text: sys }] };\n }\n const path = `v1/projects/${env.VERTEX_PROJECT}/locations/${env.VERTEX_LOCATION}/publishers/google/models/${model}:generateContent`;\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/google-vertex-ai/${path}`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.VERTEX_ACCESS_TOKEN}`,\n },\n body: JSON.stringify(body),\n };\n}\n\nfunction buildGroqRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const merged: LLMMessage[] = [];\n if (sys) merged.push({ role: 'system', content: sys });\n for (const m of messages) if (m.role !== 'system') merged.push(m);\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/groq/openai/v1/chat/completions`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.GROQ_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: merged,\n }),\n };\n}\n\nfunction buildGrokRequest(\n model: string,\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n): { url: string; headers: Record<string, string>; body: string } {\n if (!env.GROK_API_KEY) {\n throw new ValidationError('GROK_API_KEY required for grok-* model override');\n }\n const sys = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const merged: LLMMessage[] = [];\n if (sys) merged.push({ role: 'system', content: sys });\n for (const m of messages) if (m.role !== 'system') merged.push(m);\n return {\n url: `${env.AI_GATEWAY_BASE_URL}/grok/v1/chat/completions`,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${env.GROK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n max_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,\n temperature: opts.temperature ?? DEFAULT_TEMPERATURE,\n messages: merged,\n }),\n };\n}\n\n// ─── Response parsers ──────────────────────────────────────────────────────\n\ninterface AnthropicResponse {\n content?: Array<{ type: string; text?: string }>;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n model?: string;\n}\n\ninterface GeminiResponse {\n candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>;\n usageMetadata?: {\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n };\n}\n\ninterface GroqResponse {\n choices?: Array<{ message?: { content?: string } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n model?: string;\n}\n\nfunction parseAnthropic(\n json: unknown,\n): { content: string; input: number; output: number; cacheRead: number; cacheWrite: number; model?: string } {\n const r = json as AnthropicResponse;\n return {\n content: r.content?.find((c) => c.type === 'text')?.text ?? '',\n input: r.usage?.input_tokens ?? 0,\n output: r.usage?.output_tokens ?? 0,\n cacheRead: r.usage?.cache_read_input_tokens ?? 0,\n cacheWrite: r.usage?.cache_creation_input_tokens ?? 0,\n model: r.model,\n };\n}\n\nfunction parseGemini(json: unknown): { content: string; input: number; output: number } {\n const r = json as GeminiResponse;\n const text =\n r.candidates?.[0]?.content?.parts?.map((p) => p.text ?? '').join('') ?? '';\n return {\n content: text,\n input: r.usageMetadata?.promptTokenCount ?? 0,\n output: r.usageMetadata?.candidatesTokenCount ?? 0,\n };\n}\n\nfunction parseGroq(json: unknown): { content: string; input: number; output: number; model?: string } {\n const r = json as GroqResponse;\n return {\n content: r.choices?.[0]?.message?.content ?? '',\n input: r.usage?.prompt_tokens ?? 0,\n output: r.usage?.completion_tokens ?? 0,\n model: r.model,\n };\n}\n\n// ─── Core call with backoff ────────────────────────────────────────────────\n\n/**\n * Calls a provider with per-provider exponential backoff.\n *\n * Retries up to {@link PER_PROVIDER_MAX_ATTEMPTS} times on 429 or transient 5xx.\n * Other 4xx codes are treated as terminal and not retried.\n * AbortError is never retried — it bubbles immediately.\n *\n * @param provider - Provider name, used for error tagging.\n * @param request - Pre-built HTTP request descriptor.\n * @param fetchImpl - Fetch implementation (injectable for tests).\n * @param signal - Optional AbortSignal for cancellation.\n * @param logger - Optional logger for per-attempt warnings.\n * @param nowFn - Optional clock injection for testability.\n * @returns Parsed JSON body, optional AI Gateway request ID, and attempt count.\n */\nasync function callWithBackoff(\n provider: LLMProvider,\n request: { url: string; headers: Record<string, string>; body: string },\n fetchImpl: typeof fetch,\n signal: AbortSignal | undefined,\n logger: Logger | undefined,\n nowFn?: () => number,\n): Promise<{ json: unknown; gatewayRequestId?: string; attempts: number }> {\n /**\n * Helper: mark provider cooling down and then throw the error.\n * Called whenever we determine we've exhausted all retries for the provider.\n * AbortError is never counted as a provider exhaustion — it bypasses this.\n */\n function exhaustAndThrow(err: ProviderError): never {\n markProviderCoolingDown(provider, nowFn ?? Date.now);\n throw err;\n }\n\n let lastErr: ProviderError | undefined;\n for (let attempt = 1; attempt <= PER_PROVIDER_MAX_ATTEMPTS; attempt++) {\n try {\n const response = await fetchImpl(request.url, {\n method: 'POST',\n headers: request.headers,\n body: request.body,\n signal,\n });\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n const retryable = isRetryableForBackoff(response.status);\n const err: ProviderError = {\n provider,\n status: response.status,\n retryable,\n message: `${provider} ${String(response.status)}: ${text.slice(0, 300)}`,\n };\n logger?.warn?.('llm.provider.error', { provider, status: response.status, attempt });\n if (!err.retryable || attempt === PER_PROVIDER_MAX_ATTEMPTS) {\n if (err.retryable) exhaustAndThrow(err); // retryable but exhausted\n throw err; // terminal non-retryable error — no cooldown\n }\n lastErr = err;\n } else {\n const gatewayRequestId = response.headers.get('cf-aig-request-id') ?? undefined;\n clearProviderCooldown(provider);\n return { json: await response.json(), gatewayRequestId, attempts: attempt };\n }\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n if (typeof e === 'object' && e !== null && 'retryable' in e) {\n const err = e as ProviderError;\n if (!err.retryable || attempt === PER_PROVIDER_MAX_ATTEMPTS) {\n if (err.retryable) exhaustAndThrow(err); // retryable but exhausted\n throw err; // terminal — no cooldown\n }\n lastErr = err;\n } else {\n const err: ProviderError = {\n provider,\n status: 0,\n retryable: true,\n message: e instanceof Error ? e.message : String(e),\n };\n if (attempt === PER_PROVIDER_MAX_ATTEMPTS) exhaustAndThrow(err);\n lastErr = err;\n }\n }\n // Exponential backoff with jitter: base=500ms, cap=8000ms, jitter up to 250ms\n const backoffMs = computeBackoffMs(attempt - 1);\n await sleep(backoffMs, signal);\n }\n // Fallthrough — should not be reached, but mark cooling down defensively.\n markProviderCoolingDown(provider, nowFn ?? Date.now);\n throw lastErr ?? ({ provider, status: 0, retryable: false, message: 'exhausted' } as ProviderError);\n}\n\nfunction isProviderError(err: unknown): err is ProviderError {\n return (\n typeof err === 'object' &&\n err !== null &&\n typeof (err as { status?: unknown }).status === 'number' &&\n typeof (err as { message?: unknown }).message === 'string' &&\n typeof (err as { provider?: unknown }).provider === 'string'\n );\n}\n\n// ─── Routing ───────────────────────────────────────────────────────────────\n\ninterface RoutePlan {\n primary: { provider: LLMProvider; model: string };\n fallback?: { provider: LLMProvider; model: string };\n}\n\nfunction plan(tier: LLMTier, opts: LLMOptions, tokenEstimate: number): RoutePlan {\n if (opts.model) {\n // Explicit override — best-effort provider detection.\n const m = opts.model;\n if (m.startsWith('claude')) return { primary: { provider: 'anthropic', model: m } };\n if (m.startsWith('gemini')) return { primary: { provider: 'gemini', model: m } };\n if (m.startsWith('grok')) return { primary: { provider: 'grok', model: m } };\n return { primary: { provider: 'groq', model: m } };\n }\n const longContext = tokenEstimate >= (opts.longContextThreshold ?? DEFAULT_LONG_CONTEXT_THRESHOLD);\n switch (tier) {\n case 'verifier':\n return { primary: { provider: 'groq', model: MODELS.groq.verifier } };\n case 'smart':\n return longContext\n ? {\n primary: { provider: 'gemini', model: MODELS.gemini.smart },\n fallback: { provider: 'anthropic', model: MODELS.anthropic.smart },\n }\n : {\n primary: { provider: 'anthropic', model: MODELS.anthropic.smart },\n fallback: { provider: 'gemini', model: MODELS.gemini.smart },\n };\n case 'fast':\n return { primary: { provider: 'anthropic', model: MODELS.anthropic.fast } };\n case 'balanced':\n default:\n return longContext\n ? {\n primary: { provider: 'gemini', model: MODELS.gemini.smart },\n fallback: { provider: 'anthropic', model: MODELS.anthropic.balanced },\n }\n : {\n primary: { provider: 'anthropic', model: MODELS.anthropic.balanced },\n fallback: { provider: 'gemini', model: MODELS.gemini.smart },\n };\n }\n}\n\nasync function callOne(\n leg: { provider: LLMProvider; model: string },\n messages: LLMMessage[],\n opts: LLMOptions,\n env: LLMEnv,\n fetchImpl: typeof fetch,\n logger: Logger | undefined,\n nowFn?: () => number,\n): Promise<{ parsed: { content: string; input: number; output: number; cacheRead?: number; cacheWrite?: number; model?: string }; gatewayRequestId?: string; attempts: number }> {\n let req: { url: string; headers: Record<string, string>; body: string };\n switch (leg.provider) {\n case 'anthropic':\n req = buildAnthropicRequest(leg.model, messages, opts, env);\n break;\n case 'gemini':\n req = buildGeminiRequest(leg.model, messages, opts, env);\n break;\n case 'groq':\n req = buildGroqRequest(leg.model, messages, opts, env);\n break;\n case 'grok':\n req = buildGrokRequest(leg.model, messages, opts, env);\n break;\n }\n const { json, gatewayRequestId, attempts } = await callWithBackoff(\n leg.provider,\n req,\n fetchImpl,\n opts.signal,\n logger,\n nowFn,\n );\n switch (leg.provider) {\n case 'anthropic':\n return { parsed: parseAnthropic(json), gatewayRequestId, attempts };\n case 'gemini':\n return { parsed: parseGemini(json), gatewayRequestId, attempts };\n case 'groq':\n return { parsed: parseGroq(json), gatewayRequestId, attempts };\n case 'grok':\n return { parsed: parseGroq(json), gatewayRequestId, attempts };\n }\n}\n\n/**\n * Run a completion through the routing plan for the requested tier.\n *\n * Routing summary (0.3.0):\n * - `fast` → Anthropic Haiku\n * - `balanced` → Anthropic Sonnet; Gemini 2.5 Pro if `longContextThreshold` exceeded\n * - `smart` → Anthropic Opus; Gemini 2.5 Pro if long-context\n * - `verifier` → Groq Llama 3.3 70B (no fallback — verifier is inherently cheap/best-effort)\n *\n * All provider traffic flows through Cloudflare AI Gateway at `AI_GATEWAY_BASE_URL`.\n *\n * Per-provider reliability guarantees (0.4.0):\n * - Exponential backoff with jitter on 429 / 5xx (base 500ms, cap 8s, up to 2 retries).\n * - Provider cooldown: after exhausting retries the provider is marked cooling down\n * for 30 seconds; subsequent calls skip it and go straight to the fallback leg.\n *\n * @param messages - Ordered chat history.\n * @param env - API key + gateway bindings.\n * @param opts - Optional tier/model/parameters override.\n * @param deps - Optional fetch/logger/clock injection (for testing).\n * @returns A {@link FactoryResponse} carrying either an {@link LLMResult} or\n * an error (`LLM_ALL_PROVIDERS_FAILED`, `LLM_RATE_LIMITED`, or `INTERNAL_ERROR`).\n */\nexport async function complete(\n messages: LLMMessage[],\n env: LLMEnv,\n opts: LLMOptions = {},\n deps: LLMDeps = {},\n): Promise<FactoryResponse<LLMResult>> {\n if (messages.length === 0) {\n throw new ValidationError('messages must not be empty');\n }\n if (!env.AI_GATEWAY_BASE_URL) {\n throw new ValidationError('AI_GATEWAY_BASE_URL is required in 0.3.0');\n }\n const fetchImpl = deps.fetch ?? fetch;\n const now = deps.now ?? (() => Date.now());\n const logger = deps.logger;\n const startedAt = now();\n\n const tier: LLMTier = opts.tier ?? 'balanced';\n const system = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const tokenEstimate = estimateTokens(messages, system);\n const route = plan(tier, opts, tokenEstimate);\n\n const attemptLog: Array<{ provider: LLMProvider; status?: number; message: string }> = [];\n\n for (const leg of [route.primary, route.fallback].filter(Boolean) as Array<{ provider: LLMProvider; model: string }>) {\n // Skip providers that are currently in their cooldown window.\n if (isProviderCoolingDown(leg.provider, now)) {\n logger?.warn?.('llm.provider.coolingDown', { provider: leg.provider });\n attemptLog.push({ provider: leg.provider, message: 'skipped: cooling down' });\n continue;\n }\n try {\n const result = await callOne(leg, messages, opts, env, fetchImpl, logger, now);\n if (!result.parsed.content) {\n throw { provider: leg.provider, status: 200, retryable: false, message: 'empty content' } satisfies ProviderError;\n }\n logger?.info?.('llm.complete', {\n provider: leg.provider,\n model: leg.model,\n tier,\n tokenEstimate,\n attempts: result.attempts,\n runId: opts.runId,\n project: opts.project,\n actor: opts.actor,\n });\n return {\n data: {\n content: result.parsed.content,\n provider: leg.provider,\n model: result.parsed.model ?? leg.model,\n tier,\n tokens: {\n input: result.parsed.input,\n output: result.parsed.output,\n cacheRead: result.parsed.cacheRead,\n cacheWrite: result.parsed.cacheWrite,\n },\n latency: now() - startedAt,\n attempts: result.attempts,\n gatewayRequestId: result.gatewayRequestId,\n },\n error: null,\n };\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') {\n return toErrorResponse(\n new InternalError('llm call aborted', { provider: leg.provider, model: leg.model }),\n );\n }\n if (isProviderError(e)) {\n attemptLog.push({ provider: e.provider, status: e.status, message: e.message });\n if (e.status === 429 && !route.fallback) {\n return toErrorResponse(\n new RateLimitError(`llm rate limited on ${e.provider}`, { attempts: attemptLog }),\n );\n }\n logger?.warn?.('llm.leg.failed', { provider: leg.provider, status: e.status });\n continue;\n }\n attemptLog.push({ provider: leg.provider, message: e instanceof Error ? e.message : String(e) });\n }\n }\n\n return toErrorResponse(\n new InternalError('LLM_ALL_PROVIDERS_FAILED', { attempts: attemptLog, tier, tokenEstimate }),\n );\n}\n\n// ─── Streaming ────────────────────────────────────────────────────────────\n\n/**\n * Anthropic server-sent event shapes used by the streaming parser.\n * Only the fields we consume are typed; the rest are ignored.\n */\ninterface AnthropicStreamEvent {\n type: string;\n index?: number;\n delta?: { type?: string; text?: string };\n message?: {\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n model?: string;\n };\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n };\n}\n\n/**\n * Streams a completion from the primary Anthropic provider, yielding text chunks\n * as they arrive. Falls back to the non-streaming {@link complete} function when\n * the provider does not support streaming (i.e. a non-Anthropic primary is selected).\n *\n * The generator's **return value** (accessible via `gen.return()` or by consuming\n * the full iteration) is an {@link LLMResult} with the same shape as {@link complete}.\n *\n * Usage pattern:\n * ```ts\n * const gen = completionStream(messages, env, opts);\n * for await (const chunk of gen) {\n * // stream chunk to client\n * }\n * const result = (await gen.return(undefined)).value; // LLMResult\n * ```\n *\n * @param messages - Ordered chat history.\n * @param env - API key + gateway bindings.\n * @param opts - Optional tier/model/parameters override. Accepts `deps` as nested field.\n * @returns An async generator that yields `string` chunks and returns an {@link LLMResult}.\n */\nexport async function* completionStream(\n messages: LLMMessage[],\n env: LLMEnv,\n opts: LLMOptions & { deps?: LLMDeps } = {},\n): AsyncGenerator<string, LLMResult, unknown> {\n if (messages.length === 0) {\n throw new ValidationError('messages must not be empty');\n }\n if (!env.AI_GATEWAY_BASE_URL) {\n throw new ValidationError('AI_GATEWAY_BASE_URL is required in 0.3.0');\n }\n\n const deps: LLMDeps = opts.deps ?? {};\n const fetchImpl = deps.fetch ?? fetch;\n const now = deps.now ?? (() => Date.now());\n const logger = deps.logger;\n const startedAt = now();\n\n const tier: LLMTier = opts.tier ?? 'balanced';\n const system = opts.system ?? messages.find((m) => m.role === 'system')?.content;\n const tokenEstimate = estimateTokens(messages, system);\n const route = plan(tier, opts, tokenEstimate);\n\n // Only Anthropic supports streaming in the current implementation.\n // For all other primaries, fall back to non-streaming complete().\n if (route.primary.provider !== 'anthropic') {\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', { error: result.error });\n }\n yield result.data.content;\n return result.data;\n }\n\n // Check cooldown before attempting the streaming call.\n if (isProviderCoolingDown(route.primary.provider, now)) {\n logger?.warn?.('llm.provider.coolingDown', { provider: route.primary.provider });\n // Fall back to non-streaming complete() which will handle the fallback leg.\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', { error: result.error });\n }\n yield result.data.content;\n return result.data;\n }\n\n const req = buildAnthropicRequest(route.primary.model, messages, opts, env, true);\n\n let response: Response;\n try {\n response = await fetchImpl(req.url, {\n method: 'POST',\n headers: req.headers,\n body: req.body,\n // Fall back to a 60 s default when the caller provides no signal — prevents\n // a hung provider connection from consuming the Worker's wall-clock budget.\n signal: opts.signal ?? AbortSignal.timeout(60_000),\n });\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') {\n throw new InternalError('llm call aborted', {\n provider: route.primary.provider,\n model: route.primary.model,\n });\n }\n throw new InternalError('llm stream fetch failed', {\n message: e instanceof Error ? e.message : String(e),\n });\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n const retryable = isRetryableForBackoff(response.status);\n if (retryable && response.status === 429) {\n markProviderCoolingDown(route.primary.provider, now);\n }\n // Fall back to non-streaming complete() which will try the fallback leg.\n const result = await complete(messages, env, opts, deps);\n if (result.error !== null || result.data === null) {\n throw new InternalError('LLM_ALL_PROVIDERS_FAILED', {\n streamError: `${route.primary.provider} ${String(response.status)}: ${text.slice(0, 300)}`,\n error: result.error,\n });\n }\n yield result.data.content;\n return result.data;\n }\n\n if (!response.body) {\n throw new InternalError('llm stream response body is null', {\n provider: route.primary.provider,\n });\n }\n\n // Stream SSE events from Anthropic.\n const decoder = new TextDecoder();\n let accumulatedText = '';\n let inputTokens = 0;\n let outputTokens = 0;\n let cacheRead = 0;\n let cacheWrite = 0;\n let modelName: string | undefined;\n const gatewayRequestId: string | undefined = response.headers.get('cf-aig-request-id') ?? undefined;\n\n const reader = response.body.getReader();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // SSE lines are delimited by '\\n'. Events are separated by '\\n\\n'.\n const lines = buffer.split('\\n');\n // Keep the last (potentially incomplete) line in the buffer.\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const data = line.slice(6).trim();\n if (data === '[DONE]') break;\n let event: AnthropicStreamEvent;\n try {\n event = JSON.parse(data) as AnthropicStreamEvent;\n } catch {\n continue; // Skip malformed SSE lines.\n }\n\n switch (event.type) {\n case 'message_start':\n inputTokens = event.message?.usage?.input_tokens ?? 0;\n cacheRead = event.message?.usage?.cache_read_input_tokens ?? 0;\n cacheWrite = event.message?.usage?.cache_creation_input_tokens ?? 0;\n modelName = event.message?.model;\n break;\n case 'content_block_delta':\n if (event.delta?.type === 'text_delta' && typeof event.delta.text === 'string') {\n accumulatedText += event.delta.text;\n yield event.delta.text;\n }\n break;\n case 'message_delta':\n outputTokens = event.usage?.output_tokens ?? outputTokens;\n break;\n default:\n break;\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n clearProviderCooldown(route.primary.provider);\n logger?.info?.('llm.completionStream', {\n provider: route.primary.provider,\n model: route.primary.model,\n tier,\n tokenEstimate,\n runId: opts.runId,\n project: opts.project,\n actor: opts.actor,\n });\n\n return {\n content: accumulatedText,\n provider: route.primary.provider,\n model: modelName ?? route.primary.model,\n tier,\n tokens: { input: inputTokens, output: outputTokens, cacheRead, cacheWrite },\n latency: now() - startedAt,\n attempts: 1,\n gatewayRequestId,\n };\n}\n\n// ─── Grounding assertion ───────────────────────────────────────────────────\n\n/**\n * Returns `true` if `response` contains at least one verbatim phrase of at\n * least 5 consecutive whitespace-delimited tokens that also appears in one of\n * the `sources` strings.\n *\n * Returns `true` unconditionally when `sources` is empty (no grounding\n * documents means grounding cannot be violated).\n *\n * This is a lightweight guard for RAG pipelines — it detects obvious\n * hallucinations where the model generates content not present in any\n * retrieved source. It is NOT a semantic similarity check.\n *\n * @param response - The LLM-generated text to inspect.\n * @param sources - Retrieved source documents to check against.\n * @returns `true` if the response is grounded, `false` if hallucination detected.\n *\n * @example\n * ```ts\n * const grounded = assertGrounding(llmAnswer, retrievedDocs);\n * if (!grounded) {\n * // flag or re-rank the response\n * }\n * ```\n */\nexport function assertGrounding(response: string, sources: string[]): boolean {\n if (sources.length === 0) return true;\n\n const WINDOW = 5;\n const responseTokens = response.split(/\\s+/).filter((t) => t.length > 0);\n\n if (responseTokens.length < WINDOW) return false;\n\n // Build a set of all 5-token ngrams from each source for O(n) lookup.\n const sourceNgrams = new Set<string>();\n for (const source of sources) {\n const tokens = source.split(/\\s+/).filter((t) => t.length > 0);\n for (let i = 0; i <= tokens.length - WINDOW; i++) {\n const ngram = tokens.slice(i, i + WINDOW).join(' ');\n sourceNgrams.add(ngram);\n }\n }\n\n if (sourceNgrams.size === 0) return false;\n\n // Slide a window of WINDOW tokens over the response and check for a match.\n for (let i = 0; i <= responseTokens.length - WINDOW; i++) {\n const ngram = responseTokens.slice(i, i + WINDOW).join(' ');\n if (sourceNgrams.has(ngram)) return true;\n }\n\n return false;\n}\n\n// ─── Exported helpers (kept for existing consumers) ───────────────────────\n\nexport { MODELS, isProviderCoolingDown, markProviderCoolingDown, clearProviderCooldown, PROVIDER_COOLDOWN_MS };\nexport { BASE_BACKOFF_MS };\n"],"mappings":"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","names":[]}
|