llm-relay 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -13
- package/config.example.json +10 -3
- package/dist/anthropic.d.ts +21 -1
- package/dist/anthropic.js.map +1 -1
- package/dist/authEnv.d.ts +69 -0
- package/dist/authEnv.js +66 -1
- package/dist/authEnv.js.map +1 -1
- package/dist/backend.d.ts +14 -0
- package/dist/backend.js +30 -10
- package/dist/backend.js.map +1 -1
- package/dist/benchmarks.d.ts +24 -2
- package/dist/benchmarks.js +45 -14
- package/dist/benchmarks.js.map +1 -1
- package/dist/candidates.d.ts +6 -2
- package/dist/candidates.js +5 -2
- package/dist/candidates.js.map +1 -1
- package/dist/catalog.js +53 -7
- package/dist/catalog.js.map +1 -1
- package/dist/circuit-breaker.d.ts +65 -6
- package/dist/circuit-breaker.js +107 -29
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/cli.d.ts +79 -0
- package/dist/cli.js +243 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +31 -0
- package/dist/config.js +131 -12
- package/dist/config.js.map +1 -1
- package/dist/dispatch.d.ts +13 -2
- package/dist/dispatch.js +115 -15
- package/dist/dispatch.js.map +1 -1
- package/dist/documents.js +47 -4
- package/dist/documents.js.map +1 -1
- package/dist/dotenv.d.ts +12 -0
- package/dist/dotenv.js +68 -0
- package/dist/dotenv.js.map +1 -0
- package/dist/emitSse.d.ts +4 -0
- package/dist/emitSse.js +62 -14
- package/dist/emitSse.js.map +1 -1
- package/dist/key-checker.d.ts +25 -3
- package/dist/key-checker.js +256 -38
- package/dist/key-checker.js.map +1 -1
- package/dist/log.d.ts +33 -0
- package/dist/log.js +54 -8
- package/dist/log.js.map +1 -1
- package/dist/onboarding.js +5 -1
- package/dist/onboarding.js.map +1 -1
- package/dist/ping/cadence.js +9 -2
- package/dist/ping/cadence.js.map +1 -1
- package/dist/ping/metrics.js +10 -0
- package/dist/ping/metrics.js.map +1 -1
- package/dist/ping/ping.js +10 -9
- package/dist/ping/ping.js.map +1 -1
- package/dist/ping/probe-cache.js +6 -1
- package/dist/ping/probe-cache.js.map +1 -1
- package/dist/pool-health.d.ts +36 -0
- package/dist/pool-health.js +112 -0
- package/dist/pool-health.js.map +1 -0
- package/dist/registry.js +5 -1
- package/dist/registry.js.map +1 -1
- package/dist/repair.d.ts +57 -1
- package/dist/repair.js +123 -5
- package/dist/repair.js.map +1 -1
- package/dist/reshaper.d.ts +29 -1
- package/dist/reshaper.js +40 -11
- package/dist/reshaper.js.map +1 -1
- package/dist/self-update.d.ts +75 -2
- package/dist/self-update.js +188 -28
- package/dist/self-update.js.map +1 -1
- package/dist/server.d.ts +32 -1
- package/dist/server.js +321 -45
- package/dist/server.js.map +1 -1
- package/dist/setup-claude.d.ts +23 -1
- package/dist/setup-claude.js +24 -12
- package/dist/setup-claude.js.map +1 -1
- package/dist/sse.js +45 -2
- package/dist/sse.js.map +1 -1
- package/dist/telemetry.d.ts +28 -2
- package/dist/telemetry.js +58 -10
- package/dist/telemetry.js.map +1 -1
- package/dist/tier-data.js +10 -1
- package/dist/tier-data.js.map +1 -1
- package/package.json +3 -2
- package/scripts/install-skill.mjs +31 -16
- package/skills/llm-relay/SKILL.md +57 -7
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A standalone, **loopback** Anthropic-Messages-API reverse proxy. It forwards `/v
|
|
|
8
8
|
|
|
9
9
|
- **Transparent passthrough** — forwards streaming and non-streaming `/v1/messages` byte-for-byte.
|
|
10
10
|
- **`detect` mode** — deterministic tool_use validation (Ajv2020) with metadata-only logging of pass/fail/uncheckable. Behavior is unchanged; it only observes.
|
|
11
|
-
- **`repair` mode** — on a validation failure, a cheap reshaper model corrects the call, the result is **re-validated**, and the corrected response is re-emitted (JSON or freshly-serialized SSE). Destructive-tool calls are **refused, never fabricated**; unrepairable calls **fail-clean** (502). Valid calls pass through untouched.
|
|
11
|
+
- **`repair` mode** — on a validation failure, a cheap reshaper model corrects the call, the result is **re-validated**, and the corrected response is re-emitted (JSON or freshly-serialized SSE). Destructive-tool calls are **refused, never fabricated**; unrepairable calls **fail-clean** (502). Valid calls pass through untouched. The refusal matches the tool **name exactly** (case-insensitively) — see [Destructive-tool refusal](#destructive-tool-refusal-repairdestructivetools) for which tools that now covers.
|
|
12
12
|
- **OpenAI-compatible backends** (`backend.kind:"openai"`) — front NIM / vLLM / OpenRouter / LM Studio. Requests are translated Anthropic→OpenAI and responses back (streaming SSE + non-streaming) via [`llm-bridge`](https://github.com/supermemoryai/llm-bridge) (zero-dep). The validate/repair layer always sees Anthropic Messages, regardless of backend. Verified live end-to-end.
|
|
13
13
|
- **Streaming repair** — text-block SSE frames stream to the client **as they arrive**; the proxy only withholds from the first `tool_use` block. A pure-text response is byte-for-byte passthrough with zero added latency; a valid tool call flushes the withheld frames verbatim; an invalid one is repaired with only the corrected trailing blocks re-emitted (`message_start` + leading text already delivered). A mid-stream repair failure surfaces as an SSE `error` event, never a fabricated call. Handles LF and CRLF frame delimiters and multibyte UTF-8 across chunk boundaries.
|
|
14
14
|
|
|
@@ -66,8 +66,9 @@ llm-relay
|
|
|
66
66
|
- **Subagent offload** (`routing.offload` + `routing.subagents`): route Claude Code *subagents* to other providers while the human's own conversation stays on passthrough — with no agent files and no model ids in the prompt. **Off by default**; `llm-relay offload on` flips it without a restart, `llm-relay candidates` shows what to point it at. See below.
|
|
67
67
|
|
|
68
68
|
### 3. Prompt Token & Context Length Guardrails
|
|
69
|
-
-
|
|
70
|
-
- Rejects oversized
|
|
69
|
+
- Estimates the request's prompt token count (`estimateRequestTokens`) against the target model's context limit, read from the warm catalog cache (`cachedLimits()` — it never fetches, so a cold cache costs no round-trip on the request path).
|
|
70
|
+
- Rejects an oversized request before network transmission with an HTTP 400 (`request prompt estimated tokens … exceeds the context limit …`), protecting backends from context-window overflow.
|
|
71
|
+
- ⚠ **It only fires against a limit the *serving* provider published.** If that provider publishes no limit (NIM publishes none), there is no guardrail: the request goes upstream and the backend returns its own authoritative error. llm-relay will not reject a request against a number it guessed — see the per-(provider, model) note under [Choosing where to offload](#choosing-where-to-offload-llm-relay-candidates).
|
|
71
72
|
|
|
72
73
|
### 4. Background Adaptive Health Monitoring & Persistent Caching
|
|
73
74
|
- **Adaptive Cadence Loop**: Background `PingLoop` dynamically adjusts probe frequency across 4 operational modes: `speed` (2s interval at startup/activity), `normal` (10s), `slow` (30s after 5m idle), and `forced` (4s).
|
|
@@ -86,7 +87,8 @@ llm-relay
|
|
|
86
87
|
the prompt.
|
|
87
88
|
|
|
88
89
|
### 6. Programmatic Telemetry & Quota Access for Claude
|
|
89
|
-
- **HTTP
|
|
90
|
+
- **Read-only HTTP endpoints**: `GET /telemetry` (live JSON metrics), `GET /registry` (full provider/routing/model catalog with quality scores), `GET /ping` (trigger health probe pass & mode summary), `GET /health` (diagnostic status), `GET /candidates` (the un-blended offload decision table).
|
|
91
|
+
- **Mutating HTTP endpoints**: `GET|POST /offload` (read/flip the subagent-offload switch), `GET|POST /dispatch` (the dispatch ladder). ⚠ **Loopback is not authorization** — any page you visit can POST cross-origin to a loopback listener without a preflight, and these two write your `config.json` and steer lane order. They therefore reject a present-but-non-loopback `Origin` with 403, require `content-type: application/json` on a mutating request, and require a loopback `Host` (closing DNS rebinding). An **absent** `Origin` is allowed on purpose — that is what a CLI sends, and it is what keeps `llm-relay offload on` working against a running proxy with no restart.
|
|
90
92
|
- **CLI Commands**: `llm-relay telemetry` outputs live telemetry metrics; `llm-relay models` lists live model catalogs with SWE-bench & quality scores; `llm-relay ping` performs live health & latency probes.
|
|
91
93
|
- **Response Headers**: Proxy responses include `x-llm-relay-quota-percent`, `x-llm-relay-stability-score`, and `x-llm-relay-target`.
|
|
92
94
|
|
|
@@ -100,10 +102,14 @@ llm-relay
|
|
|
100
102
|
| `llm-relay onboard` | Run guided setup wizard for 100%-free providers & subscription keys |
|
|
101
103
|
| `llm-relay setup claude-desktop` (or `desktop`) | Auto-patch `claude_desktop_config.json` for Claude Desktop |
|
|
102
104
|
| `llm-relay setup claude-cli` | Display & verify Claude CLI wrapper configuration |
|
|
103
|
-
| `llm-relay keys` (or `check-keys`) | Validate provider API
|
|
105
|
+
| `llm-relay keys` (or `check-keys`) | Validate provider API **credentials** (escalates past a public `/models` to an authenticated probe) |
|
|
106
|
+
| `llm-relay pools [--probe]` | List pool members; `--probe` sends a real completion to each — the only check that catches a listed-but-dead **model** |
|
|
104
107
|
| `llm-relay telemetry` | Output live JSON telemetry, stability scores, and quota metrics |
|
|
105
108
|
| `llm-relay models [-p <name>] [-r]` | Query live `/models` catalog per provider (`-p` filter, `-r` force refresh) |
|
|
106
109
|
| `llm-relay ping [-p <name>]` | Perform live health, latency & quota probe across providers |
|
|
110
|
+
| `llm-relay offload [on\|off\|status]` | Read or flip the subagent-offload switch — applies to the next request, no restart |
|
|
111
|
+
| `llm-relay candidates [-p <name>]` | The un-blended offload decision table (capability, cost, live health, quota, breaker state) |
|
|
112
|
+
| `llm-relay dispatch [lane] [-t <task>]` | Which lane to hand a whole delegated task to next; it returns the command, **you** run it (`-x <lane>` reports one spent) |
|
|
107
113
|
|
|
108
114
|
---
|
|
109
115
|
|
|
@@ -123,8 +129,34 @@ llm-relay
|
|
|
123
129
|
|
|
124
130
|
# Check key status & signup URLs:
|
|
125
131
|
llm-relay keys
|
|
132
|
+
|
|
133
|
+
# Check that every model in your pools actually answers:
|
|
134
|
+
llm-relay pools --probe
|
|
126
135
|
```
|
|
127
136
|
|
|
137
|
+
**New here?** [docs/QUICKSTART.md](docs/QUICKSTART.md) is a staged setup guide written to be
|
|
138
|
+
handed straight to an AI assistant ("set this up for me"), covering free providers, the offload
|
|
139
|
+
switch, local models, and using your other CLI subscriptions as fallback lanes.
|
|
140
|
+
|
|
141
|
+
### Verifying a setup — two checks, two different questions
|
|
142
|
+
|
|
143
|
+
`keys` answers *are my credentials good?* `pools --probe` answers *will the models I configured
|
|
144
|
+
actually answer?* Both are needed, and the cheap one can be confidently wrong in either
|
|
145
|
+
direction:
|
|
146
|
+
|
|
147
|
+
- A 200 from a provider's `/models` proves nothing when that endpoint is **public** — a revoked
|
|
148
|
+
key still returns the full catalogue. `keys` now re-probes anonymously and escalates to an
|
|
149
|
+
authenticated completion when it must.
|
|
150
|
+
- A 401/403 on that probe does **not** prove the key is bad — free-tier rosters list premium
|
|
151
|
+
models a valid key cannot touch. The probe is compared against the same request sent with no
|
|
152
|
+
credentials: a different status means the key authenticated; an identical one means nothing
|
|
153
|
+
could be concluded, reported as `UNVERIFIED` rather than as a bad key.
|
|
154
|
+
- Neither of those can see a model that is configured, catalogued, and dead. Only
|
|
155
|
+
`pools --probe` can.
|
|
156
|
+
|
|
157
|
+
Keys are read from the environment and, if present, from `~/.llm-relay/.env` (one `KEY=value`
|
|
158
|
+
per line). **A variable already set in the environment always wins over the file.**
|
|
159
|
+
|
|
128
160
|
A global install also drops a **Claude Code skill** at `~/.claude/skills/llm-relay/SKILL.md` — an
|
|
129
161
|
operating guide (addressing pools/models, the offload switch, `@relay:` directives, reading the
|
|
130
162
|
candidates table, failure modes) that Claude loads on demand. It refreshes automatically on every
|
|
@@ -235,7 +267,8 @@ a `routing` block that maps each request's `model` to one provider + backend mod
|
|
|
235
267
|
}
|
|
236
268
|
},
|
|
237
269
|
"mode": "repair", // detect | repair (strict accepted, aliases detect)
|
|
238
|
-
|
|
270
|
+
// Omit `destructiveTools` to get exactly this default list. Names are matched EXACTLY.
|
|
271
|
+
"repair": { "maxAttempts": 2, "destructiveTools": ["Bash","BashOutput","Write","Edit","MultiEdit","NotebookEdit","rm","delete","delete_file","remove","overwrite","drop","reset","force_push"] },
|
|
239
272
|
"log": { "level": "metadata", "file": null } // metadata-only; NEVER logs headers/bodies
|
|
240
273
|
}
|
|
241
274
|
```
|
|
@@ -259,6 +292,33 @@ Claude Code subagent frontmatter (`model:`), which accepts a full model id but n
|
|
|
259
292
|
list. A pool is the indirection that gives those callers ranking and failover. `pool` is a
|
|
260
293
|
reserved provider name; configuring a provider called `pool` fails at load.
|
|
261
294
|
|
|
295
|
+
### Destructive-tool refusal (`repair.destructiveTools`)
|
|
296
|
+
|
|
297
|
+
A repaired tool call may run under `--dangerously-skip-permissions`, so llm-relay refuses to emit
|
|
298
|
+
one that names a destructive tool — it never guesses arguments for it. Two things about the list
|
|
299
|
+
are worth knowing before you configure it:
|
|
300
|
+
|
|
301
|
+
- **Matching is exact on the tool name, case-insensitively** — not substring. A pattern ending in
|
|
302
|
+
`*` is an opt-in prefix form (`"git_*"` covers `git_push` and `git_reset_hard` but not
|
|
303
|
+
`gitlab_read`); a bare `"*"` matches nothing.
|
|
304
|
+
- **The default list leads with the harness's own write/execute tools** — `Bash`, `BashOutput`,
|
|
305
|
+
`Write`, `Edit`, `MultiEdit`, `NotebookEdit` — then the conventional MCP-style names (`rm`,
|
|
306
|
+
`delete`, `delete_file`, `remove`, `overwrite`, `drop`, `reset`, `force_push`).
|
|
307
|
+
|
|
308
|
+
Both of those changed, and both are visible in behaviour. Refusal used to be substring matching
|
|
309
|
+
over fragments like `rm`/`delete`/`push`, which was wrong in **both** directions at once: none of
|
|
310
|
+
those fragments occur in `Bash`/`Write`/`Edit`, so the tools that can actually destroy something
|
|
311
|
+
were never guarded — while `push` matched `PushNotification` and `reset` matched `ResetZoom`,
|
|
312
|
+
refusing safe calls. So:
|
|
313
|
+
|
|
314
|
+
- a malformed `Bash`/`Write`/`Edit`/`MultiEdit`/`NotebookEdit`/`BashOutput` call that used to be
|
|
315
|
+
repaired is now **refused** (logged as `repair: "refused_destructive"`; the request fails clean
|
|
316
|
+
instead of emitting a call the model did not correctly produce);
|
|
317
|
+
- a call named `PushNotification`, `ResetZoom` or `ForceRefresh` is now **permitted**.
|
|
318
|
+
|
|
319
|
+
There is no built-in list inside the proxy: an empty `repair.destructiveTools` refuses nothing, so
|
|
320
|
+
coverage is always traceable to your config.
|
|
321
|
+
|
|
262
322
|
### Subagent offload (`routing.offload` + `routing.subagents`)
|
|
263
323
|
|
|
264
324
|
Send Claude Code **subagents** to other providers while the human's own conversation stays on the
|
|
@@ -389,6 +449,11 @@ candidates and fails over on transport errors. A **refusal** is never retried on
|
|
|
389
449
|
candidate — a reshaper declining to guess is a real judgement, and retrying it elsewhere is
|
|
390
450
|
shopping for a more compliant answer, which is how a fabricated tool call gets through.
|
|
391
451
|
|
|
452
|
+
If **every** candidate fails at the transport level, that is a total outage, not a judgement: the
|
|
453
|
+
turn fails clean and is logged `repair: "failed"` (nothing was reachable), never `"refused"` (a
|
|
454
|
+
model declined). The two are kept distinguishable in the log because they call for opposite
|
|
455
|
+
responses — one is an infrastructure problem, the other is the safety boundary working.
|
|
456
|
+
|
|
392
457
|
Anthropic-kind entries in the pool are skipped (they cannot reshape); a pool with no usable
|
|
393
458
|
target is a loud startup error, never a silently absent reshaper.
|
|
394
459
|
|
|
@@ -449,11 +514,15 @@ default, and dispatcher-style usage is just "send namespaced ids + read `/regist
|
|
|
449
514
|
|
|
450
515
|
### Model tiers from leaderboards (never a hand-maintained table)
|
|
451
516
|
|
|
452
|
-
`npm run sync:tiers` snapshots capability rankings from **
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
517
|
+
`npm run sync:tiers` snapshots capability rankings into `docs/tier-data.json` from **four** sources
|
|
518
|
+
— **OpenRouter** (Artificial Analysis intelligence / coding / agentic indices, Design Arena Elo,
|
|
519
|
+
context length, pricing, tool support), **BFCL** (Berkeley Function-Calling Leaderboard — tool-use
|
|
520
|
+
accuracy, the primary signal for a tool-call proxy, incl. its Irrelevance-Detection metric = the
|
|
521
|
+
malformed-call proxy), **LMArena** (general capability) and **Aider** (polyglot edit benchmark) —
|
|
522
|
+
and prints the top tool-callers so you can pick tier targets from real data. Every source is
|
|
523
|
+
synced-not-forked, and each is independently failable so one dead endpoint does not cost the
|
|
524
|
+
others; a **schema change inside** a source still fails the sync loudly, because a renamed column
|
|
525
|
+
is corruption rather than absence. Zero working sources is fatal.
|
|
457
526
|
|
|
458
527
|
The reshaper also takes `"kind": "openai"` — so `repair` mode can run entirely on an OpenAI-compatible provider (e.g. NIM) with no Anthropic key. The reshaper is asked only for the **corrected arguments per tool-call id** (not the full message envelope), which is far more reliable on weaker models; the proxy reconstructs the message and re-validates it.
|
|
459
528
|
|
|
@@ -466,10 +535,14 @@ Then point a `claude` CLI at it (see "Install & run" above) and inspect the log
|
|
|
466
535
|
|
|
467
536
|
## What it logs (per request, metadata only)
|
|
468
537
|
|
|
469
|
-
`{ ts, path, backendModel, hadTools, streamed, backendStatus, validated: pass|fail|uncheckable|skipped, toolUseCount, uncheckableCount, errorKinds[], latencyMs }`
|
|
538
|
+
`{ ts, path, backendModel, servedProvider, servedModel, hadTools, streamed, backendStatus, validated: pass|fail|uncheckable|skipped, toolUseCount, uncheckableCount, errorKinds[], repair: none|fixed|failed|refused|refused_destructive, latencyMs }`
|
|
539
|
+
|
|
540
|
+
That list is an **allow-list applied at the sink**, not a convention: the writer projects every record through it, so a caller that hands over a wider object cannot leak a header, a body or an error string carrying a key — and a new field starts being logged only when someone deliberately adds it to the list. `path` is passed through `logSafePath()`, which keeps the route and each query parameter's *name* and replaces its value with the value's length, because a `?task=` value is user prose, not metadata. A failed log write is swallowed to stderr: a full disk is a logging problem, never a request failure.
|
|
470
541
|
|
|
471
542
|
`uncheckable` = a declared tool with no `input_schema` (built-in `bash`/`text_editor`/…) or a schema that wouldn't compile — surfaced distinctly so an unvalidatable call is never miscounted as a clean pass.
|
|
472
543
|
|
|
544
|
+
⚠ `backendModel` is the model the **client asked for**, which for a tier or pool spec is routinely not the one that answered; `servedProvider`/`servedModel` are the deployment that actually served it. Draw "which model trips the validator" conclusions from the served fields. (`backendModel` is deprecated and on its way out; the served fields are absent — not `null` — on call sites not yet migrated, so an unmigrated call site can never be mistaken for a request nothing served.)
|
|
545
|
+
|
|
473
546
|
This is the dataset for deciding which backend models are *format-broken* (reshapeable later) vs pass cleanly. Run in `detect` first, measure, then decide on repair.
|
|
474
547
|
|
|
475
548
|
### Trip-rate dataset
|
|
@@ -533,6 +606,15 @@ Consumers (audit-tools dispatch, plain `claude` CLI) point `ANTHROPIC_BASE_URL`
|
|
|
533
606
|
## Dev
|
|
534
607
|
|
|
535
608
|
```bash
|
|
536
|
-
npm run
|
|
609
|
+
npm run check # typecheck + suite — the one gate, and exactly what CI runs
|
|
610
|
+
npm run typecheck # tsc --noEmit, src/ only
|
|
537
611
|
npm test # vitest (validator, SSE reconstruction, e2e transparency+detection)
|
|
612
|
+
npm run build # tsc -> dist/ (scripts/*.mjs read dist/, so rebuild before running them)
|
|
538
613
|
```
|
|
614
|
+
|
|
615
|
+
`.github/workflows/ci.yml` runs `npm run check` on every push to `main` and every pull request.
|
|
616
|
+
|
|
617
|
+
⚠ **Nothing type-checks `test/`.** `tsconfig.json` compiles `src/` only and excludes `**/*.test.ts`,
|
|
618
|
+
and vitest transpiles tests without type-checking them (no `typecheck` block in
|
|
619
|
+
`vitest.config.ts`). A `@ts-expect-error` inside a test file is therefore never evaluated and
|
|
620
|
+
proves nothing — assert at runtime instead.
|
package/config.example.json
CHANGED
|
@@ -89,13 +89,20 @@
|
|
|
89
89
|
"repair": {
|
|
90
90
|
"maxAttempts": 2,
|
|
91
91
|
"destructiveTools": [
|
|
92
|
+
"Bash",
|
|
93
|
+
"BashOutput",
|
|
94
|
+
"Write",
|
|
95
|
+
"Edit",
|
|
96
|
+
"MultiEdit",
|
|
97
|
+
"NotebookEdit",
|
|
92
98
|
"rm",
|
|
93
99
|
"delete",
|
|
94
|
-
"
|
|
95
|
-
"
|
|
100
|
+
"delete_file",
|
|
101
|
+
"remove",
|
|
96
102
|
"overwrite",
|
|
97
103
|
"drop",
|
|
98
|
-
"reset"
|
|
104
|
+
"reset",
|
|
105
|
+
"force_push"
|
|
99
106
|
]
|
|
100
107
|
},
|
|
101
108
|
"log": {
|
package/dist/anthropic.d.ts
CHANGED
|
@@ -28,10 +28,30 @@ export interface OpaqueBlock {
|
|
|
28
28
|
}
|
|
29
29
|
export type ContentBlock = TextBlock | ToolUseBlock | OpaqueBlock;
|
|
30
30
|
export type StopReason = "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | string | null;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The assistant message the proxy validates (from JSON body or reconstructed from SSE).
|
|
33
|
+
*
|
|
34
|
+
* `id` / `model` / `stop_sequence` exist only because a repaired response has to be
|
|
35
|
+
* re-serialized and the backend's own identity has to survive that (DAT-27df2443): the
|
|
36
|
+
* shape previously carried neither, so `emitSse` had nowhere to read an id from and fell
|
|
37
|
+
* back to the constant `msg_repair` for every repaired turn — every repair looked like
|
|
38
|
+
* the same message. They are all `undefined` when the source response did not carry them,
|
|
39
|
+
* which is deliberately distinguishable from a captured value; nothing here invents one.
|
|
40
|
+
* This is still not an SDK model — do not add a field without a finding that needs it AND
|
|
41
|
+
* a consumer that actually threads it through.
|
|
42
|
+
*/
|
|
32
43
|
export interface AssistantMessage {
|
|
44
|
+
/** The backend's own message id, when the response carried one. */
|
|
45
|
+
id?: string | undefined;
|
|
46
|
+
/** The model the backend reported serving, when the response carried one. */
|
|
47
|
+
model?: string | undefined;
|
|
33
48
|
content: ContentBlock[];
|
|
34
49
|
stop_reason: StopReason;
|
|
50
|
+
stop_sequence?: string | null | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Token usage as the backend reported it. An ABSENT field means "the backend did not
|
|
53
|
+
* tell us", which is not the same claim as `0`; callers must not fill it with a zero.
|
|
54
|
+
*/
|
|
35
55
|
usage?: {
|
|
36
56
|
input_tokens?: number;
|
|
37
57
|
output_tokens?: number;
|
package/dist/anthropic.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../src/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../src/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkEH,MAAM,UAAU,cAAc,CAAC,CAAe;IAC5C,OAAO,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC;AAC/B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,WAAoB;IAChD,MAAM,GAAG,GAAG,IAAI,GAAG,EAA6B,CAAC;IACjD,IACE,OAAO,WAAW,KAAK,QAAQ;QAC/B,WAAW,KAAK,IAAI;QACpB,KAAK,CAAC,OAAO,CAAE,WAAmC,CAAC,KAAK,CAAC,EACzD,CAAC;QACD,KAAK,MAAM,CAAC,IAAK,WAAoC,CAAC,KAAK,EAAE,CAAC;YAC5D,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,OAAQ,CAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAChF,MAAM,SAAS,GAAI,CAAgC,CAAC,YAAY,CAAC;gBACjE,MAAM,MAAM,GACV,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,IAAI;oBACjD,CAAC,CAAE,SAAwB;oBAC3B,CAAC,CAAC,IAAI,CAAC;gBACX,GAAG,CAAC,GAAG,CAAE,CAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/authEnv.d.ts
CHANGED
|
@@ -25,6 +25,75 @@ export interface AuthEnvResolution {
|
|
|
25
25
|
/** Every name that was considered, for diagnostics. */
|
|
26
26
|
candidates: string[];
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether a credential value counts as PRESENT. The single predicate — three
|
|
30
|
+
* call sites used to disagree (config.ts tested Boolean() with no trim while
|
|
31
|
+
* server.ts and candidates.ts trimmed), so a whitespace-only key read present
|
|
32
|
+
* to the active-key filter and absent to header construction. That gap is how a
|
|
33
|
+
* blank credential slipped past containment entirely.
|
|
34
|
+
*/
|
|
35
|
+
export declare function keyIsPresent(value: string | undefined): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a provider's credential handling is DECLARED, and if so whether the
|
|
38
|
+
* key is actually there.
|
|
39
|
+
*
|
|
40
|
+
* ⚠ Derived from the config DECLARATION, never from `resolveAuthEnv` having
|
|
41
|
+
* returned a name. Those are different questions: the alias list for anthropic
|
|
42
|
+
* includes ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN, so a provider with NO
|
|
43
|
+
* declared authEnv — an intentional passthrough — still resolves to a name
|
|
44
|
+
* whenever either variable happens to be set in the environment. Deriving state
|
|
45
|
+
* from the name would classify that passthrough as `declared-present`, making it
|
|
46
|
+
* inject a key and strip the caller's own token: the exact inversion of the one
|
|
47
|
+
* behaviour a passthrough exists to provide.
|
|
48
|
+
*/
|
|
49
|
+
export type CredentialState = "not-declared" | "declared-present" | "declared-missing";
|
|
50
|
+
export declare function credentialState(declaredAuthEnv: string | undefined, env?: NodeJS.ProcessEnv): CredentialState;
|
|
51
|
+
/**
|
|
52
|
+
* Read a declared provider credential, normalised.
|
|
53
|
+
*
|
|
54
|
+
* Returns the trimmed value when the credential is PRESENT (per `keyIsPresent`) and
|
|
55
|
+
* `undefined` otherwise, so a caller cannot accidentally hold a whitespace-only
|
|
56
|
+
* string that is truthy to `if (key)` but blank on the wire. Every credential read
|
|
57
|
+
* should go through here rather than open-coding `env[name]?.trim()` — that
|
|
58
|
+
* open-coding is what let the presence predicate drift between call sites.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readCredential(declaredAuthEnv: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Which header a provider's credential is injected into.
|
|
63
|
+
*
|
|
64
|
+
* Structurally identical to `AuthHeader` in `config.ts` and freely assignable in
|
|
65
|
+
* both directions. It is redeclared here rather than imported so this module keeps
|
|
66
|
+
* ZERO dependency on `config.ts` — `config.ts` imports this one, and `tier-data.ts`
|
|
67
|
+
* already exists as a separate module for exactly that reason.
|
|
68
|
+
*/
|
|
69
|
+
export type AuthHeaderName = "x-api-key" | "authorization";
|
|
70
|
+
/**
|
|
71
|
+
* THE construction site for a provider credential header.
|
|
72
|
+
*
|
|
73
|
+
* Returns `{}` when the credential is absent, so the builder — not each caller —
|
|
74
|
+
* is what guarantees a blank key never reaches the wire as an empty `x-api-key` or
|
|
75
|
+
* a bare `Bearer`. Callers merge the result; they must not test the key themselves.
|
|
76
|
+
*
|
|
77
|
+
* Two normalisations, both deliberate:
|
|
78
|
+
* - The value is trimmed. A key pasted into `~/.llm-relay/.env` with a trailing
|
|
79
|
+
* newline is a valid key that 401s, which reads as "my key is bad".
|
|
80
|
+
* - `Bearer ` prefixing is idempotent. Three of the existing sites already accept a
|
|
81
|
+
* value that carries its own `Bearer ` prefix; double-prefixing it would break
|
|
82
|
+
* them on migration.
|
|
83
|
+
*
|
|
84
|
+
* ⚠ It obeys the DECLARED `authHeader` and never consults `provider.kind`. Three
|
|
85
|
+
* current sites (`key-checker.ts`, `ping/ping.ts`, `pool-health.ts`) additionally
|
|
86
|
+
* force `x-api-key` on any `kind: "anthropic"` provider, which silently discards an
|
|
87
|
+
* explicit `authHeader: "authorization"`. `config.ts` already defaults an
|
|
88
|
+
* anthropic-kind provider's `authHeader` to `x-api-key`, so migrating those sites is
|
|
89
|
+
* behaviour-preserving in every case EXCEPT that explicit override — where honouring
|
|
90
|
+
* the config is the correct answer. It is called out rather than encoded so the
|
|
91
|
+
* change is a visible decision, not a silent one.
|
|
92
|
+
*
|
|
93
|
+
* Non-credential companions (`anthropic-version`, `Content-Type`) stay with the
|
|
94
|
+
* caller: this function builds the auth header and nothing else.
|
|
95
|
+
*/
|
|
96
|
+
export declare function buildAuthHeaders(key: string | undefined, authHeader: AuthHeaderName): Record<string, string>;
|
|
28
97
|
/**
|
|
29
98
|
* Pick the env-var name this provider's key actually lives under. Falls back to the
|
|
30
99
|
* declared name when nothing is set, so "missing key" diagnostics still name the
|
package/dist/authEnv.js
CHANGED
|
@@ -48,6 +48,71 @@ export function candidateEnvNames(providerName, declared) {
|
|
|
48
48
|
];
|
|
49
49
|
return [...new Set(ordered)];
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether a credential value counts as PRESENT. The single predicate — three
|
|
53
|
+
* call sites used to disagree (config.ts tested Boolean() with no trim while
|
|
54
|
+
* server.ts and candidates.ts trimmed), so a whitespace-only key read present
|
|
55
|
+
* to the active-key filter and absent to header construction. That gap is how a
|
|
56
|
+
* blank credential slipped past containment entirely.
|
|
57
|
+
*/
|
|
58
|
+
export function keyIsPresent(value) {
|
|
59
|
+
return (value ?? "").trim().length > 0;
|
|
60
|
+
}
|
|
61
|
+
export function credentialState(declaredAuthEnv, env = process.env) {
|
|
62
|
+
if (!declaredAuthEnv)
|
|
63
|
+
return "not-declared";
|
|
64
|
+
return keyIsPresent(env[declaredAuthEnv]) ? "declared-present" : "declared-missing";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read a declared provider credential, normalised.
|
|
68
|
+
*
|
|
69
|
+
* Returns the trimmed value when the credential is PRESENT (per `keyIsPresent`) and
|
|
70
|
+
* `undefined` otherwise, so a caller cannot accidentally hold a whitespace-only
|
|
71
|
+
* string that is truthy to `if (key)` but blank on the wire. Every credential read
|
|
72
|
+
* should go through here rather than open-coding `env[name]?.trim()` — that
|
|
73
|
+
* open-coding is what let the presence predicate drift between call sites.
|
|
74
|
+
*/
|
|
75
|
+
export function readCredential(declaredAuthEnv, env = process.env) {
|
|
76
|
+
if (!declaredAuthEnv)
|
|
77
|
+
return undefined;
|
|
78
|
+
const raw = env[declaredAuthEnv];
|
|
79
|
+
return keyIsPresent(raw) ? raw.trim() : undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* THE construction site for a provider credential header.
|
|
83
|
+
*
|
|
84
|
+
* Returns `{}` when the credential is absent, so the builder — not each caller —
|
|
85
|
+
* is what guarantees a blank key never reaches the wire as an empty `x-api-key` or
|
|
86
|
+
* a bare `Bearer`. Callers merge the result; they must not test the key themselves.
|
|
87
|
+
*
|
|
88
|
+
* Two normalisations, both deliberate:
|
|
89
|
+
* - The value is trimmed. A key pasted into `~/.llm-relay/.env` with a trailing
|
|
90
|
+
* newline is a valid key that 401s, which reads as "my key is bad".
|
|
91
|
+
* - `Bearer ` prefixing is idempotent. Three of the existing sites already accept a
|
|
92
|
+
* value that carries its own `Bearer ` prefix; double-prefixing it would break
|
|
93
|
+
* them on migration.
|
|
94
|
+
*
|
|
95
|
+
* ⚠ It obeys the DECLARED `authHeader` and never consults `provider.kind`. Three
|
|
96
|
+
* current sites (`key-checker.ts`, `ping/ping.ts`, `pool-health.ts`) additionally
|
|
97
|
+
* force `x-api-key` on any `kind: "anthropic"` provider, which silently discards an
|
|
98
|
+
* explicit `authHeader: "authorization"`. `config.ts` already defaults an
|
|
99
|
+
* anthropic-kind provider's `authHeader` to `x-api-key`, so migrating those sites is
|
|
100
|
+
* behaviour-preserving in every case EXCEPT that explicit override — where honouring
|
|
101
|
+
* the config is the correct answer. It is called out rather than encoded so the
|
|
102
|
+
* change is a visible decision, not a silent one.
|
|
103
|
+
*
|
|
104
|
+
* Non-credential companions (`anthropic-version`, `Content-Type`) stay with the
|
|
105
|
+
* caller: this function builds the auth header and nothing else.
|
|
106
|
+
*/
|
|
107
|
+
export function buildAuthHeaders(key, authHeader) {
|
|
108
|
+
if (!keyIsPresent(key))
|
|
109
|
+
return {};
|
|
110
|
+
const value = key.trim();
|
|
111
|
+
if (authHeader === "authorization") {
|
|
112
|
+
return { authorization: value.startsWith("Bearer ") ? value : `Bearer ${value}` };
|
|
113
|
+
}
|
|
114
|
+
return { "x-api-key": value };
|
|
115
|
+
}
|
|
51
116
|
/**
|
|
52
117
|
* Pick the env-var name this provider's key actually lives under. Falls back to the
|
|
53
118
|
* declared name when nothing is set, so "missing key" diagnostics still name the
|
|
@@ -55,7 +120,7 @@ export function candidateEnvNames(providerName, declared) {
|
|
|
55
120
|
*/
|
|
56
121
|
export function resolveAuthEnv(providerName, declared, env = process.env) {
|
|
57
122
|
const candidates = candidateEnvNames(providerName, declared);
|
|
58
|
-
const found = candidates.find((n) => (env[n]
|
|
123
|
+
const found = candidates.find((n) => keyIsPresent(env[n]));
|
|
59
124
|
return {
|
|
60
125
|
name: found ?? declared,
|
|
61
126
|
viaAlias: Boolean(found && found !== declared),
|
package/dist/authEnv.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authEnv.js","sourceRoot":"","sources":["../src/authEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4FAA4F;AAC5F,MAAM,oBAAoB,GAA6B;IACrD,MAAM,EAAE;QACN,gBAAgB;QAChB,kBAAkB;QAClB,mBAAmB;QACnB,sBAAsB;QACtB,uBAAuB;QACvB,gBAAgB;KACjB;IACD,GAAG,EAAE,CAAC,gBAAgB,EAAE,oBAAoB,EAAE,aAAa,CAAC;IAC5D,UAAU,EAAE,CAAC,oBAAoB,EAAE,qBAAqB,CAAC;IACzD,IAAI,EAAE,CAAC,cAAc,CAAC;IACtB,OAAO,EAAE,CAAC,iBAAiB,EAAE,mBAAmB,CAAC;IACjD,QAAQ,EAAE,CAAC,kBAAkB,CAAC;IAC9B,SAAS,EAAE,CAAC,mBAAmB,CAAC;IAChC,MAAM,EAAE,CAAC,gBAAgB,CAAC;IAC1B,SAAS,EAAE,CAAC,mBAAmB,EAAE,sBAAsB,CAAC;CACzD,CAAC;AAEF,4FAA4F;AAC5F,SAAS,IAAI,CAAC,YAAoB;IAChC,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AACxF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB,EAAE,QAAiB;IACvE,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7B,MAAM,OAAO,GAAG;QACd,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,oBAAoB,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QAC3D,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACzD,CAAC;IACF,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/B,CAAC;AAWD
|
|
1
|
+
{"version":3,"file":"authEnv.js","sourceRoot":"","sources":["../src/authEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4FAA4F;AAC5F,MAAM,oBAAoB,GAA6B;IACrD,MAAM,EAAE;QACN,gBAAgB;QAChB,kBAAkB;QAClB,mBAAmB;QACnB,sBAAsB;QACtB,uBAAuB;QACvB,gBAAgB;KACjB;IACD,GAAG,EAAE,CAAC,gBAAgB,EAAE,oBAAoB,EAAE,aAAa,CAAC;IAC5D,UAAU,EAAE,CAAC,oBAAoB,EAAE,qBAAqB,CAAC;IACzD,IAAI,EAAE,CAAC,cAAc,CAAC;IACtB,OAAO,EAAE,CAAC,iBAAiB,EAAE,mBAAmB,CAAC;IACjD,QAAQ,EAAE,CAAC,kBAAkB,CAAC;IAC9B,SAAS,EAAE,CAAC,mBAAmB,CAAC;IAChC,MAAM,EAAE,CAAC,gBAAgB,CAAC;IAC1B,SAAS,EAAE,CAAC,mBAAmB,EAAE,sBAAsB,CAAC;CACzD,CAAC;AAEF,4FAA4F;AAC5F,SAAS,IAAI,CAAC,YAAoB;IAChC,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AACxF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB,EAAE,QAAiB;IACvE,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7B,MAAM,OAAO,GAAG;QACd,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,oBAAoB,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QAC3D,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACzD,CAAC;IACF,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/B,CAAC;AAWD;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,KAAyB;IACpD,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AACzC,CAAC;AAiBD,MAAM,UAAU,eAAe,CAC7B,eAAmC,EACnC,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,CAAC,eAAe;QAAE,OAAO,cAAc,CAAC;IAC5C,OAAO,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,CAAC;AACtF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,eAAmC,EACnC,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,CAAC,eAAe;QAAE,OAAO,SAAS,CAAC;IACvC,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,CAAC;IACjC,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACrD,CAAC;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAuB,EACvB,UAA0B;IAE1B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,GAAI,CAAC,IAAI,EAAE,CAAC;IAC1B,IAAI,UAAU,KAAK,eAAe,EAAE,CAAC;QACnC,OAAO,EAAE,aAAa,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,KAAK,EAAE,EAAE,CAAC;IACpF,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,YAAoB,EACpB,QAA4B,EAC5B,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,UAAU,GAAG,iBAAiB,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,OAAO;QACL,IAAI,EAAE,KAAK,IAAI,QAAQ;QACvB,QAAQ,EAAE,OAAO,CAAC,KAAK,IAAI,KAAK,KAAK,QAAQ,CAAC;QAC9C,UAAU;KACX,CAAC;AACJ,CAAC"}
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { type ResolvedTarget } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Response header stating who produced an error status: the provider, or this proxy.
|
|
4
|
+
*
|
|
5
|
+
* Every failure out of `fetchBackend` is a synthesized `Response` — a refused document, a
|
|
6
|
+
* translation bug and a genuinely dead provider all arrived as a bare status code, so a
|
|
7
|
+
* caller counting backend failures (the circuit breaker) charged our own local bugs to the
|
|
8
|
+
* provider and failed over to a second provider that would have failed identically. The
|
|
9
|
+
* marker is what makes them separable; `fetchBackend` states it, the caller decides.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ERROR_ORIGIN_HEADER = "x-llm-relay-error-origin";
|
|
12
|
+
/** `upstream` = the provider answered with this status. `local` = the proxy produced it without asking. */
|
|
13
|
+
export type ErrorOrigin = "upstream" | "local";
|
|
14
|
+
/** Read the origin marker off a Response, when it carries one. */
|
|
15
|
+
export declare function errorOrigin(res: Response): ErrorOrigin | null;
|
|
2
16
|
/**
|
|
3
17
|
* Fetch the resolved provider target and return an ANTHROPIC-shaped `Response`,
|
|
4
18
|
* regardless of the backend's native wire format. For kind="anthropic" this is a
|
package/dist/backend.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { translateBetweenProviders, handleUniversalStreamRequest } from "llm-bridge";
|
|
2
2
|
import { DocumentError, transcodeDocuments } from "./documents.js";
|
|
3
|
+
/**
|
|
4
|
+
* Response header stating who produced an error status: the provider, or this proxy.
|
|
5
|
+
*
|
|
6
|
+
* Every failure out of `fetchBackend` is a synthesized `Response` — a refused document, a
|
|
7
|
+
* translation bug and a genuinely dead provider all arrived as a bare status code, so a
|
|
8
|
+
* caller counting backend failures (the circuit breaker) charged our own local bugs to the
|
|
9
|
+
* provider and failed over to a second provider that would have failed identically. The
|
|
10
|
+
* marker is what makes them separable; `fetchBackend` states it, the caller decides.
|
|
11
|
+
*/
|
|
12
|
+
export const ERROR_ORIGIN_HEADER = "x-llm-relay-error-origin";
|
|
13
|
+
/** Read the origin marker off a Response, when it carries one. */
|
|
14
|
+
export function errorOrigin(res) {
|
|
15
|
+
const v = res.headers.get(ERROR_ORIGIN_HEADER);
|
|
16
|
+
return v === "upstream" || v === "local" ? v : null;
|
|
17
|
+
}
|
|
3
18
|
/**
|
|
4
19
|
* Fetch the resolved provider target and return an ANTHROPIC-shaped `Response`,
|
|
5
20
|
* regardless of the backend's native wire format. For kind="anthropic" this is a
|
|
@@ -23,16 +38,18 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
23
38
|
reqJson = await transcodeDocuments(reqJson);
|
|
24
39
|
}
|
|
25
40
|
catch (e) {
|
|
41
|
+
// Local, both of them: the provider was never asked. Charging these to the provider's
|
|
42
|
+
// failure budget fails over to a second provider that would refuse the same document.
|
|
26
43
|
if (e instanceof DocumentError)
|
|
27
|
-
return anthropicError(400, `llm-relay: ${e.message}
|
|
28
|
-
return anthropicError(502, `document conversion failed: ${e.message}
|
|
44
|
+
return anthropicError(400, `llm-relay: ${e.message}`, "local");
|
|
45
|
+
return anthropicError(502, `document conversion failed: ${e.message}`, "local");
|
|
29
46
|
}
|
|
30
47
|
let openaiBody;
|
|
31
48
|
try {
|
|
32
49
|
openaiBody = translateBetweenProviders("anthropic", "openai", (reqJson ?? {}));
|
|
33
50
|
}
|
|
34
51
|
catch (e) {
|
|
35
|
-
return anthropicError(502, `request translation failed: ${e.message}
|
|
52
|
+
return anthropicError(502, `request translation failed: ${e.message}`, "local");
|
|
36
53
|
}
|
|
37
54
|
openaiBody.model = target.model;
|
|
38
55
|
openaiBody.stream = args.wantsStream;
|
|
@@ -70,7 +87,8 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
70
87
|
const hint = res.status === 404
|
|
71
88
|
? ` — model "${target.model}" is not served by provider "${target.provider}" (a model can be listed in /models and still 404 here)`
|
|
72
89
|
: "";
|
|
73
|
-
|
|
90
|
+
// The provider really answered with this status — the body is reworded, the origin is not.
|
|
91
|
+
return anthropicError(res.status, `openai backend HTTP ${res.status}${hint}: ${body.slice(0, 300)}`, "upstream");
|
|
74
92
|
}
|
|
75
93
|
if (args.wantsStream && res.body) {
|
|
76
94
|
const anthStream = handleUniversalStreamRequest(res.body, "openai", "anthropic");
|
|
@@ -81,7 +99,9 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
81
99
|
anthropicJson = openAiResponseToAnthropic((await res.json()), target.model ?? "");
|
|
82
100
|
}
|
|
83
101
|
catch (e) {
|
|
84
|
-
|
|
102
|
+
// The provider answered 200; this 502 is ours. Marked local so it is not mistaken
|
|
103
|
+
// for the provider being down — it is our mapper being wrong about a healthy one.
|
|
104
|
+
return anthropicError(502, `response translation failed: ${e.message}`, "local");
|
|
85
105
|
}
|
|
86
106
|
return new Response(JSON.stringify(anthropicJson), { status: 200, headers: { "content-type": "application/json" } });
|
|
87
107
|
}
|
|
@@ -118,16 +138,16 @@ export function openAiResponseToAnthropic(j, model) {
|
|
|
118
138
|
usage: { input_tokens: usage.prompt_tokens ?? 0, output_tokens: usage.completion_tokens ?? 0 },
|
|
119
139
|
};
|
|
120
140
|
}
|
|
121
|
-
function anthropicError(status, message) {
|
|
141
|
+
function anthropicError(status, message, origin) {
|
|
122
142
|
return new Response(JSON.stringify({ type: "error", error: { type: "api_error", message } }), {
|
|
123
143
|
status,
|
|
124
|
-
headers: { "content-type": "application/json" },
|
|
144
|
+
headers: { "content-type": "application/json", [ERROR_ORIGIN_HEADER]: origin },
|
|
125
145
|
});
|
|
126
146
|
}
|
|
127
|
-
function openaiError(status, message) {
|
|
147
|
+
function openaiError(status, message, origin) {
|
|
128
148
|
return new Response(JSON.stringify({ error: { message, type: "invalid_request_error" } }), {
|
|
129
149
|
status,
|
|
130
|
-
headers: { "content-type": "application/json" },
|
|
150
|
+
headers: { "content-type": "application/json", [ERROR_ORIGIN_HEADER]: origin },
|
|
131
151
|
});
|
|
132
152
|
}
|
|
133
153
|
/**
|
|
@@ -143,7 +163,7 @@ function openaiError(status, message) {
|
|
|
143
163
|
*/
|
|
144
164
|
export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
145
165
|
if (target.kind !== "openai") {
|
|
146
|
-
return openaiError(400, `llm-relay: OpenAI front requires an openai-kind provider; "${target.provider}" is ${target.kind}
|
|
166
|
+
return openaiError(400, `llm-relay: OpenAI front requires an openai-kind provider; "${target.provider}" is ${target.kind}`, "local");
|
|
147
167
|
}
|
|
148
168
|
const base = (args.reqJson ?? {});
|
|
149
169
|
const body = { ...base, model: target.model, stream: args.wantsStream };
|
package/dist/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAErF,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEnE;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAsB,EACtB,IAQC,EACD,UAAwB,KAAK;IAE7B,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;QACvG,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;QAChD,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,oBAAoB;IACpB,sFAAsF;IACtF,sFAAsF;IACtF,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC3B,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,YAAY,aAAa;YAAE,OAAO,cAAc,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
|
package/dist/benchmarks.d.ts
CHANGED
|
@@ -32,8 +32,30 @@ export interface Strength {
|
|
|
32
32
|
export declare function getStrength(spec: string, opts?: {
|
|
33
33
|
telemetryPath?: string;
|
|
34
34
|
}): Strength;
|
|
35
|
+
/** One target with the strength that ranked it, and the provenance of that strength. */
|
|
36
|
+
export interface RankedTarget {
|
|
37
|
+
target: ResolvedTarget;
|
|
38
|
+
spec: string;
|
|
39
|
+
strength: Strength;
|
|
40
|
+
}
|
|
41
|
+
export declare function specOfTarget(t: ResolvedTarget): string;
|
|
35
42
|
/**
|
|
36
|
-
* Rank targets strongest-first
|
|
37
|
-
*
|
|
43
|
+
* Rank targets strongest-first, KEEPING the provenance that produced each position.
|
|
44
|
+
*
|
|
45
|
+
* ⚠ The comparator used to read `getStrength(spec).score` and throw the rest away, which made a
|
|
46
|
+
* `neutral` 50 — "nobody publishes anything about this model" — indistinguishable from a `snapshot`
|
|
47
|
+
* 50 measured across five leaderboards (`ARC-31833353`). A provenance-free number was deciding
|
|
48
|
+
* which backend serves a request, the one thing this module exists to prevent.
|
|
49
|
+
*
|
|
50
|
+
* Resolution order:
|
|
51
|
+
* 1. score, highest first — the actual capability estimate, unchanged and never adjusted by basis;
|
|
52
|
+
* 2. on an exact tie, the better-evidenced basis (snapshot > telemetry > neutral);
|
|
53
|
+
* 3. still tied, the larger signal count — a 5-source consensus over a 1-source guess;
|
|
54
|
+
* 4. still tied, config order (`sort` is stable), so a pool's declared order is the last word.
|
|
55
|
+
*
|
|
56
|
+
* Steps 2–3 are tie-breaks, never score adjustments: a model is not penalised for signals nobody
|
|
57
|
+
* publishes, it just loses a coin-flip to one we actually know something about.
|
|
38
58
|
*/
|
|
59
|
+
export declare function rankTargetsWithProvenance(targets: ResolvedTarget[]): RankedTarget[];
|
|
60
|
+
/** Ranked targets only. Use `rankTargetsWithProvenance` when the caller can report WHY. */
|
|
39
61
|
export declare function rankTargetsByBenchmark(targets: ResolvedTarget[]): ResolvedTarget[];
|