Skip to content

fix(compression): drain the worker pool queue when spawn throws synchronously - #3

Open
insoln wants to merge 790 commits into
release/v3.8.51from
fix/compression-worker-pool-queue-jam
Open

insoln wants to merge 790 commits into
release/v3.8.51from
fix/compression-worker-pool-queue-jam

Conversation

@insoln

@insoln insoln commented Sep 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes #2.

In the Turbopack standalone production build, CompressionWorkerPool jams its job queue on the very first job: spawn() throws a synchronous MODULE_NOT_FOUND (Turbopack rewrites new Worker(resolveWorkerFile()) into its module-context map, which only contains .ts keys while the runtime value is a .js path) before the job is dequeued. The rejected caller's promise is silently fail-opened by strategySelector, but the queued PendingJob — retaining the full parsed request body and its closures — stays in pool.queue forever. Under Claude Code traffic this leaked ~2.5–3 GB of main-isolate heap per hour until the heap-pressure guard began shedding with 503s.

Changes

  1. compressionWorkerPool.ts — the queue can no longer jam. dispatch() wraps spawn() in try/catch; on a synchronous throw it fail-opens every queued job (queue.splice(0) → resolve unchanged) and marks the pool broken, so subsequent run() calls resolve immediately instead of re-jamming. This kills the whole leak class regardless of which bundler/module-loader breaks the spawn.
  2. compressionWorkerPool.ts — test seam. The constructor accepts an optional workerFactory; tests inject a synchronously-throwing factory (exactly the production Turbopack failure shape).
  3. failOpenNotifier.ts + callers — fail-open is no longer silent. Spawn failure, broken-pool short-circuit, strategy-level fallback, and LLMLingua spawn/post failures emit a rate-limited console.warn. Repeated identical details are suppressed for 60 s, while a distinct failure mode is still logged immediately; tracked detail keys are bounded.
  4. engines/llmlingua/worker.ts — ERR_WORKER_PATH. new Worker(pathToFileURL(...).href, …) passed a file:// string, which Node ≥ 21 rejects synchronously. Extracted llmlinguaWorkerSpecifier() returns a URL object.

What this PR deliberately does NOT fix

  • Bundler worker resolution (issue §"Fix worker resolution under Turbopack") and bundling uuid into the esbuild'd worker: those are build-pipeline changes; this PR guarantees they can never again leak memory when they break.
  • The shipped esbuild'd worker's own brokenness (secondary finding in the issue).

Validation

Change type: performance/memory (fail-open semantics unchanged — every failure path still returns { body, compressed: false, stats: null }).

  • TDD: tests/unit/compression/compression-worker-pool-jam.test.ts fails on the untouched release base (jobs hang → timeout) and passes after the fix:
    • every queued job fail-opens when spawn throws synchronously
    • transient spawn failures (EMFILE, ERR_WORKER_INIT_FAILED) are retried on the next wave; structural ones (MODULE_NOT_FOUND etc.) permanently break the pool
    • a populated queue drains when a replacement spawn throws after a worker error
    • postMessage throw, worker error/exit, per-job timeout and close() all settle fail-open — run() never rejects and never strands a promise
    • the fail-open notifier suppresses repeat details but logs a new failure mode immediately
    • broken-pool short-circuits remain observable after the initial spawn failure
    • a broken pool never retries its worker factory
    • the llmlingua worker entry is a URL object, not a file:// string
  • Focused: node --import tsx/esm --test tests/unit/compression/compression-worker-pool-jam.test.ts — 14 passed
  • Adjacent regressions: compression-worker.test.ts, llmlingua-worker.test.ts, llmlingua-worker-resolution.test.ts, llmlingua-failopen.test.ts, compression-worker-file-resolution.test.ts, strategySelector.test.ts — 68 passed
  • node scripts/check/check-mutation-test-coverage.mjs — the new test file is listed in stryker.conf.json tap.testFiles (the remaining drift, quotaScoring.ts, is pre-existing on the release base)
  • npm run typecheck:core
  • Focused Prettier on touched files
  • git diff --check
  • No frozen file-size baselines touched (all four files are unfrozen; check:file-size / check:test-file-size OK)

Tests Added

  • tests/unit/compression/compression-worker-pool-jam.test.ts — the permanent regression guard for the jam/leak class.

@insoln
insoln marked this pull request as ready for review September 4, 2026 16:50
@insoln
insoln changed the base branch from main to release/v3.8.51 September 4, 2026 19:34
@insoln
insoln requested a lite review from Copilot September 5, 2026 00:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

CompressionWorkerPool can be marked permanently broken (and drains queued jobs) on a structural spawn failure even when a healthy worker already exists, which can unnecessarily disable compression despite available capacity.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a production memory leak/jam class in the compression worker path by ensuring CompressionWorkerPool cannot strand queued jobs when worker spawning fails synchronously (e.g., Turbopack standalone MODULE_NOT_FOUND), and adds rate-limited observability for all fail-open paths (including LLMLingua worker failures).

Changes:

  • Make CompressionWorkerPool fail-open and drain queued jobs on synchronous spawn errors, and short-circuit future runs when the pool is broken.
  • Add a shared, rate-limited fail-open notifier used by strategy selection, the pool, and the LLMLingua worker (with sanitization).
  • Fix LLMLingua worker spawning on modern Node by passing a URL object (not a file:// string) to new Worker(...), and add regression tests.
File summaries
File Description
tests/unit/compression/compression-worker-pool-jam.test.ts Adds regression tests covering synchronous spawn throws, queue draining, broken-pool behavior, notifier rate limiting, and LLMLingua worker specifier behavior.
stryker.conf.json Includes the new unit test file in the mutation-test allowlist input set.
open-sse/services/compression/strategySelector.ts Logs (rate-limited) when dynamic import / worker-path execution fails open, rather than silently swallowing.
open-sse/services/compression/failOpenNotifier.ts Introduces the shared rate-limited + sanitized console.warn notifier used across compression worker fail-open paths.
open-sse/services/compression/engines/llmlingua/worker.ts Uses a URL object specifier for Worker entry; adds logging on spawn/postMessage fail-open paths; adds test harness seam.
open-sse/services/compression/compressionWorkerPool.ts Wraps spawn/postMessage failure paths to ensure queue draining and observability; adds test seam for worker construction.
changelog.d/fixes/3-compression-worker-pool-queue-jam.md Documents the fix and related observability/LLMLingua worker specifier adjustment.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The changelog fragment links to a fork (insoln/OmniRoute) instead of the canonical repository link pattern used elsewhere in changelog.d entries.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

changelog.d/fixes/3-compression-worker-pool-queue-jam.md:1

  • The changelog fragment links to the insoln/OmniRoute fork and PR #3, but existing changelog.d entries consistently link to the canonical diegosouzapw/OmniRoute repository. This will produce a broken/misleading reference when merged upstream; please update the link target (and PR/issue number if needed) to match the canonical repo’s URL pattern.
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@insoln

insoln commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Follow-up review addressed in f62ceff: the changelog now references canonical upstream PR cheaper-inference#12755 instead of fork PR #3.

MumuTW and others added 21 commits September 18, 2026 11:31
…cheaper-inference#13408)

* test(coverage): clean stale coverage output before test:coverage runs

* chore(changelog): add fragment for coverage pre-clean
…schemas (cheaper-inference#13561)

Anthropic's Messages API rejects a tool whose `input_schema` carries a
composition keyword at the root with:

  tools.N.custom.input_schema: input_schema does not support oneOf,
  allOf, or anyOf at the top level

The refusal happens before inference, so a single MCP/agent tool carrying
a root-level union fails every request that ships the catalog, and combo
failover cannot recover from it.

Both conversion paths that build a Claude `input_schema` from a client
payload now flatten such a root union into a plain object schema:
object-compatible branches contribute their `properties` (root wins on a
name collision), the root is pinned to `type: "object"`, and only `allOf`
contributes `required` — `anyOf`/`oneOf` branch requirements are
alternatives and promoting them would refuse calls the original schema
accepts. Nested unions are untouched and clean schemas pass through
unchanged.

Closes cheaper-inference#13552
…esolution (cheaper-inference#13564 cheaper-inference#13563) (cheaper-inference#13566)

* fix(sse): release native Codex turn pin when pinned model is model-scoped unusable (cheaper-inference#13564)

Long-running Codex sessions die with 400 NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE whenever
the model pinned to the current turn becomes model-scoped unusable mid-session (per-model
quota lockout, connection cooldown, exhausted accounts). Claude Code has no equivalent
pin and already falls back to the next healthy combo model; Codex now matches.

Release the turn pin when all pinned provider+model targets are model-scoped unusable
and fall through to full combo routing, re-pinning to whichever model succeeds. Preserve
the pin on provider-wide outages (circuit breaker OPEN, provider cooldown) and when the
pinned target is still healthy.

Also prunes the stale ESLint suppression entry for combo.ts that this change orphaned
(createPinnedModelUnavailableResponse import dropped; pre-existing getBootstrapLatencyMs
remains the sole residual unused var).

* fix(api): resolve codex-settings apiKey via canonical resolver instead of 400 (cheaper-inference#13563)

Applying Codex settings from /dashboard/cli-code/codex always failed with
400 "baseUrl, apiKey and model are required" when the dashboard sent an empty
apiKey (cloud mode with no management key selected) — baseUrl and model are
already Zod-gated, so that response could only ever fire on the empty key.

The codex-settings route had diverged from the sibling CLI tools (cline/forge/
openclaw/grok-build/jcode): an inline if(!apiKey) 400 guard plus a hand-rolled
getApiKeyById lookup, instead of the shared resolveApiKey(keyId, apiKey) helper
which resolves by keyId, falls back to the submitted apiKey, then to
sk_omniroute. This change makes codex-settings use the canonical resolver, so:

- empty apiKey + valid keyId -> the real DB key is written to auth.json
- empty apiKey + no keyId   -> sk_omniroute default (config still applies)
- explicit apiKey           -> written verbatim (unchanged)

* docs(changelog): add fragments for Codex turn-pin fallback and codex-settings apiKey resolution
…aper-inference#13622)

* fix(models): keep OpenRouter :batch variants out of chat routing

ModelSync imported OpenRouter's Batch-API-only variants into the chat
catalogue. A chat completion against one is rejected upstream with

  404 This model is only available through the Batch API.
      Use the /api/beta/batches endpoint instead.

which cheaper-inference#13596 measured 91 times in 41 hours, third by volume, plus the
`model not found - locking mode` failover churn behind it.

OpenRouter's /models carries no endpoint metadata that separates a batch
variant from a chat one, so `classifyExplicitEndpoints` cannot decide it
and the rule belongs in `modelEndpointPolicy`, beside the OpenAI
image/video policy and for the same reason: the file exists so discovery,
import and catalog projection agree on one answer.

Matched as the exact `:batch` suffix, not "has a variant suffix" --
`:free`, `:nitro`, `:floor`, `:online`, `:extended` and `:thinking` are
routing hints on the same chat model, and excluding them would silently
shrink the routable catalogue. Applied unconditionally for this provider:
there is no "batch" endpoint name an upstream could declare next to a chat
one, and the already-stored rows carry the synthetic `["chat"]` default
that re-imported them in the first place.

Closes cheaper-inference#13596

* docs(changelog): add fragment for the OpenRouter batch-variant fix
…etime purchases (cheaper-inference#13689)

* fix(providers): sync OpenRouter :free 1000/day tier from /credits lifetime purchases

* chore: rename changelog fragment to PR number 13689
…quest (cheaper-inference#13738)

Google validates every `functionDeclarations[].name` against one grammar and
rejects the WHOLE GenerateContentRequest when any single one is invalid, so six
`1c_*` tools in a 109-tool MCP catalog made every request 400, including
requests that would never call them (cheaper-inference#13715).

`normalizeGeminiToolName` removed invalid characters, collapsed underscores and
stripped leading and trailing ones, none of which touches a leading digit. The
strip is also why a leading underscore was not a workaround: the client's
`_1c_probe` was normalized back to `1c_probe` and rejected for the same reason.

The prefix goes inside the normalizer rather than at the call site, because
that keeps the guarantee on the one value every path reads. `buildHashedGemini
ToolName` builds its name from the normalized string and inherits its first
character, so a fix applied later would hold for short names and fail silently
for the long ones a 109-tool catalog is full of. It also runs before the
collision check, so two names that newly collide are still separated by the
existing hashed path.

The reverse direction needs nothing: the sanitized name now differs from the
client's, so `buildChangedToolNameMap` carries the original and the response
translator restores the client's spelling on the model's functionCall.

Six cells: no declared name starts with a digit, a leading underscore reaches
Google letter-first, the reverse map returns the client's spelling, a
letter-first name is untouched, an over-long digit-first name keeps the
guarantee through the hash path, and two colliding digit-first names stay
distinct. Four mutations, all killed.
…lectors (cheaper-inference#13740)

* fix(api): type compatible-provider-node models in /v1/models by the node's apiType

Model rows discovered from an OpenAI-compatible provider node rarely carry
endpoint metadata — a TEI / Infinity / vLLM `/v1/models` listing is just ids —
and the catalog defaulted such rows to `["chat"]`. An `embeddings`-typed node
exposing `bge-m3` and a `rerank`-typed node exposing `bge-reranker-v2-m3`
therefore both surfaced in GET /v1/models as untyped chat models: clients
that build their picker from `type: "embedding"` / `type: "rerank"` never saw
them, and chat pickers listed models that 400 on chat.

- src/shared/constants/modelSupportedEndpoints.ts: add
  defaultEndpointsForProviderNodeApiType(apiType) — embeddings → ["embeddings"],
  rerank → ["rerank"], audio-* → themselves, images-generations → ["images"],
  chat/responses/unknown → ["chat"] (unchanged default).
- src/app/api/v1/models/catalog.ts: build a node-id → apiType map next to the
  existing node-id → type map; the synced-model and custom-model loops fall
  back to the node's modality instead of ["chat"] when a row has no
  supportedEndpoints; the custom-overlay merge path also classifies
  `type`/`subtype` from the overlay's explicit supportedEndpoints, so a manual
  `["rerank"]` row layered on a discovered chat-default row is re-typed.

Explicit supportedEndpoints on any row still take precedence, and chat /
responses nodes keep the historical behavior.

tests/unit/catalog-provider-node-apitype-endpoints.test.ts covers the helper
and the catalog end-to-end for embeddings, rerank, mixed, chat, and overlay
cases via getUnifiedModelsResponse().

* chore(changelog): name the cheaper-inference#13734 fragment

* refactor(api): keep the provider-node modality helpers out of catalog.ts

catalog.ts is frozen by the file-size gate (must not grow past 2075
lines) and the apiType fallback pushed it to 2093. Move the node
apiType index, the endpoint fallback and the overlay type/subtype
fields into catalogNodeModality.ts so catalog.ts ends one line
shorter than the base; behaviour and tests are unchanged.

* fix(api): give nodeModelEndpoints a string[] return so the catalog classifier typechecks

The API-route typecheck gate flagged TS2345 at both classifyModelSupportedEndpoints()
call sites: the helper returned `ModelSupportedEndpoint[] | unknown[]`, and unknown[]
is not a readonly string[]. The base code only passed because the synced row's
supportedEndpoints was untyped. Same pass-through cast overlayEndpoints() already uses;
no behaviour change.

* fix(memory): list provider-node models in the embedding and rerank selectors

GET /api/memory/embedding-providers and GET /api/memory/rerank-providers
appended local provider nodes by apiType alone and always with models: [].
A node typed "embeddings" that also serves a rerank model — one TEI /
Infinity / vLLM box hosting both bge-m3 and bge-reranker-v2-m3 is the
common self-hosted layout — was filtered out of the Rerank selector
entirely (apiType not in chat/responses/rerank) and showed up in the
Embedding selector as a provider with nothing to pick. Typing prefix/model
by hand worked because the request path resolves it directly; only the
convenience layer was blind.

Add src/lib/memory/embedding/nodeModalityListings.ts, which builds the
listing from the node's synced + custom model rows, typing each row the way
/v1/models does (explicit supportedEndpoints wins, otherwise the node's
apiType via defaultEndpointsForProviderNodeApiType; a custom overlay
re-types a discovered row). A node is listed for a modality when its
apiType matches, when it is a generic chat/responses node (historical
behaviour, kept so catalog-less nodes still appear), or when any of its
rows is typed for the modality. Both endpoints use it; the curated
registries stay first and win on prefix collisions.

* chore(changelog): name the cheaper-inference#13740 fragment
…r-inference#13685) (cheaper-inference#13741)

Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
…inference#13750)

bug_report.yml and feature_request.yml prefilled `[BUG] ` / `[Feature] ` while
CONTRIBUTING.md documents Conventional Commits with a scope list, so reporters
who followed the template verbatim had their title renamed by triage. The
templates now teach the convention instead: `fix(): ` and `feat(): `.

Each form gains one intro line pointing at the commit-message section of
CONTRIBUTING.md, where the scope list lives. `test_coverage_task.yml` is left
untouched, as its `[Coverage] ` prefix is not part of the convention.

Refs cheaper-inference#13688
cheaper-inference#13760)

* fix(usage): preserve nested prompt cache reads

* docs(changelog): add nested cache-read cost fix

---------

Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…13809)

Co-authored-by: ginettododo <117327638+ginettododo@users.noreply.github.com>
Co-authored-by: ginettododo <117327638+ginettododo@users.noreply.github.com>
… every PR (cheaper-inference#13814)

* fix(ci): clear the tap.testFiles drift that reds the gate on every PR

check-mutation-test-coverage --strict fails on a pristine checkout of
release/v3.8.51 with no PR diff involved, so the mutation-test-coverage
gate is red on every open PR regardless of what it changes.

Six covering unit tests across four mutated modules were absent from
stryker.conf.json tap.testFiles, which means their mutant kills were not
being counted:

  accountFallback.ts          daily-reset-tz-threading, noauth-model-lockout
  sse/services/auth.ts        free-badge-provider-gate, noauth-model-lockout
  combo/comboPredicates.ts    local-token-budget-429-skips-cooldown
  combo/rrState.ts            daily-reset-tz-threading

Four distinct files — two of them cover two modules each. Inserted into
the alphabetical run, matching the file's existing convention; the list
has a second unsorted appended group that is left alone.

After: "No drift — every covering unit test is listed in tap.testFiles",
exit 0. All four files pass (31 tests) so registering them does not
introduce a failing mutation run.

Noticed while reviewing cheaper-inference#13743, which targets a fifth file that has
already been registered by 25bc16d.

* fix(ci): drop a dangling tap.testFiles entry and guard against new ones

Merging the release line in surfaced that stryker.conf.json still names
tests/unit/plugin-sandbox-permissions.test.ts, which does not exist —
one dangling path out of 428 entries, pre-existing on the base rather
than introduced here.

check-mutation-test-coverage already guards one direction: a test that
covers a mutated module but is missing from tap.testFiles. The other
direction was silent. Stryker resolves the list into its sandbox, so an
entry left behind after its test file is deleted or renamed costs
coverage without failing loudly — the same class of drift this PR is
about, arriving from the opposite side.
…nce#13819)

Co-authored-by: William Echo <175406538+qinghuanandejiangshi@users.noreply.github.com>
…nymously (cheaper-inference#13820)

* feat(security): warn at boot when the inference server is exposed anonymously

`GET /v1/models` follows the dashboard login posture
(`isAuthRequired()` / `requireAuthForModels`) while the inference routes
follow `REQUIRE_API_KEY`. On an instance with an admin password set and
`REQUIRE_API_KEY=false`, `/v1/models` answers 401 while `/v1/responses`
is open to anyone who can reach the port — so the most natural probe an
operator runs reports the opposite of the truth.

cheaper-inference#12568 added a boot warning for exactly this combination, but wired it
only into the API bridge and the live dashboard WebSocket. The Next
server that actually answers `/v1/chat/completions` and `/v1/responses`
never reached it, and it is the one that binds every interface by
default (`process.env.HOST || "0.0.0.0"`).

Wire the existing guard into the Next boot hook, and document the split.

Resolving the bound host needed care: two entrypoints bind that server
and they read different variables. `run-next.mjs` honours `HOST`; the
Docker entrypoint delegates to Next's generated `server.js`, which reads
`HOSTNAME`. `run-next.mjs` now publishes what it actually binds as
`OMNIROUTE_BOUND_HOST`, and the guard reads that, then `HOSTNAME`, then
the shared `0.0.0.0` default. `HOST` is deliberately absent from the
chain: the standalone server ignores it, so consulting it there would
warn about an interface the server is not on — and one false warning
teaches an operator to ignore the next one.

Closes cheaper-inference#13695

* docs(changelog): add changelog.d entry for cheaper-inference#13820
…tation (cheaper-inference#13825)

* fix(providers): refresh uncloseai free-model roster after upstream rotation

hermes.ai.unturf.com rotated its lineup: /v1/models now serves exactly one
model (Lorbus/Qwen3.6-27B-int4-AutoRound, vllm, max_model_len 65536) while
every previously catalogued id (adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic,
qwen3.6:27b, gemma4:31b) returns 404 on /v1/chat/completions. Requests routed
through the static seed failed although the provider itself is healthy — a
live completion against the new id succeeds.

- registry seed: replace the three dead ids with the live one (+contextLength)
- FREE_MODEL_BUDGETS: 3 rows -> 1 (catalog totals 443 -> 441; counts synced in
  README and free-tier-budget.svg)
- noauth authHint: verified-live-model pointer updated; PROVIDER_REFERENCE.md
  regenerated
- regression test pins the live id and forbids the retired ids in both the
  registry seed and the free catalog

Verified live on 2026-09-15 against https://hermes.ai.unturf.com/v1/models
and /v1/chat/completions.

* docs(providers): sync free-tier entry count after uncloseai merge

The uncloseai roster refresh (3 entries -> 1) dropped the live free-tier
catalog total from 491 to 489 once merged with the current release tip.
README.md and free-tier-budget.svg still quoted 491 after the merge;
update both to the real count so check:docs-counts-sync stays green.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: anon <anon@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…e#13837)

Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
Co-authored-by: luwei.will <luwei.will@bytedance.com>
… OpenAI-format providers (cheaper-inference#13855)

Built-in providers that speak the OpenAI Chat wire format skipped
normalizeOpenAICompatibleTools(), which only ran for custom
openai-compatible-* connections. A client tool whose type is not
"function" (a named Claude server tool, a nameless hosted tool) was
forwarded verbatim, and agentrouter's GLM backend rejected the whole
request with 400 tools[0].type:type is illegal.

Extract the gate into shouldNormalizeFunctionToolsOnly(): custom
openai-compatible-* providers keep normalizing on every target, and a
conservative allowlist of built-in providers (agentrouter first)
normalizes on the OpenAI Chat target only. OpenAI itself stays off the
list, so its custom tools pass through untouched.

Closes cheaper-inference#13789
* fix(capabilities): align combo multimodal vision hints

* test(capabilities): cover combo multimodal consistency

* chore(changelog): note combo multimodal capability fix
JxnLexn and others added 29 commits September 22, 2026 07:20
…meters (cheaper-inference#14279)

Gemini/antigravity returned a hard 400 ("Unknown name \"~optional\" ...
Cannot find field") the moment a tool's parameter schema contained a
`~optional` key, taking down every Gemini-family model in a combo's
fallback chain at once -- confirmed live: this exact error killed 100% of
a production combo's usable fallbacks for ~18 hours (the two remaining
candidates ahead of it were OpenCode free-tier models, permanently
inaccessible via API regardless).

Root cause: `~`-prefixed keys are the Standard Schema convention (Zod 4+,
Valibot, ArkType) for internal/vendor metadata, namespaced with a leading
`~` specifically so it can never collide with a real schema property name.
A tool built from one of those libraries leaked a literal `~optional` key
into a property's subschema. GEMINI_UNSUPPORTED_SCHEMA_KEYS already listed
the plain `"optional"` string, but `removeUnsupportedKeywords`'s exact-match
check doesn't catch the tilde-prefixed form, so it survived sanitization
and Gemini's OpenAPI 3.0 schema subset rejected the whole request.

Fix: strip any `~`-prefixed key at every schema level, the same way `x-`
vendor extensions are already stripped -- this covers `~optional` and any
other Standard Schema metadata key the same libraries may emit, rather than
only patching this one literal key.

Testing: new regression test confirmed failing on unpatched code and
passing after the fix; full existing Gemini/schema-stripping test suite
(495 tests) still green.
…soning model aliases (cheaper-inference#14277) (cheaper-inference#14281)

When selecting the first-class `codex-app-server` provider directly through
the executor registry (`open-sse/executors/index.ts`), the lazy loader
instantiated `CodexAppServerExecutor({}, "codex-app-server")` without
supplying a WebSocket transport function, preventing any connection from
opening.

Additionally, `CodexAppServerExecutor.execute` passed the raw `input.model`
directly to `turn/start` without stripping reasoning suffix aliases (e.g.,
`gpt-5.5-medium`), which caused upstream app-server turns to fail because the
aliased name is not recognized as a valid model ID.

Fixes:
1. In `open-sse/executors/index.ts`, wire the shared WebSocket transport
   (`codex.getCodexAppServerWebsocketTransport()`) into the `codex-app-server`
   executor factory.
2. In `open-sse/executors/codex-app-server.ts`, use `splitCodexReasoningSuffix`
   to send `baseModel` to `turn/start` while passing the selected effort
   separately, prioritizing explicit model suffix selection over request body
   defaults (cheaper-inference#2331). Also support `body.reasoning_effort` in `extractEffort`.

Added regression tests in `tests/unit/codex-app-server.test.ts` verifying:
- baseModel extraction and effort derivation from model suffixes on `turn/start`
- precedence of suffix effort over body reasoning effort
- forwarding of body reasoning effort when the model is unsuffixed
- registry executor initialization with injected transport

Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
Co-authored-by: Aref Alapour <aref-alapour@users.noreply.github.com>
…s a projectId (cheaper-inference#14282)

* fix(antigravity): never persist or send the manual-project sentinel as a projectId

ensureAntigravityProjectAssigned() returns __REQUIRES_GCP_PROJECT__ when Google
does not auto-provision a project (BYOP). Only the in-request path guarded it;
token refresh, models discovery and the executor refresh persisted it as the
connection's projectId. The connection then looked configured, was PREFERRED by
preferAntigravityConnectionsWithStoredProject, and every request went upstream
as projects/__REQUIRES_GCP_PROJECT__ (HTTP 400, no lockout, no account
failover), so one poisoned account served nearly all traffic.

Add isUsableAntigravityProjectId() (rejects blank + the sentinel) and use it in
the persist helper, the stored-project selector, token refresh, models
discovery and the executor. A row already poisoned now behaves as "no project":
it takes the typed 422 GCP_PROJECT_REQUIRED path and is dropped by the selector
when a healthy sibling exists.

* chore(changelog): add fragment for cheaper-inference#14282
…r-inference#14284)

* feat(kiro): expose the provider-native Opus 5 Max effort tier

Advertise "<base>-max" for Kiro's claude-opus-5 in the Claude effort catalog,
allow "max" in the Kiro effort values, and enable the adaptive-thinking
envelope for claude-opus-5.

The original change (the effortStandardization / adaptiveThinking edits and
the kiro-opus-5-max-effort test) is by tarciorick. The scoped
KIRO_OPUS_5_MAX_VARIANT_RE guard is an addition: the shared
CLAUDE_EFFORT_SUFFIX_RE must stay byte-identical to its sibling copies (drift
guard in claude-effort-variants.test.ts), so the synthesized -max id is
excluded by a separate regex scoped to that one id.

* chore(changelog): add fragment for cheaper-inference#14284
…e and usage resets (cheaper-inference#14268) (cheaper-inference#14285)

When stacked prompt compression telemetry was added, per-engine breakdown
logs were written to `compression_engine_breakdown`. While the parent
`compression_analytics` table was regularly trimmed on an operator-configurable
retention policy (default 30 days) and purged on usage resets,
`compression_engine_breakdown` was omitted from both `cleanup.ts` and
`RESET_TARGETS`. In long-running deployments (5+ months), this table
accumulated 731,000+ unpruned rows without any deletion path.

Wired `compression_engine_breakdown` into `cleanupCompressionEngineBreakdown()`
with a 30-day cutoff, hooked it into nightly auto-cleanup, and added the
table to `RESET_TARGETS` so manual purges clean it completely.

Fixes cheaper-inference#14268

Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
Co-authored-by: Koosha Paridehpour <KooshaPari@users.noreply.github.com>
…nference#14289)

* fix(providers): fail closed for retired gemini-cli

* docs(changelog): name fragment for PR 14289
…stener is not read as free (cheaper-inference#14307)

The cheaper-inference#4425 port guard waits for the listen port to free up before respawning a
crashed child. It probed only 127.0.0.1, which does not detect the address
`omniroute serve` actually binds by default: 0.0.0.0.

Node sets SO_REUSEADDR on every listener it creates. On macOS/BSD that lets a
specific-address bind coexist with an existing wildcard bind (Linux still
rejects the overlap in LISTEN state), so binding 127.0.0.1:PORT succeeds while
another process holds 0.0.0.0:PORT. The probe therefore reported "free", the
wait resolved immediately, the respawned child failed with EADDRINUSE, and the
crash/restart cascade cheaper-inference#4425 set out to fix continued — indefinitely under a
`KeepAlive` supervisor such as launchd or systemd.

Probe the wildcard and loopback (plus any caller-supplied host) and treat the
port as busy if any of them is occupied.

Verified on macOS 27.0 / Node 22.22.3: with a server holding 0.0.0.0:PORT,
`isPortFree(PORT)` returned true before and now returns false, and a child
rebind of 0.0.0.0:PORT fails with EADDRINUSE as expected.

Tests: tests/unit/supervisor-policy-4425.test.ts

Co-authored-by: Gery.MK Song <gery@macunzip.dev>
…stay eligible (cheaper-inference#14312)

* feat(sse): forward the auto mode classifier beta to Anthropic-format upstreams

* docs(changelog): fragment for the auto mode classifier beta pass-through

* refactor(sse): move the client beta application into anthropicHeaders
…s API (cheaper-inference#14316)

DeepSeek's primary public API is /chat/completions, not /responses. The
previous default caused multi-turn tool-call requests to fail with:

  400 The reasoning_text in the thinking mode must be passed back to the API

because the Responses API requires the caller to echo reasoning_text on every
turn, while the Chat Completions protocol does not.

Changes:
- format: "openai-responses" → "openai"
- baseUrl: .../responses → .../chat/completions
- Move openai-responses to alternateFormats ("Responses-compatible") so
  operators who explicitly need that path can still select it per-connection
- Update unit test assertions to match the new default and alternate count
…ing (cheaper-inference#12474)

* feat(providers): add Lyceum pay-per-use provider + credit quota

Lyceum (lyceum.technology) is an OpenAI-compatible, usage-based inference
provider. Registered as a first-class apikey provider mirroring the
llmgateway/openrouter pattern:

- registry/lyceum: buildOpenAiCompatibleRegistryEntry (base
  https://api.lyceum.technology/openai/v1, live /models discovery,
  passthrough). Generic DefaultExecutor handles chat/embeddings — no
  custom executor or translator needed.
- apikey metadata card + AGGREGATOR_PROVIDER_IDS + PROVIDER_ENDPOINTS.
- lyceumQuotaFetcher: reads the credit balance from
  GET /api/v2/external/billing/credits (pay-per-use), surfaced as a
  "credits" window in Dashboard > Limits and quota-aware preflight.
  Registered via quotaTrackersBatch (keeps chat.ts frozen).
- usage leaf + dispatch + fetcher/supported/apikey-limits lists + label.
- tests: registry-shape (wave1-c) + 16-case quota fetcher/usage suite.
- docs: changelog fragment, regenerated PROVIDER_REFERENCE, provider
  count 354->355 across README/AGENTS/llm.txt (+42 i18n mirrors)/SVGs,
  file-size baseline bump for the gateways.ts catalog entry.

* docs(providers): regenerate provider reference for Lyceum (359→360)

Rebased onto release/v3.8.51 after cheaper-inference#12462 (LLM Gateway DevPass quota) merged
and the base advanced. The live registry now totals 360 providers, so the
auto-generated reference and the diagram/marketing counts are regenerated
against the new base:

- npm run gen:provider-reference → docs/reference/PROVIDER_REFERENCE.md
- 359→360 in README.md, AGENTS.md, llm.txt (provider count), package.json
  description and the diagram SVGs (readme-hero, promise-pillars,
  comparison-table, cli-terminal, tier-flow-dark/light)
- node scripts/i18n/sync-llm-mirrors.mjs → 65 locale mirrors

Addresses the maintainer review asking to rerun gen-provider-reference.ts and
push the doc files missing from the original diff, which left Docs Gates red.

* test(providers): refresh count assertions + golden snapshot for the new Lyceum provider
…C-741) (cheaper-inference#13610)

* feat(db): per-request cost ledger + per-key tpm/rpm/monthly quota (RIC-741)

Add M3 cost transparency + team autonomy:
- request_cost_ledger: one row per completed call with provider/model/token/
  unit-price/amount breakdown, written from the existing recordCost paths.
- api_key_quota_limits + api_key_quota_counters: KISS counter+threshold quota
  for tpm (tokens/minute) and rpm (requests/minute) via 2-bucket sliding
  window; monthly USD cap reads from the ledger month SUM.
- checkKeyQuota/recordKeyQuotaUsage domain gate (fail-open B16/B29), wired
  into enforceApiKeyPolicy pre-request and the usage-record post hooks.
- /api/usage/key-quota route + setKeyQuotaSchema for per-key config.
- Migration 177; tests cover ledger traceability and tpm/rpm/monthly break.

* fix(db): renumber cost-ledger/key-quota migration to 180

177 and 178 already landed on release/v3.8.51 by the time this branch was
analyzed (177_provider_connection_synced_models_at.sql,
178_memory_fts_au_conditional.sql); 179 also landed since. Renumber to the
next free slot and fix the file's own internal comment to match.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(db): use the canonical toNumber helper instead of local redefinitions

ESLint no-restricted-syntax bars new local toNumber definitions in favor of
@/shared/utils/numeric (cheaper-inference#7879, DRY extraction) — replace the 3 near-identical
local copies in costLedger.ts, keyQuota.ts and costLedgerRecorder.ts. Also
wires keyQuota.ts's getKeyQuotaStatus to reuse the already-defined
toIsoWindowStart helper instead of duplicating the window-start math inline,
which fixes the unused-var lint error on that function.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(sse): keep chatCore.ts under its frozen file-size ratchet

The recordCost() wiring for the per-request cost ledger added ~24 lines to
chatCore.ts, an already-frozen file (file-size-baseline.json caps it at
6146 lines). Extract the shared provider/model/tokens/serviceTier/requestId
breakdown into buildCostCtx() and the apiKeyInfo?.id && estimatedCost > 0
guard into recordChatCallCost() (both in src/domain/costRules.ts), and the
streaming ledger-details object into buildStreamLedgerDetails() in
streamingCost.ts. No behavior change: same 18 cost-ledger/domain-cost-rules
tests pass unmodified; net effect is chatCore.ts now at 6143 lines (3 under
the frozen ceiling) instead of 6165.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs: bump the migration count to 178 after the new cost-ledger/key-quota migration

check:docs-all (stale-migrations, STRICT) failed because README.md, AGENTS.md
and llm.txt (plus its 65 docs/i18n/*/llm.txt mirrors, which must be exact
body copies of the root file) still said "177 migrations" after this PR
added migration 181_request_cost_ledger_and_key_quota.sql, bringing the
real count to 178.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Ant Rich <ant@richants.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
The release tip went red on check:file-size after the 2026-09-22 merge wave:
cheaper-inference#14179 grew open-sse/executors/opencode.ts past its frozen ceiling and the
PR->release fast-gates do not run check:file-size, so every merge-train
boarding afterwards inherits the red. Absorbed once at the tip under the
owner-approved train-rebaseline policy. Measured clean: check:file-size passes
on the tip with this entry.
…eneration footers (cheaper-inference#14441)

Hard Rule cheaper-inference#16 had no automated enforcement: eight contributor commits reached release/v3.8.51 carrying `Co-Authored-By: Claude …`, `Codex <codex@openai.com>` and `Claude-Session:` trailers through squash-merge bodies (cheaper-inference#14436).

The gate runs in three places: the husky commit-msg hook (local), the quality.yml fast-gates loop (PR→release/**) and a PR-only lint step in ci.yml (PR→main). It reads the event payload on PRs and no-ops elsewhere. Human co-authors are explicitly allowed — only AI/bot names, AI-owned e-mail domains and AI-generation footers are rejected.

Refs cheaper-inference#14436.
…heaper-inference#14356)

* feat(dashboard): let synced models carry a context-window override

Models imported through a provider's /models import render as "imported" rows
with no edit affordance, so the manual context-window override cheaper-inference#4125 added for
custom models was unreachable for exactly the models that need it most: third
-party OpenAI-compatible aggregators whose discovery metadata understates
context length. The GUI offered no correction path at all.

The backend already accepted the write. `PUT /api/provider-models` lists
`contextWindowOverride` in its compatOnly branch, which is the path taken when
no `customModels` row exists, and persists it through `setModelContextOverride`
with `source="manual"` — the source the 24h reconciler never overwrites.

The read path was the missing half. `GET /api/provider-models` attached
`contextWindowOverride` only to custom-model rows, so a value stored against a
synced model could be written and never read back: the UI had nothing to show
and nothing to seed an editor with. The GET now returns the provider's
overrides directly, and the row renders the same badge and inline editor as a
custom row.

cheaper-inference#4125 semantics are unchanged and now shared rather than duplicated: the parse
helper moves to providerPageHelpers so "blank clears" and "zero is invalid"
have one definition instead of two that can drift.

Both new props are optional and the affordance renders only when the section
supplies a handler, so a row that cannot take an override is byte-identical to
before. Invalid input keeps the editor open and reports, rather than silently
writing a wrong value or discarding the one typed.

No new i18n keys: the contextWindowOverride* strings already ship in all 66
locales for the custom-model form.

Mutation-tested 4/4 — rendering the pencil unconditionally, treating blank as
invalid, accepting zero, and closing the editor on invalid input each fail the
new tests. tests/unit/ui is 1458 pass / 94 fail against 1452 / 94 on a stashed
tree: the same failures, plus my six.

Does not implement the max-output-tokens half of the issue: unlike
contextWindowOverride, no capability key is in the PUT compatOnly allowlist, so
that needs its own backend change and belongs in its own PR.

Closes cheaper-inference#14337

* fix(dashboard): drop the invalid Badge variant on the override badge

`check:dashboard-typecheck` caught a real one: `Badge` has no `secondary`
variant, so the override badge raised TS2322 under
tsconfig.typecheck-dashboard.json.

My local `tsc --noEmit -p tsconfig.json` never saw it — that config does not
cover these files the same way, which is the whole reason the dashboard gate
exists as a separate scoped check with a frozen baseline.

Rendered as the same styled span CustomModelsSection already uses for this exact
badge, rather than picking another variant: same visual treatment as the custom
-model 🪟 badge, and one fewer component contract to get wrong.

With this, the dashboard gate reports the same 5 pre-existing errors on this
branch as on release/v3.8.51 — verified by running the check in a worktree on
the base — so the branch adds none.
…nference#14412)

Detected by a full-repo Grounded (reference-integrity) audit and confirmed
with targeted tsc probes against the root tsconfig.json paths:

- src/lib/db/discovery.ts: logger import "../../open-sse/utils/logger"
  cannot resolve from src/lib/db/ (TS2307); use the @omniroute/open-sse
  alias like the sibling DB modules. Masked in CI because typecheck:core
  compiles an empty include set.
- scripts/ad-hoc/regen-opencode-config.ts: import and Usage header still
  pointed at the pre-move root scripts/ location (TS2307); fixed both so
  the script runs exactly as documented.
- tests/unit/combo/recovery-hint.test.ts: import renamed the ghost type
  RecoveryAction -> ComboRecoveryAction (TS2305) and rebuilt the
  emptyDiag() fixture on the current ComboDiagnostics shape (post-cheaper-inference#12659);
  all 10 behavioral assertions unchanged.

Validation: node --import tsx/esm --test tests/unit/combo/recovery-hint.test.ts
(11/11 pass), scoped tsc probe (0 errors), eslint clean on changed files.

Audit reference: https://github.com/gonisulaimann/Grounded

Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
…sponse (cheaper-inference#14456) (cheaper-inference#14457)

* fix(resilience): release admission lease on client disconnect (cheaper-inference#14456)

releaseChatAdmissionWhenDone wired lease release into three consumer-driven
paths only: pull-to-done, pull-throws, and cancel. A client that disconnects
mid-stream may stop pulling and never cancel, so none of them run and the
heavyweight slot is charged for the lifetime of the process. Once activeHeavy
reaches OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT every heavy request is shed with
503 chat_admission_busy, with waiting=0 and no real load.

Observe the request signal as a fallback release path and route all five call
sites through it. Release is idempotent so a late abort after a clean finish
cannot double-decrement.

* refactor(admission): extract lease release into chatAdmissionRelease

check:file-size freezes chatBodyAdmission.ts at 1206 lines and the fix pushed
it to 1242. Move the release binding into its own module and re-export it from
the original path, so every existing import site is unchanged. File is now
1159 lines; check:file-size passes.

---------

Co-authored-by: xiaoyaner0201 <xiaoyaner0201@users.noreply.github.com>
…eaper-inference#14460)

* fix(translator): keep a Claude image whose source is an HTTPS URL

Claude accepts an image block as { source: { type: "url", url } } next to
base64. claude-to-openai.ts took that in its image case (592ca9b, 05/04)
but not in the tool_result branch below it, where image lifting was added
for base64 only (7b139fd, cheaper-inference#5100, 27/06): a tool returning a URL image had
the raw block JSON.stringify'd into the tool message. claude-to-gemini.ts
had no URL branch at all — cheaper-inference#13335 lifted base64 tool_result images there and
said so in its notes, leaving the URL half for a follow-up. This is it.

Lift a URL tool_result image into the following user turn, as the base64
half already does, and send a URL image to Gemini as fileData { fileUri },
the mapping helpers/geminiHelper.ts uses for an OpenAI image_url that is a
URL. HTTPS only, as validation/schemas/apiV1.ts already requires of every
media URL and as Gemini documents for an external fileUri: an empty, http:,
data: or file: url stays dropped instead of reaching Gemini as a fileUri it
would reject.

* docs(changelog): add fragment for cheaper-inference#14460

---------

Co-authored-by: datrixlab <325650023+datrixlab@users.noreply.github.com>
…nference#13999) (cheaper-inference#14439)

* fix(api): settle logs export stream on mid-stream DB error (cheaper-inference#13999)

* refactor(api): extract the log-export stream builder to clear the new-code complexity ratchet (cheaper-inference#13999)
…release:reconcile (cheaper-inference#14440)

readFragments ran one `git log --diff-filter=A -- <file>` per fragment (≈4.5 s each on a loaded devbox → 364 fragments ≈ 30 min in the v3.8.51 round-3 pass) plus one `git show` per fragment. It now builds a path→oldest-adding-commit map from a single `git log --diff-filter=A --name-only` walk over changelog.d/ and reads every blob with one `git cat-file --batch`; the same fields (originHash/originPr/prefixPr/text) come out, phantom fragments keep their original origin. Measured: 5.3 s for the whole pass on the same box. Tests: tests/unit/reconcile-changelog-fragments.test.ts (temp git repos).
…cheaper-inference#14110) (cheaper-inference#14445)

* fix(guardrails): fail closed on unknown-root paths in ambiguous prose (cheaper-inference#14110)

PR cheaper-inference#13295 narrowed findUnquotedPathEnd's ambiguity fallback to
failClosedAmbiguity only, but the redactUnquotedAbsolutePathSpans call site
only set that flag for Windows/file-URI/known-POSIX-root candidates. An
unquoted POSIX path whose root is not in POSIX_FILESYSTEM_ROOTS (e.g.
/custom/internal in a container), followed by ambiguous prose, fell through
unredacted -- regressing HR#12 (never leak err.message/paths).

Per owner decision on cheaper-inference#14110 (fail-closed), extend mustFailClosed in
redactUnquotedAbsolutePathSpans to also cover an unknown-root POSIX candidate
(isPosixPath) once findUnquotedPathEnd returns -1. This intentionally also
fail-closes a bare, unanchored API route in prose (route- and filesystem-
shaped candidates are lexically indistinguishable with no second anchor) --
the cheaper-inference#13144 guarantee that a route immediately followed by another absolute-
path span keeps its trailing prose is unaffected and re-asserted as a
co-located regression test.

Reactivates the cheaper-inference#14110-SUSPENDED assertion in
error-public-boundaries-hardening.fixture.ts and folds in the proven repro.

* chore(quality): register the cheaper-inference#14110 redaction test in stryker tap.testFiles
…cheaper-inference#14208) (cheaper-inference#14446)

deepseek-web's tool-call parser only recognized tags literally named
`tool`/`tool_call`. Some harness builds instead emit a well-formed grammar
wrapped in doubled full-width-pipe "DSML" namespace markers
(`<||DSML|| calls>`, `<||DSML|| invoke name="...">`,
`<||DSML|| parameter name="...">`), which neither this parser, the generic
single-pipe DSML normalizer, nor the canonical <tool> fallback recognized —
the block was silently dropped and the raw DSML text leaked to the user.

Add normalizeDsmlInvokeMarkup() to rewrite the DSML calls/invoke/parameter
grammar (single- and double-pipe) into the canonical <tool>/<parameter> tags
before tokenization, so parseDeepSeekToolCalls resolves it into a proper
tool_calls entry.
…calls (cheaper-inference#14154) (cheaper-inference#14447)

Codex MultiAgent V2 treats a collaboration function_call's arguments as
backend ciphertext unless the item carries an explicit
encrypted_function_args: [] plaintext-delivery marker (codex-rs
ToolCall::direct_source). The vendored ChatGPT-web bridge already emitted
this marker for spawn_agent/send_message/followup_task via its own local
plaintextCollaborationFields() helper, but the three general Responses-API
output emitters used for every non-OpenAI chat-format upstream never did:
the streaming translator (translator/response/openai-responses.ts), the
non-streaming client translator (handlers/chatCore/nonStreamingClientTranslate.ts),
and the Responses transformer (transformer/responsesTransformer.ts).

Extracts the marker into a shared
translator/response/openai-responses/collaborationPlaintextMarker.ts helper
(bridge.ts now imports it instead of keeping its own copy) and a
functionCallIdentity.ts helper that composes the existing cheaper-inference#7936 identity
restoration with the new marker for the streaming translator. Wires the
Responses transformer with the same requestToolIdentityMap plumbing the
other two emitters already had (it previously restored neither namespace
nor the marker at all), threading the ledger through from
handlers/responsesHandler.ts before handleChatCore deletes the side
channel.
…ailed" (cheaper-inference#14309) (cheaper-inference#14463)

open-sse/utils/proxyFetch.ts already computes a detailed transport
diagnosis (DNS/socket error code, syscall, address) whenever a direct
fetch fails on both the pooled undici dispatcher and the native-fetch
fallback, attaching it to the thrown error as `.proxyFetchDetail`. But
src/lib/providers/validation/transport.ts::toValidationErrorResult()
only ever read `error.message` — always the generic "fetch failed"
string by undici/native-fetch design — and never `error.cause` (where
SafeOutboundFetchError's normalizeFetchFailure() stores the original
error carrying `.proxyFetchDetail`). The computed diagnosis was
silently discarded before it ever reached the dashboard.

toValidationErrorResult() now walks one level of `error.cause` when
the message is the generic "fetch failed" string and, if a
`.proxyFetchDetail` is found there, surfaces it (still routed through
sanitizeErrorMessage()) instead of the bare message.

Also extracted the direct-path detail-string builder (and the existing
redactProxyDetailsInMessage() redaction helper) out of proxyFetch.ts
into a new open-sse/utils/proxyFetchRedaction.ts module, applying the
same proxy-URL/credential redaction to the direct-path detail that the
proxy-path message already had — closing a latent redaction gap
between the two branches — without growing proxyFetch.ts past its
frozen file-size baseline.

This does not explain the reporter's own underlying transport failure
(likely local network/DNS/firewall config on their machine); it makes
that failure diagnosable instead of opaque.
…v3.8.51 vs main, api/v1 + image handlers) (cheaper-inference#14365)

Fixes from the 2026-09-21 E2E run of /omni-code-review (15 lenses, 3 rounds): delete-completed keyState audit without the redundant re-validation, honest image_size logging with a warn on clamp, one shared fetchUntrustedRemoteImage and one shared stringifyImageErrorForLog for both image sinks, tests. Follow-ups: cheaper-inference#14332-cheaper-inference#14335.
Replays cheaper-inference#12755 on top of cheaper-inference#13637 so both fixes survive intact.

A synchronous spawn failure left the queue stranded: nothing else calls dispatch() again, so queued jobs and their full request bodies were retained for the lifetime of the process. Queued jobs now reject as retryable when no worker is left to drain them, while a capacity-spawn failure with a healthy worker keeps the backlog for that worker.

Preserve cheaper-inference#13637's reject-based runtime-fault contract: fast faults retry compression in-process, while a dispatch timeout degrades to the uncompressed body after spending the worker budget. Keep fail-open reporting, strip non-cloneable callbacks from wire jobs, and guard postMessage failures.

Add an end-to-end regression proving the cheaper-inference#13423 shared-subobject shape remains worker-eligible and falls back to in-process compression after synchronous MODULE_NOT_FOUND.
@insoln
insoln force-pushed the fix/compression-worker-pool-queue-jam branch from 286006e to 656e716 Compare September 22, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet