llm-relay 0.61.0 → 0.63.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.
Files changed (235) hide show
  1. package/dist/accounting-store-io.js +0 -33
  2. package/dist/accounting-store-io.js.map +1 -1
  3. package/dist/accounting-store-schema.js +0 -65
  4. package/dist/accounting-store-schema.js.map +1 -1
  5. package/dist/accounting-store.js +0 -195
  6. package/dist/accounting-store.js.map +1 -1
  7. package/dist/accounting.js +0 -72
  8. package/dist/accounting.js.map +1 -1
  9. package/dist/anthropic.js +0 -18
  10. package/dist/anthropic.js.map +1 -1
  11. package/dist/authEnv.js +0 -78
  12. package/dist/authEnv.js.map +1 -1
  13. package/dist/availability-snapshot.js +0 -58
  14. package/dist/availability-snapshot.js.map +1 -1
  15. package/dist/availability.js +0 -162
  16. package/dist/availability.js.map +1 -1
  17. package/dist/backend.js +1 -416
  18. package/dist/backend.js.map +1 -1
  19. package/dist/benchmarks.js +1 -63
  20. package/dist/benchmarks.js.map +1 -1
  21. package/dist/breaker-persistence.js +0 -59
  22. package/dist/breaker-persistence.js.map +1 -1
  23. package/dist/candidates.js +0 -64
  24. package/dist/candidates.js.map +1 -1
  25. package/dist/catalog.js +1 -140
  26. package/dist/catalog.js.map +1 -1
  27. package/dist/circuit-breaker.js +1 -90
  28. package/dist/circuit-breaker.js.map +1 -1
  29. package/dist/claude-hook.js +0 -81
  30. package/dist/claude-hook.js.map +1 -1
  31. package/dist/cli.d.ts +89 -1
  32. package/dist/cli.js +115 -702
  33. package/dist/cli.js.map +1 -1
  34. package/dist/config-edit.js +0 -11
  35. package/dist/config-edit.js.map +1 -1
  36. package/dist/config.d.ts +21 -0
  37. package/dist/config.js +22 -372
  38. package/dist/config.js.map +1 -1
  39. package/dist/configured-limits.js +0 -58
  40. package/dist/configured-limits.js.map +1 -1
  41. package/dist/context-limits.js +0 -97
  42. package/dist/context-limits.js.map +1 -1
  43. package/dist/control-authorization.js +0 -36
  44. package/dist/control-authorization.js.map +1 -1
  45. package/dist/cooldown-clear.js +0 -6
  46. package/dist/cooldown-clear.js.map +1 -1
  47. package/dist/credential-fleet.js +0 -5
  48. package/dist/credential-fleet.js.map +1 -1
  49. package/dist/credential-select.js +4 -30
  50. package/dist/credential-select.js.map +1 -1
  51. package/dist/dashboard-auth.js +0 -29
  52. package/dist/dashboard-auth.js.map +1 -1
  53. package/dist/dashboard-contract.js +0 -46
  54. package/dist/dashboard-contract.js.map +1 -1
  55. package/dist/dashboard-routes.js +0 -32
  56. package/dist/dashboard-routes.js.map +1 -1
  57. package/dist/dashboard-snapshot.js +0 -154
  58. package/dist/dashboard-snapshot.js.map +1 -1
  59. package/dist/dashboard-static.js +0 -22
  60. package/dist/dashboard-static.js.map +1 -1
  61. package/dist/delegate-gate/cast-necessity.js +1 -22
  62. package/dist/delegate-gate/cast-necessity.js.map +1 -1
  63. package/dist/delegate-gate/cli.js +0 -6
  64. package/dist/delegate-gate/cli.js.map +1 -1
  65. package/dist/delegate-gate/diff-parser.js +0 -32
  66. package/dist/delegate-gate/diff-parser.js.map +1 -1
  67. package/dist/delegate-gate/fix.js +0 -10
  68. package/dist/delegate-gate/fix.js.map +1 -1
  69. package/dist/delegate-gate/gate.js +0 -12
  70. package/dist/delegate-gate/gate.js.map +1 -1
  71. package/dist/delegate-gate/minimality.js +0 -38
  72. package/dist/delegate-gate/minimality.js.map +1 -1
  73. package/dist/delegate-gate/post-image.js +1 -13
  74. package/dist/delegate-gate/post-image.js.map +1 -1
  75. package/dist/delegate-gate/shared-state.js +0 -23
  76. package/dist/delegate-gate/shared-state.js.map +1 -1
  77. package/dist/delegate-gate/test-assertions.js +1 -54
  78. package/dist/delegate-gate/test-assertions.js.map +1 -1
  79. package/dist/delegate-gate/types.js +0 -11
  80. package/dist/delegate-gate/types.js.map +1 -1
  81. package/dist/dialect-stream.js +9 -37
  82. package/dist/dialect-stream.js.map +1 -1
  83. package/dist/dispatch-exhaustion-persistence.js +0 -35
  84. package/dist/dispatch-exhaustion-persistence.js.map +1 -1
  85. package/dist/dispatch.js +0 -269
  86. package/dist/dispatch.js.map +1 -1
  87. package/dist/documents.js +0 -92
  88. package/dist/documents.js.map +1 -1
  89. package/dist/dotenv.js +0 -22
  90. package/dist/dotenv.js.map +1 -1
  91. package/dist/dynamic-pools.js +0 -93
  92. package/dist/dynamic-pools.js.map +1 -1
  93. package/dist/emitSse.js +0 -64
  94. package/dist/emitSse.js.map +1 -1
  95. package/dist/executable-lookup.d.ts +28 -0
  96. package/dist/executable-lookup.js +42 -0
  97. package/dist/executable-lookup.js.map +1 -0
  98. package/dist/hard-cap.js +1 -32
  99. package/dist/hard-cap.js.map +1 -1
  100. package/dist/host-routing.js +0 -40
  101. package/dist/host-routing.js.map +1 -1
  102. package/dist/installed-hosts.d.ts +64 -0
  103. package/dist/installed-hosts.js +75 -0
  104. package/dist/installed-hosts.js.map +1 -0
  105. package/dist/json-shape.js +0 -19
  106. package/dist/json-shape.js.map +1 -1
  107. package/dist/kernel/contracts.js +0 -26
  108. package/dist/kernel/contracts.js.map +1 -1
  109. package/dist/kernel/request-lifecycle.js +0 -4
  110. package/dist/kernel/request-lifecycle.js.map +1 -1
  111. package/dist/key-checker.js +0 -115
  112. package/dist/key-checker.js.map +1 -1
  113. package/dist/key-import.js +0 -20
  114. package/dist/key-import.js.map +1 -1
  115. package/dist/keys-cli.js +0 -15
  116. package/dist/keys-cli.js.map +1 -1
  117. package/dist/keystore.js +2 -51
  118. package/dist/keystore.js.map +1 -1
  119. package/dist/lane-cadence.js +1 -51
  120. package/dist/lane-cadence.js.map +1 -1
  121. package/dist/lane-manifest.js +0 -54
  122. package/dist/lane-manifest.js.map +1 -1
  123. package/dist/lane-probe.js +0 -36
  124. package/dist/lane-probe.js.map +1 -1
  125. package/dist/lane-quota-probe.js +0 -61
  126. package/dist/lane-quota-probe.js.map +1 -1
  127. package/dist/log.js +0 -17
  128. package/dist/log.js.map +1 -1
  129. package/dist/mcp/lane-runner.d.ts +114 -0
  130. package/dist/mcp/lane-runner.js +186 -0
  131. package/dist/mcp/lane-runner.js.map +1 -0
  132. package/dist/mcp/protocol.d.ts +98 -0
  133. package/dist/mcp/protocol.js +67 -0
  134. package/dist/mcp/protocol.js.map +1 -0
  135. package/dist/mcp/server.d.ts +78 -0
  136. package/dist/mcp/server.js +360 -0
  137. package/dist/mcp/server.js.map +1 -0
  138. package/dist/metadata.js +0 -73
  139. package/dist/metadata.js.map +1 -1
  140. package/dist/network-block.js +0 -45
  141. package/dist/network-block.js.map +1 -1
  142. package/dist/offload.js +0 -21
  143. package/dist/offload.js.map +1 -1
  144. package/dist/onboarding.js +0 -36
  145. package/dist/onboarding.js.map +1 -1
  146. package/dist/openai-dialect.js +0 -32
  147. package/dist/openai-dialect.js.map +1 -1
  148. package/dist/openai-request.js +0 -236
  149. package/dist/openai-request.js.map +1 -1
  150. package/dist/os-keyring.d.ts +11 -2
  151. package/dist/os-keyring.js +3 -39
  152. package/dist/os-keyring.js.map +1 -1
  153. package/dist/ping/cadence.js +3 -90
  154. package/dist/ping/cadence.js.map +1 -1
  155. package/dist/ping/metrics.js +0 -91
  156. package/dist/ping/metrics.js.map +1 -1
  157. package/dist/ping/ping.js +0 -17
  158. package/dist/ping/ping.js.map +1 -1
  159. package/dist/ping/probe-cache.js +2 -53
  160. package/dist/ping/probe-cache.js.map +1 -1
  161. package/dist/ping/quota.js +0 -17
  162. package/dist/ping/quota.js.map +1 -1
  163. package/dist/ping/runtime-telemetry.js +1 -18
  164. package/dist/ping/runtime-telemetry.js.map +1 -1
  165. package/dist/pool-health.js +0 -49
  166. package/dist/pool-health.js.map +1 -1
  167. package/dist/presets.js +0 -4
  168. package/dist/presets.js.map +1 -1
  169. package/dist/process-safety-net.js +0 -36
  170. package/dist/process-safety-net.js.map +1 -1
  171. package/dist/quota-demotion.js +1 -35
  172. package/dist/quota-demotion.js.map +1 -1
  173. package/dist/quota-observation.js +0 -21
  174. package/dist/quota-observation.js.map +1 -1
  175. package/dist/rate-limits.js +1 -94
  176. package/dist/rate-limits.js.map +1 -1
  177. package/dist/refusal-interpretation.js +2 -327
  178. package/dist/refusal-interpretation.js.map +1 -1
  179. package/dist/registry.js +0 -11
  180. package/dist/registry.js.map +1 -1
  181. package/dist/repair.js +1 -119
  182. package/dist/repair.js.map +1 -1
  183. package/dist/request-log.js +0 -2
  184. package/dist/request-log.js.map +1 -1
  185. package/dist/reshaper.js +0 -85
  186. package/dist/reshaper.js.map +1 -1
  187. package/dist/resolved-attempt.js +0 -3
  188. package/dist/resolved-attempt.js.map +1 -1
  189. package/dist/responses-request.js +0 -187
  190. package/dist/responses-request.js.map +1 -1
  191. package/dist/routes/admin.js +0 -38
  192. package/dist/routes/admin.js.map +1 -1
  193. package/dist/secret-file-acl.js +0 -26
  194. package/dist/secret-file-acl.js.map +1 -1
  195. package/dist/self-update.js +0 -118
  196. package/dist/self-update.js.map +1 -1
  197. package/dist/server.js +15 -952
  198. package/dist/server.js.map +1 -1
  199. package/dist/session-pin.js +0 -9
  200. package/dist/session-pin.js.map +1 -1
  201. package/dist/setup-claude.js +0 -9
  202. package/dist/setup-claude.js.map +1 -1
  203. package/dist/spend-headroom.js +0 -20
  204. package/dist/spend-headroom.js.map +1 -1
  205. package/dist/sse-frames.js +0 -31
  206. package/dist/sse-frames.js.map +1 -1
  207. package/dist/sse.js +1 -19
  208. package/dist/sse.js.map +1 -1
  209. package/dist/state-paths.js +0 -16
  210. package/dist/state-paths.js.map +1 -1
  211. package/dist/stream-commit.js +1 -27
  212. package/dist/stream-commit.js.map +1 -1
  213. package/dist/target-facts.js +2 -130
  214. package/dist/target-facts.js.map +1 -1
  215. package/dist/telemetry.js +0 -8
  216. package/dist/telemetry.js.map +1 -1
  217. package/dist/think-tags.js +1 -38
  218. package/dist/think-tags.js.map +1 -1
  219. package/dist/tier-data.js +0 -29
  220. package/dist/tier-data.js.map +1 -1
  221. package/dist/tool-dialects.js +1 -103
  222. package/dist/tool-dialects.js.map +1 -1
  223. package/dist/tool-use-ids.js +0 -51
  224. package/dist/tool-use-ids.js.map +1 -1
  225. package/dist/usage-observer.js +0 -52
  226. package/dist/usage-observer.js.map +1 -1
  227. package/dist/validator.js +1 -25
  228. package/dist/validator.js.map +1 -1
  229. package/dist/winenv.js +0 -63
  230. package/dist/winenv.js.map +1 -1
  231. package/dist/write-behind.js +0 -12
  232. package/dist/write-behind.js.map +1 -1
  233. package/package.json +2 -2
  234. package/scripts/install-skill.mjs +62 -9
  235. package/skills/llm-relay/SKILL.md +26 -1
package/dist/backend.js CHANGED
@@ -12,12 +12,6 @@ import { knownToolUseIds, rewriteToolUseIds, rewriteToolUseIdsInStream } from ".
12
12
  import { STREAM_PREFLIGHT_LIMIT } from "./stream-commit.js";
13
13
  import { inspectDialectInOpenAiChat, recoverDialectInOpenAiChatStream, } from "./openai-dialect.js";
14
14
  import { observeUsage } from "./usage-observer.js";
15
- /**
16
- * A tool-call envelope was present in the response text but could not be parsed — truncated, or a
17
- * dialect variant we do not model. Distinct from a mapper defect: the relay's translation is fine,
18
- * the SERVING HOST returned an unusable body. Carried as a retriable failure so the pool fails over
19
- * to a host that parses its models' dialect.
20
- */
21
15
  export class DialectUnparseableError extends Error {
22
16
  dialect;
23
17
  constructor(dialect) {
@@ -26,15 +20,6 @@ export class DialectUnparseableError extends Error {
26
20
  this.name = "DialectUnparseableError";
27
21
  }
28
22
  }
29
- /**
30
- * A tool call recovered from assistant TEXT names a tool on the operator's destructive list.
31
- *
32
- * Unlike `DialectUnparseableError` this is NOT a statement about the host: the envelope parsed
33
- * fine. It is a config decision, so it is carried as a LOCAL failure — the walk must not fail over
34
- * (re-asking N models to produce the same refused action) and the deployment must not be charged,
35
- * the same rule under which a hard cap "never registers on the breaker — it is config, not
36
- * health". See docs/dialect-rescue-destructive-refusal-2026-08-24.md.
37
- */
38
23
  export class DialectDestructiveError extends Error {
39
24
  dialect;
40
25
  refused;
@@ -45,69 +30,16 @@ export class DialectDestructiveError extends Error {
45
30
  this.name = "DialectDestructiveError";
46
31
  }
47
32
  }
48
- /**
49
- * Response header stating who produced an error status: the provider, or this proxy.
50
- *
51
- * Every failure out of `fetchBackend` is a synthesized `Response` — a refused document, a
52
- * translation bug and a genuinely dead provider all arrived as a bare status code, so a
53
- * caller counting backend failures (the circuit breaker) charged our own local bugs to the
54
- * provider and failed over to a second provider that would have failed identically. The
55
- * marker is what makes them separable; `fetchBackend` states it, the caller decides.
56
- */
57
33
  export const ERROR_ORIGIN_HEADER = "x-llm-relay-error-origin";
58
- /** Read the origin marker off a Response, when it carries one. */
59
- /**
60
- * The declared origins, as a runtime set derived from the type rather than hand-listed beside it.
61
- *
62
- * `errorOrigin` is the ONE validator for this header, and its two-literal test was the drift seam:
63
- * a third `ErrorOrigin` member would have been silently rejected here and defaulted to `"upstream"`
64
- * by every caller — an unknown origin reported as the provider's fault, and `upstream` also means
65
- * RETRIABLE, so the walk would reroll other pool members for a fault that may be the relay's own.
66
- * Deriving the set makes a new member a compile error at the table instead.
67
- */
68
34
  const ERROR_ORIGINS = { upstream: true, local: true };
69
35
  export function errorOrigin(res) {
70
36
  const v = res.headers.get(ERROR_ORIGIN_HEADER);
71
37
  return v !== null && Object.hasOwn(ERROR_ORIGINS, v) ? v : null;
72
38
  }
73
- /**
74
- * Response header naming the deployment that actually answered — the (provider, model) left
75
- * standing after pool expansion, benchmark ranking, breaker demotion and failover.
76
- *
77
- * A pool request's most basic debugging question is "who served this?", and until now the only
78
- * way to answer it was to correlate timestamps against the proxy's own log. When every candidate
79
- * fails it carries the list that was tried instead, so an exhausted pool is self-describing.
80
- */
81
39
  export const SERVED_BY_HEADER = "x-llm-relay-served-by";
82
- /**
83
- * WHY each of those candidates dropped out — `"13 tried, 0 served: 4x402, 5x429, 3x403, 1x400"`.
84
- *
85
- * `SERVED_BY_HEADER` answers "who was tried"; this answers "what happened to them", which is the
86
- * half that turns a pool exhaustion into a diagnosis. Without it a client holds one member's
87
- * error — a HuggingFace 402 pointing at a billing page — while the other twelve failed for three
88
- * unrelated reasons, and the correct action ("use another pool") is invisible.
89
- *
90
- * A header rather than a rewritten body, deliberately: the served body stays the last candidate's
91
- * real upstream error, because a true upstream error beats a synthesized one.
92
- */
93
40
  export const POOL_ATTEMPTS_HEADER = "x-llm-relay-pool-attempts";
94
- /** Response metadata headers for credential-aware pool diagnostics. */
95
41
  export const CREDENTIAL_HEADER = "x-llm-relay-credential";
96
42
  export const CREDENTIAL_ATTEMPTS_HEADER = "x-llm-relay-credential-attempts";
97
- /**
98
- * How many refusals in this walk said something the relay could not interpret.
99
- *
100
- * The learned-eligibility store converges only as fast as somebody explains the messages it does
101
- * not recognise, and a pull-only queue is a backlog nobody works. This is the push half: the
102
- * caller — which is usually an agent that is about to report a pool failure to a human anyway —
103
- * finds out at the moment it matters that a NEW kind of refusal just appeared, and can run
104
- * `llm-relay eligibility` while the context is still in hand.
105
- *
106
- * ⚠ It is a COUNT, never the message. The message is untrusted text from an external service, and
107
- * putting it in a response header would be the relay handing an agent attacker-controlled prose in
108
- * a field agents tend to trust. The count says "go look"; `llm-relay eligibility` shows the text
109
- * with its provenance and the enum-constrained verdicts it may receive.
110
- */
111
43
  export const UNKNOWN_REFUSAL_HEADER = "x-llm-relay-unknown-refusal";
112
44
  function oneShotFetch(fetchFn, signal, onEgress) {
113
45
  let invoked = false;
@@ -119,112 +51,23 @@ function oneShotFetch(fetchFn, signal, onEgress) {
119
51
  return fetchFn(input, init);
120
52
  };
121
53
  }
122
- /**
123
- * This answer came from BELOW the effort band that was asked for.
124
- *
125
- * An effort pool falls back to lower-banded live members once its own band is exhausted, because a
126
- * band with nothing behind it turns "the strongest models are busy" into "no answer at all". That
127
- * fallback is automatic — but it must never be silent. A capability downgrade that reads as an
128
- * ordinary 200 is indistinguishable from having got what you asked for, which is the failure mode
129
- * that lets a caller build on a weaker answer without knowing it did.
130
- *
131
- * Names the deployment and the band it fell out of, e.g. `groq/llama-3.3-70b (below xhigh)`.
132
- */
133
54
  export const DEGRADED_HEADER = "x-llm-relay-degraded";
134
- /**
135
- * The walk's FIRST choice was demoted for a spent quota and the answer came from further down.
136
- *
137
- * Same maxim as `DEGRADED_HEADER` — automatic degradation is acceptable only because it is
138
- * announced — but a different fact: capability fell below the requested band there, while here a
139
- * quota figure said the first choice was spent until a known reset. Value is one bounded line,
140
- * e.g. `groq/llama-3.3-70b (requests/minute remaining 0, provider-stated)` — axis/period/remaining
141
- * plus the basis, no credential values, nothing secret.
142
- */
143
55
  export const QUOTA_DEMOTED_HEADER = "x-llm-relay-quota-demoted";
144
- /**
145
- * This request was refused by an OPERATOR-SET HARD CAP (G2) — not by a provider.
146
- *
147
- * Value is one line per capped credential cell, e.g.
148
- * `a/nim/z-ai/glm-5.2 requests/day 450/450` — label/deployment, axis/period, the inclusive
149
- * used/cap pair. Present only when EVERY walked candidate was capped: a partial walk serves
150
- * from whoever remained, and the pool-attempts header carries the `Nxcapped` tally beside the
151
- * other outcomes instead. Nothing secret: the label is the config slot name, the figures are
152
- * the operator's own declaration and this relay's own ledger reading.
153
- *
154
- * BOUNDED by cell count, not just by cell width: at most `MAX_CAPPED_HEADER_CELLS` (5) cells are
155
- * named and the rest are counted as `+K more`. A capped attempt costs no walk start budget, so a
156
- * fully capped 30-member dynamic pool reaches every one of them and an unbounded join would be a
157
- * ~1.2 KB header. The header says what stopped the request; it is not a roster.
158
- */
159
56
  export const HARD_CAP_HEADER = "x-llm-relay-capped";
160
- /**
161
- * This answer came from a deployment that is NOT free.
162
- *
163
- * Pools rank free capacity first but no longer exclude paid capacity, so a spent free lane is not
164
- * a dead end. That is only acceptable if spending is announced: an unflagged paid response is
165
- * indistinguishable from a free one, and the difference is money. Same reasoning as
166
- * `DEGRADED_HEADER` — automatic fallback is fine, silent fallback is not.
167
- *
168
- * Carries the deployment and how its cost was assessed, e.g.
169
- * `openrouter/anthropic/claude-sonnet-5 (paid, published-price)`.
170
- */
171
57
  export const PAID_HEADER = "x-llm-relay-paid";
172
- /** This response contains a tool call reconstructed from a recognized text dialect envelope. */
173
58
  export const TOOL_DIALECT_HEADER = "x-llm-relay-tool-dialect";
174
- /**
175
- * Per-response proof that a streamed dialect refusal was the RELAY's, not the upstream's.
176
- *
177
- * A WeakMap rather than a header or a body field, for the reason the signal exists at all: anything
178
- * on the wire can be echoed by a counterparty. The server hands this to `probeStreamForCommit`, and
179
- * a lane the relay never wrapped simply has no entry — so its bytes can never earn `local`
180
- * provenance. Same shape as `attachUpstreamMetadata`, and deliberately not merged into it: that
181
- * carries log METADATA about a finished response, this is live state for one in-flight stream.
182
- */
183
59
  const dialectRefusalSignals = new WeakMap();
184
60
  export function dialectRefusalSignalOf(response) {
185
61
  return dialectRefusalSignals.get(response);
186
62
  }
187
- /**
188
- * `tool_use` ids in this response were MINTED by the relay because the host reused ones the
189
- * conversation already carried — value `"<n> rewritten"`.
190
- *
191
- * Same maxim as `DEGRADED_HEADER`: an automatic fix is acceptable only because it is announced.
192
- * A count, never an id: the ids themselves are in the body the caller already has, and a header
193
- * is not the place to restate content.
194
- *
195
- * ⚠ Buffered responses only. On a stream the headers are written before the first
196
- * `content_block_start` exists, so a count there could only be a guess; the streaming pass
197
- * reports through `toolUseIdRewrites()` instead, which the server reads for its log record after
198
- * the stream drains. See `src/tool-use-ids.ts`.
199
- */
200
63
  export const TOOL_USE_IDS_HEADER = "x-llm-relay-tool-use-ids";
201
- /**
202
- * OUTBOUND tool-call ids were rewritten to this provider's stated id shape — value
203
- * `"<n> rewritten"`. Today that is only mistral's `^[a-zA-Z0-9]{9}$` (`compat.toolCallIds:
204
- * "strict9"`; see `src/openai-request.ts` for the 400 that states the rule).
205
- *
206
- * The request-direction sibling of `TOOL_USE_IDS_HEADER`, and the same maxim: an automatic fix is
207
- * acceptable only because it is announced, a count and never an id. Unlike the response-direction
208
- * mint the figure is final BEFORE the request is even sent, so it rides a streamed response's
209
- * headers too.
210
- */
211
64
  export const TOOL_CALL_IDS_HEADER = "x-llm-relay-tool-call-ids";
212
- /**
213
- * The provider's `Retry-After` in milliseconds, or null.
214
- *
215
- * Accepts both RFC 9110 forms — delta-seconds and an HTTP-date — because providers use both
216
- * (groq sends seconds, some CDNs in front of a provider send a date). A date in the past, a
217
- * negative delta or an unparseable value yields null rather than 0: "the provider said nothing
218
- * usable" and "the provider said retry immediately" call for different cooldowns, and treating
219
- * garbage as 0 would silently disable the backoff this exists to honour.
220
- */
221
65
  export function parseRetryAfterMs(value, now = Date.now()) {
222
66
  if (typeof value !== "string")
223
67
  return null;
224
68
  const raw = value.trim();
225
69
  if (raw.length === 0)
226
70
  return null;
227
- // delta-seconds — integer per the RFC, but providers do emit fractions ("20.45").
228
71
  if (/^\d+(\.\d+)?$/.test(raw)) {
229
72
  const ms = Math.round(Number(raw) * 1000);
230
73
  return Number.isFinite(ms) && ms >= 0 ? ms : null;
@@ -235,45 +78,26 @@ export function parseRetryAfterMs(value, now = Date.now()) {
235
78
  const delta = at - now;
236
79
  return delta > 0 ? delta : null;
237
80
  }
238
- // Response provenance is private process state, not a wire header: callers can
239
- // read it for logging without accidentally forwarding it to the client.
240
81
  const upstreamResponseMetadata = new WeakMap();
241
82
  const postHeaderBodyFailures = new WeakMap();
242
83
  function attachPostHeaderBodyFailure(response, cause) {
243
84
  postHeaderBodyFailures.set(response, { kind: "post-header-body-failure", cause });
244
85
  return response;
245
86
  }
246
- /** Read adapter-private post-header body failure metadata. */
247
87
  export function postHeaderBodyFailure(response) {
248
88
  return postHeaderBodyFailures.get(response);
249
89
  }
250
- /** Raw model id stated by the upstream response, before any relay translation. */
251
90
  export function upstreamReportedModel(response) {
252
91
  return upstreamResponseMetadata.get(response)?.reportedModel;
253
92
  }
254
- /**
255
- * How many `tool_use` ids this response had minted, or `undefined` when none were.
256
- *
257
- * On a streamed response the figure is final only once the body has drained — read it where the
258
- * server reports end-of-stream facts, not before it writes headers.
259
- */
260
93
  export function toolUseIdRewrites(response) {
261
94
  const n = upstreamResponseMetadata.get(response)?.toolUseIdRewrites;
262
95
  return n !== undefined && n > 0 ? n : undefined;
263
96
  }
264
- /**
265
- * How many OUTBOUND tool-call ids this request had rewritten to the provider's stated shape, or
266
- * `undefined` when none were. Final at request-mapping time, so it is readable as soon as the
267
- * Response exists.
268
- */
269
97
  export function toolCallIdRewrites(response) {
270
98
  const n = upstreamResponseMetadata.get(response)?.toolCallIdRewrites;
271
99
  return n !== undefined && n > 0 ? n : undefined;
272
100
  }
273
- /**
274
- * How many replayed tool calls this request carried gemini's thought-signature sentinel on, or
275
- * `undefined` when none did. Final at request-mapping time, like `toolCallIdRewrites`.
276
- */
277
101
  export function thoughtSignatureSentinels(response) {
278
102
  const n = upstreamResponseMetadata.get(response)?.thoughtSignatureSentinels;
279
103
  return n !== undefined && n > 0 ? n : undefined;
@@ -287,22 +111,9 @@ const ANTHROPIC_STREAM_EVENT_FIELDS = new Map([
287
111
  ["message_start", "message"], ["content_block_start", "content_block"],
288
112
  ["content_block_delta", "delta"], ["message_delta", "delta"], ["error", "error"],
289
113
  ]);
290
- /**
291
- * Validate only the protocol structure the response mappers rely on. Optional identifiers,
292
- * model names and usage remain optional because several compatible providers legitimately omit
293
- * them; the required response discriminator must not be optional, or `{}` becomes a successful
294
- * empty assistant message.
295
- */
296
114
  function invalidEnvelopeReason(value, protocol, streamed) {
297
115
  if (!isRecord(value))
298
116
  return "expected a JSON object";
299
- // An error envelope seen here under `streamed` is the FIRST event of a 2xx stream — preflight
300
- // inspects nothing later, so this cannot fire mid-stream. Pre-commit, an in-band error frame
301
- // is a dead turn, not a response: failing it makes the candidate loop walk on to a member
302
- // that answers, where forwarding it spent the whole pool on one member's error inside a 200
303
- // (adoption review §1.1). An error frame arriving AFTER a valid first event still streams
304
- // through untouched — post-commit, honesty beats replay. The reason carries a bounded excerpt
305
- // of the upstream's own message, same maxim as serving the last candidate's real error.
306
117
  if (streamed && isRecord(value.error)) {
307
118
  const message = typeof value.error.message === "string" ? value.error.message.slice(0, 200) : "";
308
119
  return message
@@ -373,7 +184,6 @@ function captureReportedModel(metadata, value, protocol, streamed) {
373
184
  metadata.reportedModel = envelope.model;
374
185
  }
375
186
  }
376
- /** Inspect the first data event before handing a provider stream to llm-bridge. */
377
187
  async function preflightResponseStream(body, protocol) {
378
188
  const reader = body.getReader();
379
189
  const chunks = [];
@@ -395,9 +205,6 @@ async function preflightResponseStream(body, protocol) {
395
205
  };
396
206
  const replay = () => {
397
207
  let prefixIndex = 0;
398
- // The first valid event can be a ping. Keep observing the untouched raw
399
- // stream while the consumer drains it so a later message_start/chunk can
400
- // still supply the upstream's model before terminal logging.
401
208
  captureCompleteEvents();
402
209
  return { ok: true, metadata, body: new ReadableStream({
403
210
  async pull(controller) {
@@ -465,7 +272,6 @@ async function preflightResponseStream(body, protocol) {
465
272
  captureReportedModel(metadata, JSON.parse(data), protocol, true);
466
273
  }
467
274
  catch {
468
- // Preflight owns envelope validity; this observer owns provenance only.
469
275
  }
470
276
  };
471
277
  while (byteLength <= STREAM_PREFLIGHT_LIMIT) {
@@ -475,8 +281,6 @@ async function preflightResponseStream(body, protocol) {
475
281
  if (next.done) {
476
282
  buffered += decoder.decode();
477
283
  let finalReason = buffered.trim() ? inspectEvent(buffered) : undefined;
478
- // A few compatible providers ignore `stream: true` and return one valid buffered
479
- // completion. Preserve the old adapter behaviour for that genuine envelope.
480
284
  if (finalReason === undefined && buffered.trim()) {
481
285
  try {
482
286
  const parsed = JSON.parse(buffered);
@@ -484,7 +288,6 @@ async function preflightResponseStream(body, protocol) {
484
288
  finalReason = invalidEnvelopeReason(parsed, protocol, false);
485
289
  }
486
290
  catch {
487
- // The SSE-specific reason below remains more useful.
488
291
  }
489
292
  }
490
293
  if (finalReason === null)
@@ -512,14 +315,6 @@ async function preflightResponseStream(body, protocol) {
512
315
  }
513
316
  return fail("no response event within preflight limit");
514
317
  }
515
- /**
516
- * Fetch the resolved provider target and return an ANTHROPIC-shaped `Response`,
517
- * regardless of the backend's native wire format. For kind="anthropic" this is a
518
- * passthrough. For kind="openai" (NIM/vLLM/OpenRouter/Gemini) the request is
519
- * translated Anthropic→OpenAI and the response translated back (streaming via
520
- * llm-bridge's SSE re-encoder, non-streaming via a direct mapper) — so the rest
521
- * of the proxy (validate/repair) always sees Anthropic Messages.
522
- */
523
318
  export async function fetchBackend(attempt, args, fetchFn = fetch) {
524
319
  const invokeFetch = oneShotFetch(fetchFn, args.signal, args.onEgress);
525
320
  const target = attempt.target;
@@ -532,8 +327,6 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
532
327
  const res = args.usage
533
328
  ? observeUsage(native, "anthropic-messages", args.usage, { streamed: nativeStreamed })
534
329
  : native;
535
- // Preserve passthrough bytes, but do not preserve a successful status for a malformed
536
- // Messages envelope. Inspecting a clone leaves the original buffered body byte-exact.
537
330
  const messagesPath = args.path.split("?", 1)[0] === "/v1/messages";
538
331
  if (!res.ok || !messagesPath)
539
332
  return res;
@@ -573,35 +366,19 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
573
366
  captureReportedModel(metadata, body, "anthropic-messages", false);
574
367
  return attachUpstreamMetadata(res, metadata);
575
368
  }
576
- // kind === "openai"
577
- // Documents are transcoded to markdown BEFORE the request mapper, or refused: a `document`
578
- // block has no OpenAI representation, and the pre-mapper behaviour put its whole base64
579
- // payload into the prompt. The pre-pass walks the top level of each turn AND the content of
580
- // each `tool_result`, so a document a tool returned is converted rather than refused.
581
369
  let reqJson = args.reqJson;
582
370
  try {
583
371
  reqJson = await transcodeDocuments(reqJson);
584
372
  }
585
373
  catch (e) {
586
- // Local, both of them: the provider was never asked. Charging these to the provider's
587
- // failure budget fails over to a second provider that would refuse the same document.
588
374
  if (e instanceof DocumentError)
589
375
  return anthropicError(400, `llm-relay: ${e.message}`, "local");
590
376
  return anthropicError(502, `document conversion failed: ${e.message}`, "local");
591
377
  }
592
378
  let openaiBody;
593
- // Outbound id rewrites, from the RESOLVED compat mode on the target — the mapper is handed a
594
- // decision, never a provider name to re-derive one from. `preserve` (everyone but mistral)
595
- // leaves this 0 and the outbound bytes untouched.
596
379
  let toolCallIdsRewritten = 0;
597
- // The sibling pass, resolved the same way: `none` (everyone but Google's Generative Language
598
- // API) leaves this 0 and adds nothing to the body.
599
380
  let sentinelsStamped = 0;
600
381
  try {
601
- // The REQUEST direction is relay-owned (`openai-request.ts`); only the RESPONSE direction is
602
- // still llm-bridge's. llm-bridge's `universalToOpenAI` has no case for a tool_call/tool_result
603
- // block, so it stringified its own IR envelope into the outbound prompt — see
604
- // docs/tool-call-dialect-leak.md §"Second mechanism".
605
382
  openaiBody = anthropicRequestToOpenAi(reqJson, {
606
383
  model: target.model,
607
384
  stream: args.wantsStream,
@@ -612,15 +389,10 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
612
389
  });
613
390
  }
614
391
  catch (e) {
615
- // A block we will not put on the wire is the caller's request being unrepresentable, not a
616
- // provider failure — same clean local 400 as an unconvertible document.
617
392
  if (e instanceof RequestMappingError)
618
393
  return anthropicError(400, `llm-relay: ${e.message}`, "local");
619
394
  return anthropicError(502, `request translation failed: ${e.message}`, "local");
620
395
  }
621
- // OpenAI-compatible backends omit usage from streamed responses unless asked. Without
622
- // this the translated `message_delta` reports output_tokens: 0 and anything metering
623
- // off the stream undercounts. Not universally supported — see the 400 retry below.
624
396
  if (args.wantsStream)
625
397
  openaiBody.stream_options = { include_usage: true };
626
398
  const post = (body) => invokeFetch(target.base + "/chat/completions", {
@@ -630,8 +402,6 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
630
402
  signal: args.signal,
631
403
  });
632
404
  let res = await post(openaiBody);
633
- // A backend that doesn't know `stream_options` rejects the whole request (400/422).
634
- // Drop the hint and retry once rather than failing a request over telemetry.
635
405
  if (!res.ok && openaiBody.stream_options && (res.status === 400 || res.status === 422)) {
636
406
  await res.body?.cancel().catch(() => { });
637
407
  const { stream_options: _omit, ...withoutUsage } = openaiBody;
@@ -649,16 +419,9 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
649
419
  catch (cause) {
650
420
  return attachPostHeaderBodyFailure(res, cause);
651
421
  }
652
- // A 404 here is nearly always the model id, not the route — and a provider's
653
- // /models catalog is not proof: several ids NIM lists return 404 from
654
- // /chat/completions. Say so, or this reads as a proxy bug.
655
422
  const hint = res.status === 404
656
423
  ? ` — model "${target.model}" is not served by provider "${target.provider}" (a model can be listed in /models and still 404 here)`
657
424
  : "";
658
- // The provider really answered with this status — the body is reworded, the origin is not.
659
- // `Retry-After` is carried onto the synthesized error: this response is a NEW Response, so
660
- // without this the one header stating when the provider will serve again was destroyed here,
661
- // and neither the breaker's cooldown nor the client's backoff could ever honour it.
662
425
  return anthropicError(res.status, `openai backend HTTP ${res.status}${hint}: ${body.slice(0, 300)}`, "upstream", {
663
426
  ...retryAfterHeader(res.headers),
664
427
  });
@@ -677,28 +440,12 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
677
440
  }
678
441
  try {
679
442
  const anthStream = handleUniversalStreamRequest(preflight.body, "openai", "anthropic");
680
- // Strip a complete message-opening think block BEFORE dialect scanning. Otherwise the
681
- // preamble can hide a tool envelope from recovery. Native Anthropic streams never enter
682
- // this branch, and direct OpenAI Chat passthrough is handled on the other front.
683
443
  const strippedStream = stripThinkTagsInStream(anthStream);
684
- // Recover a tool call this host returned as raw dialect TEXT. Gated on the request actually
685
- // declaring tools: with none declared there is no call to recover, and wrapping the stream
686
- // would add holdback latency for nothing.
687
444
  const schemas = toolSchemaMap(args.reqJson);
688
445
  const refusalSignal = dialectRefusalSignal();
689
446
  const recovered = schemas.size > 0
690
447
  ? recoverDialectInStream(strippedStream, schemas, args.isDestructive, refusalSignal)
691
448
  : strippedStream;
692
- // AFTER recovery, so a call the relay reconstructed is covered too, and BEFORE anything that
693
- // watches for the first `tool_use` — validation and repair must see the ids the client will.
694
- // The taken-set is a thunk: a response with no tool call never walks the conversation.
695
- // ⚠ Deliberately NOT gated on `schemas.size > 0` like the line above, and the asymmetry is
696
- // the point: recovery needs a schema to parse a call out of text, uniqueness needs only an
697
- // id to exist. A host may emit `tool_calls` a request never declared — Claude Code's compact
698
- // turn declares none while the conversation it summarizes is full of tool_use ids — and that
699
- // is exactly the turn the normalizer would empty to `[Tool use interrupted]`. The framing
700
- // holdback is already paid by `stripThinkTagsInStream` above, and an event with no
701
- // `tool_use` substring costs one scan, so the gate would buy little and lose that case.
702
449
  const metadata = preflight.metadata;
703
450
  if (toolCallIdsRewritten > 0)
704
451
  metadata.toolCallIdRewrites = toolCallIdsRewritten;
@@ -709,8 +456,6 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
709
456
  status: res.status,
710
457
  headers: {
711
458
  "content-type": "text/event-stream",
712
- // Unlike the response-direction mint, this count is a REQUEST fact and was final
713
- // before a byte was sent — so a stream can announce it honestly.
714
459
  ...(toolCallIdsRewritten > 0 ? { [TOOL_CALL_IDS_HEADER]: `${toolCallIdsRewritten} rewritten` } : {}),
715
460
  },
716
461
  }), metadata);
@@ -745,31 +490,16 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
745
490
  }
746
491
  catch (e) {
747
492
  if (e instanceof DialectUnparseableError) {
748
- // NOT a mapper defect — the translation is fine and the HOST returned an unusable body. It
749
- // counts against this deployment so the breaker sees it and the pool fails over to a host
750
- // that parses its models' dialect. 502 is retriable, which is what drives the walk.
751
493
  return anthropicError(502, `llm-relay: ${e.message}`, "upstream", {}, "tool_dialect_unparseable");
752
494
  }
753
495
  if (e instanceof DialectDestructiveError) {
754
- // "local" is the load-bearing part: it makes `localFailure` true in the walk, so the request
755
- // is NOT rerolled onto another candidate and the deployment is not blamed. A refusal is a
756
- // config decision, not a health signal — the same line the hard cap draws.
757
496
  return anthropicError(502, `llm-relay: ${e.message}`, "local", { [TOOL_DIALECT_HEADER]: "refused-destructive" }, DIALECT_REFUSED_DESTRUCTIVE_CODE);
758
497
  }
759
- // The provider returned a valid source envelope, so a failure after this point belongs to
760
- // the relay mapper rather than the provider or its failure budget.
761
498
  return anthropicError(502, `llm-relay: response translation failed: ${e.message}`, "local", {}, "relay_mapper_defect");
762
499
  }
763
- // A host that reuses a tool-call id across turns makes Claude Code drop the call while building
764
- // its next request; mint a fresh one where it collides. The pass runs on the TRANSLATED message,
765
- // so a dialect-recovered call is covered too and keeps its `tu_recovered_*` id unless that id is
766
- // itself already in the conversation. See `src/tool-use-ids.ts`.
767
500
  const mintedIds = mintUniqueToolUseIds(anthropicJson, args.reqJson);
768
501
  if (mintedIds.message)
769
502
  anthropicJson = mintedIds.message;
770
- // Announce a recovered call for the same reason `x-llm-relay-degraded` is announced: a response
771
- // whose tool call the RELAY reconstructed is not the same as one that arrived correct, and an
772
- // unflagged reconstruction is indistinguishable from a host that worked.
773
503
  const recoveredDialect = recoveredDialectOf(anthropicJson);
774
504
  const response = new Response(JSON.stringify(anthropicJson), {
775
505
  status: 200,
@@ -790,18 +520,10 @@ export async function fetchBackend(attempt, args, fetchFn = fetch) {
790
520
  metadata.toolUseIdRewrites = mintedIds.rewritten;
791
521
  if (toolCallIdsRewritten > 0)
792
522
  metadata.toolCallIdRewrites = toolCallIdsRewritten;
793
- // No header for this one — see `UpstreamResponseMetadata.thoughtSignatureSentinels`.
794
523
  if (sentinelsStamped > 0)
795
524
  metadata.thoughtSignatureSentinels = sentinelsStamped;
796
525
  return attachUpstreamMetadata(response, metadata);
797
526
  }
798
- /**
799
- * Give a translated message's `tool_use` blocks ids the conversation has not already used.
800
- *
801
- * Lazy on purpose: a response with no `tool_use` block never walks the request's messages, so the
802
- * ordinary text completion — the overwhelming majority of traffic — pays one array scan. `message`
803
- * is null when nothing changed, so an untouched response is never re-built.
804
- */
805
527
  function mintUniqueToolUseIds(message, reqJson) {
806
528
  const content = message.content;
807
529
  if (!Array.isArray(content) || !content.some((b) => isRecord(b) && b.type === "tool_use")) {
@@ -812,31 +534,18 @@ function mintUniqueToolUseIds(message, reqJson) {
812
534
  return { message: null, rewritten: 0 };
813
535
  return { message: { ...message, content: out.content }, rewritten: out.rewritten };
814
536
  }
815
- /** Did this translated message carry a tool call the relay reconstructed from text? */
816
537
  function recoveredDialectOf(message) {
817
538
  const content = message.content ?? [];
818
539
  return content.some((b) => b.type === "tool_use" && typeof b.id === "string" && b.id.startsWith("tu_recovered_"))
819
540
  ? "recovered"
820
541
  : null;
821
542
  }
822
- /**
823
- * Map a non-streaming OpenAI chat completion into an Anthropic message.
824
- *
825
- * `schemas` enables recovery of a tool call the HOST failed to parse: some free hosts return the
826
- * model's native tool-call dialect as assistant TEXT instead of populating `tool_calls`, which
827
- * without this reaches the client as markup it treats as a final answer (see
828
- * docs/tool-call-dialect-leak.md). Omitting it keeps the pure translation behaviour.
829
- */
830
543
  export function openAiResponseToAnthropic(j, model, schemas, isDestructive) {
831
544
  const choice = j.choices?.[0] ?? {};
832
545
  const msg = choice.message ?? {};
833
546
  const content = [];
834
547
  let toolCalls = msg.tool_calls ?? [];
835
- // This mapper is the translated OpenAI→Anthropic seam. Strip first so an opening think block
836
- // cannot conceal a following dialect envelope from the recovery pass below.
837
548
  const messageText = typeof msg.content === "string" ? stripOpeningThinkTag(msg.content) : null;
838
- // Recover ONLY when the host parsed nothing. A host that populated `tool_calls` has already
839
- // spoken; second-guessing it here would be inference, not translation.
840
549
  let recovered = [];
841
550
  if (toolCalls.length === 0 && messageText !== null && messageText.length > 0) {
842
551
  const out = recoverToolCalls(messageText, schemas ?? new Map(), isDestructive);
@@ -846,15 +555,9 @@ export function openAiResponseToAnthropic(j, model, schemas, isDestructive) {
846
555
  content.push({ type: "text", text: out.text });
847
556
  }
848
557
  else if (out.status === "refused-destructive") {
849
- // The relay reconstructed a destructive call out of prose. Refuse it whole rather than
850
- // commit it — "refused, never fabricated". Terminal, not retriable: see the error class.
851
558
  throw new DialectDestructiveError(out.dialect, out.refused);
852
559
  }
853
560
  else if (out.status === "detected") {
854
- // Framing present, nothing parseable — a truncated or unmodelled envelope. Fail clean so
855
- // failover reaches a host that parses, exactly as repair fails clean on an unrepairable
856
- // call. Returning the fragment would hand the client a "final answer" that is really the
857
- // tail of a broken tool call, which is the misdiagnosis this whole path exists to prevent.
858
561
  throw new DialectUnparseableError(out.dialect);
859
562
  }
860
563
  else {
@@ -902,7 +605,6 @@ export function openAiResponseToAnthropic(j, model, schemas, isDestructive) {
902
605
  ...(usage ? { usage } : {}),
903
606
  };
904
607
  }
905
- /** The upstream's `Retry-After`, as a header object to spread, or `{}` when it sent none. */
906
608
  function retryAfterHeader(hh) {
907
609
  const v = hh.get("retry-after");
908
610
  return v ? { "retry-after": v } : {};
@@ -919,11 +621,6 @@ function openaiError(status, message, origin, type = "invalid_request_error", ex
919
621
  headers: { "content-type": "application/json", [ERROR_ORIGIN_HEADER]: origin, ...extra },
920
622
  });
921
623
  }
922
- /**
923
- * Remove the usage-only Chat SSE event we requested on the caller's behalf. This sits after
924
- * the observer: accounting sees the native bytes immediately, while the client sees its
925
- * original stream contract. It deliberately matches only an empty-choices event with usage.
926
- */
927
624
  function suppressRelayAddedOpenAiUsageFrames(body) {
928
625
  let pending = new Uint8Array(0);
929
626
  const append = (left, right) => {
@@ -944,7 +641,6 @@ function suppressRelayAddedOpenAiUsageFrames(body) {
944
641
  const isUsageOnly = (frame) => {
945
642
  let event;
946
643
  try {
947
- // Fatal decoding means opaque/noncanonical bytes pass through untouched.
948
644
  event = new TextDecoder("utf-8", { fatal: true }).decode(frame);
949
645
  }
950
646
  catch {
@@ -976,8 +672,6 @@ function suppressRelayAddedOpenAiUsageFrames(body) {
976
672
  if (!isUsageOnly(frame))
977
673
  controller.enqueue(frame);
978
674
  }
979
- // Do not let a malformed provider frame become unbounded buffering. Retained bytes are
980
- // always original slices, never decoder/re-encoder output.
981
675
  if ((final || pending.byteLength > 16 * 1024) && pending.byteLength > 0) {
982
676
  controller.enqueue(pending);
983
677
  pending = new Uint8Array(0);
@@ -1001,13 +695,6 @@ function nativeResponseIsStreamed(response, wantsStream) {
1001
695
  return false;
1002
696
  return wantsStream;
1003
697
  }
1004
- /**
1005
- * Turn an Anthropic Message response into the response envelope expected by an OpenAI client.
1006
- *
1007
- * This is deliberately separate from llm-bridge's request translation. Provider request bodies
1008
- * and provider response bodies are different contracts, and treating a response as a request
1009
- * loses tool calls, stop reasons and usage on the way back to the caller.
1010
- */
1011
698
  export function anthropicMessageToOpenAi(body, protocol, fallbackModel = "") {
1012
699
  const content = Array.isArray(body.content) ? body.content : [];
1013
700
  const textParts = [];
@@ -1090,27 +777,9 @@ export function anthropicMessageToOpenAi(body, protocol, fallbackModel = "") {
1090
777
  out.usage = withOpenAiTotal(usage);
1091
778
  return out;
1092
779
  }
1093
- /**
1094
- * A cache figure this relay is willing to repeat downstream: a finite, non-negative number.
1095
- * Negative or non-finite is malformed host output, not a measurement, so it is treated exactly
1096
- * like an absent field — propagating it would shrink a prompt total or publish a negative
1097
- * `cached_tokens`, i.e. manufacture a figure nobody stated.
1098
- */
1099
780
  function measuredCacheTokens(v) {
1100
781
  return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : undefined;
1101
782
  }
1102
- /**
1103
- * Anthropic→OpenAI prompt-side mapping (used when an anthropic-kind backend answers a
1104
- * Chat/Responses front-door request).
1105
- *
1106
- * The two protocols disagree on what "prompt tokens" means: OpenAI's `prompt_tokens` INCLUDES
1107
- * the cached subset; Anthropic's `input_tokens` EXCLUDES cache reads and cache writes, which
1108
- * ride in their own fields. Mapping input_tokens straight across therefore UNDER-STATES the
1109
- * prompt by exactly the cache traffic, so it is summed back here. `cache_read_input_tokens`
1110
- * additionally becomes `prompt_tokens_details.cached_tokens` — the field an OpenAI client
1111
- * reads to know how much of the prompt was served from cache — and each output field is
1112
- * emitted only when its source was actually reported, never zero-filled.
1113
- */
1114
783
  function openAiUsage(raw) {
1115
784
  if (typeof raw !== "object" || raw === null)
1116
785
  return null;
@@ -1119,39 +788,20 @@ function openAiUsage(raw) {
1119
788
  const cacheCreation = measuredCacheTokens(usage.cache_creation_input_tokens);
1120
789
  const cacheSum = (cacheRead ?? 0) + (cacheCreation ?? 0);
1121
790
  const input = typeof usage.input_tokens === "number" ? usage.input_tokens : undefined;
1122
- // A usage object with no numeric fields is not a measurement. Keep the relay's unknown-vs-zero
1123
- // convention instead of manufacturing a cost report for an upstream that omitted usage.
1124
791
  if (input === undefined && typeof usage.output_tokens !== "number")
1125
792
  return null;
1126
793
  const out = {
1127
794
  ...(input !== undefined ? { prompt_tokens: input + cacheSum } : {}),
1128
795
  ...(typeof usage.output_tokens === "number" ? { completion_tokens: usage.output_tokens } : {}),
1129
796
  };
1130
- // The details object only when a cache read was actually reported — `{cached_tokens: 0}`
1131
- // would claim the backend stated something about caching that it did not.
1132
797
  if (cacheRead !== undefined) {
1133
798
  out.prompt_tokens_details = { cached_tokens: cacheRead };
1134
799
  }
1135
800
  return out;
1136
801
  }
1137
- /**
1138
- * OpenAI→Anthropic prompt-side mapping (the translated seam of `openAiResponseToAnthropic`).
1139
- *
1140
- * Mirror of `openAiUsage`: subtract the cached subset back OUT of `prompt_tokens`, because
1141
- * Anthropic's `input_tokens` must exclude it or every downstream reader of the Anthropic shape
1142
- * (validator, repair envelope re-attachment, SSE re-emission) sees a double-counted figure.
1143
- * Only mapped when the host actually reported `prompt_tokens_details.cached_tokens`, as a
1144
- * finite non-negative number; anything else (`cached_tokens > prompt_tokens`, negative,
1145
- * non-finite) is malformed host output rather than evidence, so the prompt figure passes
1146
- * through unchanged and the cache field is dropped — an unmeasurable split is not evidence
1147
- * either way, and inventing one would be worse than none.
1148
- */
1149
802
  function openAiPromptUsageToAnthropic(rawUsage) {
1150
803
  if (typeof rawUsage.prompt_tokens !== "number")
1151
804
  return {};
1152
- // Same trust rule as `measuredCacheTokens`, applied to the split direction: a negative or
1153
- // non-finite `cached_tokens` would make `input_tokens` EXCEED the prompt the host stated, so
1154
- // it is malformed exactly like `cached > prompt_tokens`.
1155
805
  const cached = (() => {
1156
806
  const details = rawUsage.prompt_tokens_details;
1157
807
  if (typeof details !== "object" || details === null)
@@ -1181,7 +831,6 @@ function openAiFinishReason(stopReason, hasToolCalls) {
1181
831
  return "content_filter";
1182
832
  return "stop";
1183
833
  }
1184
- /** Map an Anthropic error envelope to a client-readable OpenAI error envelope. */
1185
834
  function anthropicErrorToOpenAi(body, status) {
1186
835
  try {
1187
836
  const parsed = JSON.parse(body);
@@ -1198,30 +847,9 @@ function anthropicErrorToOpenAi(body, status) {
1198
847
  }
1199
848
  }
1200
849
  catch {
1201
- // Fall through to the normalizer, which preserves a useful bounded text message.
1202
850
  }
1203
851
  return normalizeOpenAiErrorBody(body, status) ?? body;
1204
852
  }
1205
- /**
1206
- * Coerce an upstream error body into the OpenAI error envelope — WITHOUT rewriting one that
1207
- * already conforms.
1208
- *
1209
- * The OpenAI front promises "OpenAI in, OpenAI out", but on the error path it returned whatever
1210
- * shape the provider chose. Gemini wraps its error in a JSON ARRAY (`[{"error":{…}}]`); a client
1211
- * reading `response.choices[0]` gets `undefined` from that and reports a malformed completion,
1212
- * so a plain 429 surfaces as "the model returned garbage" — which is exactly how a rate limit
1213
- * cost two days of debugging on the caller's side.
1214
- *
1215
- * Rules, in order:
1216
- * - already `{error:{…}}` → returned BYTE-EXACT. A conforming provider's message, code and
1217
- * type are its own to state, and rewriting them would lose detail.
1218
- * - `[{error:{…}}, …]` → unwrapped to the element. Same fields, now at the top level.
1219
- * - anything else (HTML, text,
1220
- * a bare string, empty) → wrapped, with the original preserved as the message.
1221
- *
1222
- * Returns null when the body is already conforming, so the caller can stream the original bytes
1223
- * rather than re-serialize them.
1224
- */
1225
853
  export function normalizeOpenAiErrorBody(body, status) {
1226
854
  let parsed;
1227
855
  try {
@@ -1233,7 +861,7 @@ export function normalizeOpenAiErrorBody(body, status) {
1233
861
  }
1234
862
  const hasError = (v) => typeof v === "object" && v !== null && typeof v.error === "object" && v.error !== null;
1235
863
  if (hasError(parsed))
1236
- return null; // conforms — do not touch it
864
+ return null;
1237
865
  if (Array.isArray(parsed)) {
1238
866
  const wrapped = parsed.find(hasError);
1239
867
  if (wrapped)
@@ -1254,23 +882,11 @@ function buildTargetHeaders(attempt) {
1254
882
  ...buildAuthHeaders(credential.value, target.authHeader),
1255
883
  };
1256
884
  }
1257
- /**
1258
- * OpenAI-compatible FRONT: an OpenAI Chat Completions or Responses request comes in, its
1259
- * `model` has already been resolved to a provider target by namespace/tier routing.
1260
- *
1261
- * The common case remains a byte-transparent OpenAI→OpenAI Chat Completions proxy. The other
1262
- * combinations use the same Anthropic-shaped internal seam as the Messages front:
1263
- * OpenAI request → Anthropic request → resolved backend → Anthropic response → OpenAI response.
1264
- * That makes an Anthropic passthrough usable from Codex and OpenAI-native IDEs without changing
1265
- * the existing Claude client path.
1266
- */
1267
885
  export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1268
886
  const invokeFetch = oneShotFetch(fetchFn, args.signal, args.onEgress);
1269
887
  const target = attempt.target;
1270
888
  const protocol = args.protocol ?? "chat";
1271
889
  const base = (args.reqJson ?? {});
1272
- // Preserve the existing direct path for the protocol/backend pair that already speaks the
1273
- // same wire format. It keeps provider-specific OpenAI fields byte-for-byte intact.
1274
890
  if (target.kind === "openai" && protocol === "chat") {
1275
891
  const schemas = toolSchemaMap(base);
1276
892
  const callerRequestedUsage = isRecord(base.stream_options) && base.stream_options.include_usage === true;
@@ -1290,8 +906,6 @@ export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1290
906
  signal: args.signal,
1291
907
  });
1292
908
  let res = await post(body);
1293
- // Only a relay-added compatibility hint is safe to remove. A caller-provided option is
1294
- // their request, not relay policy.
1295
909
  if (!res.ok && relayAddedUsage && (res.status === 400 || res.status === 422)) {
1296
910
  await res.body?.cancel().catch(() => { });
1297
911
  res = await post(originalBody);
@@ -1347,9 +961,6 @@ export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1347
961
  return openaiError(502, `llm-relay: ${error instanceof Error ? error.message : String(error)}`, "upstream", "tool_call_recovery_failed");
1348
962
  }
1349
963
  if (recovery.status === "refused-destructive") {
1350
- // "local" keeps this out of the failover walk and off the deployment's failure budget: the
1351
- // envelope parsed fine, the relay refused to commit what it reconstructed. Config, not
1352
- // health — the same line the hard cap draws.
1353
964
  return openaiError(502, `llm-relay: refused a ${recovery.dialect} tool call recovered from text because it names a destructive tool: ${describeRefused(recovery.refused)}`, "local", DIALECT_REFUSED_DESTRUCTIVE_CODE, { [TOOL_DIALECT_HEADER]: "refused-destructive" });
1354
965
  }
1355
966
  if (recovery.status === "detected") {
@@ -1370,12 +981,6 @@ export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1370
981
  }
1371
982
  let anthropicBody;
1372
983
  try {
1373
- // The RESPONSES request direction is relay-owned (`responses-request.ts`), for the same
1374
- // reason the Anthropic→OpenAI direction is: llm-bridge's `openaiResponsesToUniversal` models
1375
- // only `function_call_output`, so an assistant `function_call` was flattened into an empty
1376
- // user turn and an assistant `output_text` reached the backend as a JSON string. The CHAT
1377
- // request direction stays llm-bridge's (`openaiToUniversal` does handle `tool_calls` and
1378
- // `role:"tool"`), as does every response/stream direction.
1379
984
  anthropicBody = protocol === "responses"
1380
985
  ? openaiResponsesRequestToAnthropic(base)
1381
986
  : translateBetweenProviders("openai", "anthropic", base);
@@ -1384,8 +989,6 @@ export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1384
989
  anthropicBody.stream = args.wantsStream;
1385
990
  }
1386
991
  catch (e) {
1387
- // A shape we will not put on the wire is the caller's request being unrepresentable, not a
1388
- // provider failure — the same clean local 400 the Messages front raises.
1389
992
  if (e instanceof RequestMappingError)
1390
993
  return openaiError(400, `llm-relay: ${e.message}`, "local");
1391
994
  return openaiError(400, `llm-relay: request translation failed: ${e.message}`, "local");
@@ -1414,14 +1017,7 @@ export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1414
1017
  catch (cause) {
1415
1018
  return attachPostHeaderBodyFailure(backendRes, cause);
1416
1019
  }
1417
- // Absent or unreadable ⇒ `"upstream"`, deliberately: the provider DID answer, and the relay
1418
- // marks its own errors. `errorOrigin` is the one place the declared members are enumerated,
1419
- // and it is now derived from the type — so a third member cannot be silently rejected here.
1420
1020
  const origin = errorOrigin(backendRes) ?? "upstream";
1421
- // Same rule as the two id-rewrite counters below: rebuilding the response must not swallow the
1422
- // announcement of a decision the relay made one Response ago. A dialect-rescue destructive
1423
- // refusal arrives here as a 502 from `fetchBackend` carrying `refused-destructive`, and a
1424
- // Responses/Messages-front client is entitled to the same marker the Messages front gets.
1425
1021
  const dialect = backendRes.headers.get(TOOL_DIALECT_HEADER);
1426
1022
  const headers = {
1427
1023
  "content-type": "application/json",
@@ -1447,8 +1043,6 @@ export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1447
1043
  const targetProtocol = protocol === "responses" ? "openai-responses" : "openai";
1448
1044
  try {
1449
1045
  const output = handleUniversalStreamRequest(preflight.body, "anthropic", targetProtocol);
1450
- // Same rule as the buffered rebuild below: rebuilding the response must not swallow the
1451
- // announcement of a fix the relay applied one Response ago.
1452
1046
  const rewritten = backendRes.headers.get(TOOL_CALL_IDS_HEADER);
1453
1047
  const response = new Response(output, {
1454
1048
  status: backendRes.status,
@@ -1457,10 +1051,6 @@ export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1457
1051
  ...(rewritten ? { [TOOL_CALL_IDS_HEADER]: rewritten } : {}),
1458
1052
  },
1459
1053
  });
1460
- // Carry the refusal SIGNAL across the rebuild for the same reason as the header above: this
1461
- // Response is the one the server probes, and the object it must consult was registered
1462
- // against the inner Response one hop ago. The signal is the same live object, so a refusal
1463
- // the wrapper marks after this line is still visible.
1464
1054
  const innerRefusal = dialectRefusalSignals.get(backendRes);
1465
1055
  if (innerRefusal)
1466
1056
  dialectRefusalSignals.set(response, innerRefusal);
@@ -1488,12 +1078,7 @@ export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
1488
1078
  : `llm-relay: invalid Anthropic upstream envelope: ${invalidReason}`, generated ? "local" : "upstream", generated ? "relay_mapper_defect" : "invalid_upstream_envelope", retryAfterHeader(backendRes.headers));
1489
1079
  }
1490
1080
  try {
1491
- // This front's TRANSLATED lane runs through `fetchBackend`, so an `openai`-kind target reached
1492
- // from `/v1/responses` inherits the id mint. Rebuilding the body must not swallow the
1493
- // announcement — an automatic fix is acceptable only because it is announced, and the count
1494
- // is as true here as it was one Response ago.
1495
1081
  const minted = backendRes.headers.get(TOOL_USE_IDS_HEADER);
1496
- // …and the outbound-id rewrite the same request mapper applied on the way IN.
1497
1082
  const rewritten = backendRes.headers.get(TOOL_CALL_IDS_HEADER);
1498
1083
  const response = new Response(JSON.stringify(anthropicMessageToOpenAi(body, protocol, target.model ?? String(base.model ?? ""))), {
1499
1084
  status: backendRes.status,