mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 22:02:19 +03:00
c43fbb1b3d7c8c81ff2d016e8eb919e906608575
8963 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c43fbb1b3d |
fix(security,resilience): block origin-IP header forwarding and treat 413 as retryable TPM (#13350)
* fix(security): never forward origin-IP headers upstream Operator-set custom upstream headers could carry the client-origin IP (x-forwarded-for, x-real-ip, cf-connecting-ip, forwarded, via, ...) to the upstream provider, disclosing or spoofing it. Extend the FORBIDDEN denylist in upstreamHeaders.ts to cover the whole forwarding/IP set, mirroring the scrubbers already used by the Antigravity (antigravityHeaderScrub.ts) and Cursor CLI (cursorCliProxy.ts) paths, so the protection applies to every provider rather than those two. Covered by tests/unit/upstream-headers-sanitize.test.ts (6 passing). * fix(resilience): treat 413 payload-too-large as retryable TPM rate limit Providers with a tokens-per-minute cap (Groq among them) answer an oversized turn with 413 rather than 429. checkFallbackError() did not list 413 as retryable, so the request failed hard instead of falling back to another account or model. - Add PAYLOAD_TOO_LARGE (413) to HTTP_STATUS and to the retryable set - Return a MODEL_CAPACITY retryable fallback for 413 - Recognise "tokens per minute" / "tpm" as context-overflow patterns --------- Co-authored-by: Themedexperiencesusa <221764849+themedexperiencesusa@users.noreply.github.com> |
||
|
|
7663aadea9 |
feat(models): add Gemini 3.8 Flash tiers to Antigravity and AGY catalogs (#13318)
* feat(models): add Gemini 3.8 Flash tiers to Antigravity and AGY catalogs
* fix(antigravity): handle Gemini 3.8 Flash thought signatures, native tool calls, and output token limits
* fix: remove duplicate Gemini 3.8 model specs
* feat(models): adopt CLI catalog, pricing, and version fallbacks from #12499 (#13318)
* test(models): assert Gemini 3.8 Flash catalog and pricing presence (#13318)
* fix(antigravity): Gemini 3.8 Flash tiers have no shared -tiered endpoint
Live testing against Google's Cloud Code upstream
(daily-cloudcode-pa.googleapis.com/v1internal:streamGenerateContent),
documented in #12499, shows Gemini 3.8 Flash is served directly at
gemini-3.8-flash-high/-medium/-low. Unlike 3.7, there is no
gemini-3.8-flash-tiered endpoint for 3.8.
This branch mapped the bare id and all three tiers to an invented
gemini-3.8-flash-tiered upstream target and declared that model in the
shared Antigravity/AGY catalog. Remove the invented catalog entry, alias
only the bare "gemini-3.8-flash" display id to its default tier
(gemini-3.8-flash-high), and let -high/-medium/-low pass through
verbatim to match what the live endpoint actually serves. Also drops
the now-dead managedModelImport.ts mitm-alias branch that forced the
same invented -tiered target, and updates the catalog/alias tests
accordingly.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* revert: drop CLI catalog/pricing/version-fallback adoption also shipped by #12499
Commit
|
||
|
|
8288a4a012 |
fix(sse): deepseek-web resilience — premature session close + malformed tool-call recovery (#13226)
* fix(sse): deepseek-web collectSSEContent no longer returns a silent partial stub on premature session close
collectSSEContent() (used for the deepseek-web tool-calling / non-stream path)
drained the upstream SSE body and returned whatever content it had once the
reader reported done, with no check that DeepSeek had actually signalled
completion via response/status: "FINISHED".
When the upstream cookie session drops mid-generation (expired session,
anti-bot challenge, network interruption), the HTTP body simply closes
early. That was indistinguishable from a real completion: execute()
returned HTTP 200 with finish_reason "stop" and whatever partial stub text
had arrived so far. Observed in production call logs: a lone "I'll check
that..." / "Vou verificar..." with no continuation, reported as a
successful completion.
collectSSEContent now tracks whether the FINISHED status event was seen. If
the stream ends without it, it throws instead of returning the stub -
execute()'s existing try/catch turns that into a proper 502 that the
client, or a combo's retry/fallback logic, can react to.
Added tests/unit/deepseek-web-premature-close.test.ts covering both the
premature-close error path and the normal FINISHED completion path. Full
deepseek-web unit suite (97 tests) still passes.
* fix(sse): recover malformed deepseek-web tool-call replies and retry when unrecoverable
Two related failure modes on the deepseek-web tool-calling path, both
observed in production call logs from real agentic (VS Code Copilot-style)
usage of the deepseek combo:
1. DeepSeek's web session occasionally leaks malformed/internal formatting
tokens right after an otherwise-complete <tool>{json} body, instead of
a clean </tool> close (observed: a fully valid create_file JSON call
immediately followed by corrupted pseudo-tags). parseLooseJsonObject's
strict JSON.parse rejected the whole block over that trailing garbage,
even though a perfectly valid object sat at the start - so the call was
silently dropped and the raw tagged text was shown to the user instead
of the file being created.
deepseekWebTools.ts: added salvageLeadingJsonObject(), a quote/escape
aware balanced-brace scanner that recovers just the leading JSON object
when the strict parse fails, reusing the same salvage idea already used
elsewhere in this file (findBareJsonCandidates) for bare-JSON detection.
2. When even that salvage cannot recover a call (genuinely truncated JSON,
garbled beyond repair), execute() previously gave up on the first try.
Since this is a scraped, non-deterministic web session rather than a
real API, simply asking again is usually enough to get a clean reply.
deepseek-web.ts: the hasTools branch now detects an unparsed <tool...>
tag surviving in the cleaned content and retries with a brand-new
session, bounded to MAX_TOOL_PARSE_ATTEMPTS (2) - never an unbounded
retry loop, and a reply that parses cleanly on the first try costs no
extra latency.
Builds on the collectSSEContent premature-close fix from the same PR -
that one covers the upstream session dropping mid-stream; this one covers
the session completing but returning malformed tool-call content.
Testing:
- tests/unit/deepseek-web-tools-salvage-leading-json.test.ts (4 tests):
recovery from the exact production-observed corruption pattern, escaped
quotes/nested braces before the garbage, correct non-promotion of
genuinely truncated JSON, and no regression on well-formed blocks.
- tests/unit/deepseek-web-tool-call-retry.test.ts (3 tests): retry
succeeds on a fresh session, retry is bounded (gives up after
MAX_TOOL_PARSE_ATTEMPTS and surfaces the raw content rather than
looping forever), and a clean first reply never triggers a retry.
- Full deepseek-web unit suite: 104/104 passing, no regressions.
- The salvage fix was additionally verified directly against the exact
malformed content captured from a live production call log (not just
the hand-written test fixture).
---------
Co-authored-by: VictorRP7 <187780317+VictorRP7@users.noreply.github.com>
|
||
|
|
db022768df |
fix: FriendliAI 403 credit exhaustion misclassified as auth_error (#13040)
* Add new error signal for depleted credits * chore: add changelog fragment for #13040 FriendliAI credit-exhaustion 403 fix * test(accountFallback): add FriendliAI credit-exhaustion 403 body test (#13040) --------- Co-authored-by: fesshompa <fesshompa@yahoo.com> |
||
|
|
614ff4b60a |
fix(sse): thread errorText into shouldMarkAccountExhaustedFrom429 (#13008)
* fix(sse): thread errorText into shouldMarkAccountExhaustedFrom429
shouldPreserveQuotaSignals() (open-sse/services/quotaResetParsing.ts) gained
an errorText parameter so an explicit quota-exhausted body could override the
apikey-category default, but only one of its two call sites was updated.
checkFallbackError() passes the upstream body; shouldMarkAccountExhaustedFrom429()
still called it with the provider alone.
With errorText undefined the helper's
`Boolean(errorText) && looksLikeQuotaExhausted(errorText)` branch can never be
true, so for every apikey-category provider without per-model quotas the
connection was never marked quota-exhausted -- even when the upstream body
explicitly said a long-window cap was hit.
Thread errorText through the helper and pass it at the src/sse/handlers/chat.ts
call site. The parameter is optional and additive: OAuth-category providers and
callers that pass no body keep their existing behavior, and plain rate limits
("Rate limit exceeded, retry in 20s", "Too Many Requests") still fall through to
the short generic cooldown.
Regression guard: tests/unit/quota-signal-errortext-threading.test.ts
* test(sse): pin the chat.ts call site that forwards errorText
The errorText parameter added in the previous commit is optional, so dropping
it at the only production call site (src/sse/handlers/chat.ts) is neither a
type error nor a test failure -- the four existing cases call the helper
directly and none of them assert the wiring. The half of the patch that makes
it do anything in production could be reverted, or lost in a refactor, with
the whole suite green. That is the same failure mode this branch fixes (a
two-argument helper with a call site silently passing one), one level up.
handleSingleModelChat is not exported, so the call cannot be driven or spied
without widening the production surface. Assert at the source level instead,
following tests/unit/api-key-provider-quota-bypass-scope.test.ts, and parse
the argument list rather than regex-matching formatted text so a Prettier
reflow cannot cause a false failure or a false pass. Also pins that there is
exactly one call site, and what errorStr holds.
Verified by mutation: dropping errorStr at chat.ts now fails 1 of 5.
* chore: number quota signal changelog fragment
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: TogetherWeOwn <eng@togetherweown.com>
Co-authored-by: togetherweown[bot] <togetherweown[bot]@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
|
||
|
|
a123bd049e |
fix(images): fall back when combo leg returns empty 2xx response (#12982)
* fix(images): fall back when combo leg returns empty 2xx response fetchImageEndpoint() normalized any successful HTTP response to success:true with data.data || [], so an OpenAI-compatible provider returning 200 with an empty/malformed image payload stopped image combos on the first leg and produced an image-less 200. Require at least one usable item (non-empty b64_json or url) before declaring success; empty 2xx becomes a retryable 502 with a sanitized error so executeImageCombo() advances to the next priority leg. Valid responses and direct image-model requests are unchanged. * chore(changelog): add fragment for image-combo empty-2xx fallback (#12982) * test(images): drop misleading #10199 reference from empty-200 fallback test The test file and its production-code comment referenced issue #10199, which belongs to an unrelated, already-merged PR (auto/best-free free-tier filter fix). Rename the test file and update comments to remove the incorrect reference so future readers are not misled. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(images): fix response-shape assertions after #12268 combo envelope change release/v3.8.51 already ships #12268, which changed executeImageCombo() to return the handler's payload unchanged ({created, data: [...]}) instead of unwrapping it into a bare array. Update the two assertions that still expected a bare array so the test reflects the combo response shape that is actually live on the target branch; the fallback behavior under test is unaffected. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Tiangao (hermes) <montigaud@aikumi.pro> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
b272aa2f23 |
fix(tests): drain call-log saves before chat-pipeline DB resets (#12780) (#12966)
Pending call-log saves can still be in flight when resetStorage closes the DB instance. The orphaned save then writes into the next test's DB after getDbInstance resolves. - Drain pending saves in resetStorage before resetting DB instances. - Bind DB instance at the start of saveCallLogOperation. - Wait specifically for the Codex responses row in chat-pipeline tests. |
||
|
|
88b928b8f1 |
feat(api): accept expiresAt when creating API keys (#12952)
* feat(api): accept expiresAt when creating API keys POST /api/keys accepts expiresAt (ISO datetime, nullable) with the same semantics as the key-update path, so automation can create an expiring key in one operation instead of create-then-update. Omitted/null preserves the current non-expiring behavior; enforcement reuses the existing expiry policy without changes. * docs: name changelog fragment after PR number with credit |
||
|
|
2b7a881c6f |
fix(claude): passthrough tool_use names must match client-declared casing (#12855)
* fix(claude): passthrough tool_use names must match client-declared casing A mapless restoreClaudePassthroughToolUseName upgraded known Claude Code tool names (bash -> Bash) on every Claude-format SSE passthrough. Clients that declared lowercase tool names (pi, OpenCode, ... on claude-format executors like devin-cli-agentic) received a tool_use name they never declared: client-side tool dispatch fails and echoing the history back hard-fails with devin-cli-agentic undeclared_historical_tool (live repro: pi + dva/glm-5-2 on a self-hosted router, 400 on every agentic turn). - restoreClaudePassthroughToolUseName: alias map first (renamed -> original), then normalize to the request's declared tools[] casing, never canonicalize undeclared names. Genuine Claude Code clients keep their #7926 protection (upstream downcase -> declared PascalCase). - devin-agentic serializer: case-insensitive fallback for historical tool_use names + render the declared spelling, so case drift can no longer kill a whole turn. Tests: tests/unit/claude-passthrough-tool-name-mapless-leak.test.ts, tests/unit/devin-agentic-serializer-case-insensitive-history.test.ts * fix(stream): direct ledger lookups in claude passthrough restore restoreClaudeToolName's canonical-upgrade fallback fires even when an alias ledger exists (canonical beats the identity match). The claude passthrough lane always carries a non-empty proxy_ ledger (buildClaudePassthroughToolNameMap), so every lowercase tool_use name was upgraded to Claude Code PascalCase on the SSE path while the JSON path (direct map.get) stayed correct — the live leak behind #12721. * docs(changelog): add fragment for claude passthrough tool_use casing fix Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
88e666e596 |
fix(sse): fix double-escaped tabs in Codex JSON tool call arguments (#12841)
Fixes #12831. When the Codex upstream model produces string values in tool arguments that contain double-escaped tabs (\t inside the JSON string instead of \t), the parser outputs literal backslash-t characters. This breaks editor patches that rely on proper indentation. This commit adds a fixDoubleEscapedTabs sanitization step before parsing to restore them to single tabs. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
c193595db6 |
fix(open-sse): promote reasoning_details text to reasoning_content even when reasoning present (#12665) (#12688)
* fix(open-sse): promote reasoning_details text to reasoning_content even when reasoning present (#12665) OpenRouter thinking models return both a "reasoning" string and a "reasoning_details[].text" array for the same thinking trace. OmniRoute's reasoning promotion gated on "is any readable value present" (which includes the "reasoning" alias), so reasoning_content was never populated and clients like opencode that only read reasoning_content lost all thinking traces. Encrypted-only reasoning_details items are left intact (not flattened). Fixes all three promotion gates: - non-streaming: copyOpenAICompatibleReasoningFields now mirrors reasoning_details[].text into reasoning_content unless reasoning_content itself is present - streaming mirror block: same gate fix on getReadableReasoningValue - streaming passthrough: force re-serialization when sanitize added a reasoning_content the upstream delta did not carry (needsReserialization was false because hasUnsupportedReasoningSignal requires !readable) Tests: non-streaming + streaming unit regressions and an integration E2E that drives the full handleChat path against a mock OpenRouter provider. Also closes the same latent gate in the JSON-to-SSE rehydrator (jsonToSse.ts buildReasoningDelta): a populated reasoning string used to short-circuit the unsupported-alias mirror, so reasoning_details[].text was dropped when synthesizing an SSE stream from a non-streaming JSON body. Adds a #12665 regression test for that path, fixes an over-indented brace in stream.ts (lint), and restores the missing trailing newline in the E2E. * docs(changelog): add fragment for #12688 — fix(open-sse): promote reasoning_details text to reasoning_content even when reasoning present * fix(tests): replace any with typed casts in #12665 regressions to satisfy no-explicit-any gate |
||
|
|
0f5f83c5ed |
fix(resilience): clear the combo LKGP pin only when it names the failed target (#12235)
* fix(resilience): clear the combo LKGP pin only when it names the failed target
Re-applied onto current release/v3.8.51. The branch was 217 commits behind
and dispatchWithCooldownRetry / handleRoundRobinCombo have since moved out
of combo.ts, so this is a re-application, not a rebase: the definition is
still in combo.ts, and the 14 call sites now live in
combo/executeTargetAttempt.ts (4), combo/executeTargetGates.ts (5) and
combo/roundRobinCombo.ts (5). clearStaleLKGP is dependency-injected via
attemptLoopTypes.ts, so that signature takes the new parameter too.
Unchanged in substance. The target-scoped pin is still always cleared; the
combo-level pin is cleared only when it actually names the failed target's
provider, because it records whichever provider last *succeeded* and that
need not be the one failing now. Under `auto` the pin is a scoring input
rather than a hoist, so clearing unconditionally discarded a preference for
a healthy provider every time an unrelated target was skipped. Omitting
`failed` keeps the old behaviour for callers with no target in scope.
4 regression tests pass, including "a pin naming a healthy provider
survives another target being skipped"
mutation: clear the combo pin unconditionally (the pre-fix behaviour)
-> only that test fails, 3 pass
248 tests pass across tests/unit/lkgp*, tests/unit/combo/*
eslint clean on all five changed files (exit 0, no suppressions flag)
* docs(changelog): add fragment for the LKGP pin scope fix
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: abhisheksharma2411 <abhisheksharma2411@users.noreply.github.com>
|
||
|
|
975b29c275 |
feat(core): improve observability for dual-auth fallback execution (#11828)
* feat(core): improve observability for dual-auth fallback execution
* refactor(executors): keep the clinepass auth decision in buildClinepassHeaders
The clinepass case no longer re-decides OAuth vs BYOK off
credentials.authType; it always delegates to buildClinepassHeaders(),
which already keys the decision off credentials.accessToken, and only
the debug log line branches. The OAuth test now uses the real credential
shape ({ accessToken }) instead of an OAuth token stored in apiKey, and
a parity test pins the executor output to buildClinepassHeaders() for
both credential shapes so the two paths cannot drift apart.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
271ec25f12 | chore(quality): adjust the train-8 accountFallback ceiling to the re-measured 2517 | ||
|
|
4d1282be31 |
fix(sse): restore maxQueueDepth=0 as unbounded, sanitize refusals at the write, drain the 09-18 base-reds (#14101)
* fix(quality): drain the 09-18 base-reds, part 1 — thinking gate parity, inventory, webpack externals Reproduced on the clean tip |
||
|
|
2af688a94f |
chore(quality): file-size rebaseline for merge-train 8 (owner-approved, 2026-09-18)
28 ceilings raised to the sizes measured on the combined train tip 04cf8095 (32 contributor PRs boarding release/v3.8.51, all merge-ready; the release tip itself was green on check:file-size before boarding). Each PR adds a few irreducible call-site lines to a god-file the ratchet already freezes; the per-file attribution and the policy reference live in the baseline entry _rebaseline_2026_09_18_merge_train_8_frozen_growth. Precedent: _rebaseline_2026_07_23_v3849_merge_train_15. |
||
|
|
8feea123bb |
feat(docs): mirror every docs/ page in all 65 locales (#14106)
* feat(docs): mirror every docs/ page in all 65 locales Extends the documentation mirrors from the 22-page core set (#13940) to every Markdown page under docs/: 152 sources x 65 locales = 9,880 mirrors (6,208 new), language bars rewritten for the full locale list, state adopted so the blocking drift gate now covers all 152 pages. run-translation.mjs: an oversized block made only of table rows or list items (PROVIDER_REFERENCE.md 244-row table, FREE_TIERS.md 71-item list) is cut at item boundaries and rejoined without a blank line — the single 16-40 KB request outlived the backend socket for verbose scripts. 48 older mirrors whose tables had lost rows were retranslated with --force. * docs(i18n): refresh mirrors for the sources the base changed since the branch cut Section-level retranslation of the 29 docs (and README.md) whose source or mirrors moved on release/v3.8.51 during the run, then state adoption; the drift gate is green again on the merged tree. |
||
|
|
c059b77823 |
docs: bump the migration count 176 -> 178 (#13222 open-wa seed, #12814 proxy_logs.proxy_name)
Maintainer-side count bump after the two migration PRs merged; the docs-counts gate reads README.md, AGENTS.md and llm.txt (+ its 65 i18n mirrors), all of which are agent-instruction / protected surfaces the contributor PRs must not touch. check:docs-counts and check:docs-sync green. |
||
|
|
fda9ef78b1 |
feat(proxylogs): show registry proxy name in proxy log columns (#12814)
* feat(proxylogs): show registry proxy name in proxy log columns Registry resolution already attaches name to the runtime proxy object; the name was dropped at the persistence boundary (ProxyInfo had no name field) and never rendered. Add proxy_name column (base schema + ALTER heal for existing DBs), persist/hydrate it, render it in the ProxyLogger table and ProxyLogDetail pane with host:port fallback, and search by name. Local-only (PMO City): not submitted upstream. Re-apply after upgrades via patch file (see pmo-city-builds omniroute/Operator/runbooks/upgrade.md). * test(proxylogs): flush batched writes before asserting persisted row The v3.8.50 rebase kept upstream's batched proxy-log persistence (enqueueProxyLogs/flushProxyLogsSync); logProxyEvent no longer writes synchronously, so the persist+hydrate test closed the DB before the row was flushed. Flush explicitly first. * test(proxylogs): drain batched queue in resetStorage to avoid cross-test row bleed * docs(changelog): add changelog fragment for proxy registry name in proxy logs Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Tiangao (hermes) <montigaud@aikumi.pro> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
95b2e53727 |
feat(usage): generic billing/quota for openai-compatible connections (#13673)
* feat(usage): generic billing/quota for openai-compatible connections (#13616) Every other fetcher in services/usage hard-codes one upstream's URL, auth and response shape, which works because those providers are known. An openai-compatible connection can point at anything, and its id is minted per connection -- so it can never be a member of USAGE_SUPPORTED_PROVIDERS or a case in the dispatcher switch. So the shape comes from the connection instead. `providerSpecificData. quotaEndpoint` declares the url, auth mode, optional headers, and a mapping of dot-paths onto UsageQuota: { "url": "...", "auth": "bearer", "quotas": { "credits": { "used": "$.data.used_usd", "total": "$.data.limit_usd", "currency": "USD" } } } Dot/bracket paths (`$.a.b[0].c`) rather than full JSONPath, so the mapping stays dependency-free and legible in a config field. Three decisions worth stating: - **An unresolvable mapping reports nothing, never 0/0.** A quota reading 0 of 0 renders as fully exhausted, and an operator would act on that. A typo'd path must produce no card, not a fake outage. - **The transport error is not echoed.** The url is operator-supplied and can carry a query-string secret; the message says "unreachable" and nothing more. - **The capability is read off the connection, not the id.** `supportsProviderQuota` already takes the connection and already has a connection-shaped check (moonshot), so the gate goes there. A declared url with no `quotas` mapping does NOT count as supported: it can be fetched but can never yield a quota, and would leave a permanently empty card in Provider Limits. Verified: 7 new tests; mutations each killed by the right one -- let an unresolved mapping fall through to 0/0 -> that test alone fails echo the transport error -> that test alone fails 121 tests pass across this file, usage-families-split, provider-plugin- manifest, provider-limits* and the quota-visibility suites (the drift guard from #13134 included). eslint clean on all four files; the three no-unused-vars errors in usage.ts are byte-identical on the base branch. * fix(usage): bound the openai-compatible quota fetch with a timeout An operator-configured quotaEndpoint that never responds would hang getOpenAiCompatibleUsage()'s fetch() indefinitely, stalling that connection's Provider Limits sync. Same 15s bound as the other fetchers in this directory (grokResetCredits.ts's FETCH_TIMEOUT_MS). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(usage): pin the openai-compatible quota fetch timeout A quota endpoint that accepts the connection and never answers must be aborted by the fetch signal instead of hanging the Provider Limits sync. Fails without the AbortSignal.timeout() bound, passes with it. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: abhisheksharma2411 <abhisheksharma2411@users.noreply.github.com> |
||
|
|
c74cea3d35 |
feat(mitm): dynamically inject configured models into Antigravity model catalog (#14006)
* feat(mitm): dynamically inject configured models into Antigravity model catalog - Add /v1internal:fetchAvailableModels to ANTIGRAVITY_TARGET.endpointPatterns in src/mitm/targets/antigravity.ts - Implement catalog interception and dynamic model merging in AntigravityHandler.intercept() (src/mitm/handlers/antigravity.ts) - Merge operator's configured combos/models dynamically from the repository into Google Cloud Code's upstream catalog - Prepend injected models to agentModelSorts recommended group while preserving native models and upstream structure - Add unit tests covering target endpoint pattern declaration, catalog merging, dynamic combo retrieval, and error propagation in tests/unit/mitm-handler-antigravity.test.ts Resolves #13959 * test(mitm): isolate DATA_DIR and clean up the combo row in the antigravity catalog test The DB-backed test ("dynamic catalog pulls configured combos from database repository") creates a real combo row via src/lib/db/combos.ts, whose module- level DATA_DIR const resolves once at import time. The PR's own documented Validation command (`node --import tsx/esm tests/unit/mitm-handler-antigravity.test.ts`) runs without the `--test` flag, so the existing #10428 eval-probe/test-context guard in resolveWritableDataDir() never triggers and DATA_DIR falls through to the real ~/.omniroute home database — writing a permanent test-combo row into it every run. Set DATA_DIR to an isolated temp dir at the top of the file (before the combos.ts import), reset the DB singleton and clean up the temp dir in test.after(), and wrap the combo creation in try/finally so the created row is deleted even on assertion failure. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(mitm): prevent model name collision and filter inactive combos in antigravity catalog * feat(antigravity): integrate native auto groups, fallback to groq, and add bridge proxy - Inject OmniRoute native auto groups (auto/best-fast, auto/best-coding, auto/best-reasoning, auto/best-free, etc.) into Antigravity IDE & CLI /model selector - Add bin/antigravity-bridge.mjs with selective proxy routing to isolate native Gemini quota (zero Google token leakage) - Implement transparent self-healing model remapping to prevent upstream 410 model_shutdown errors on deprecated models - Update emergencyFallback provider from nvidia to groq/openai/gpt-oss-120b for resilient 0.02s failover * test(antigravity): add unit test suite for antigravity bridge routing and model self-healing - Add tests/unit/antigravity-bridge-routing.test.ts covering zero quota leakage for native Gemini models - Validate OmniRoute auto group routing and display name interception - Validate retired upstream model self-healing (preventing HTTP 410 crashes) - Export helper methods from bin/antigravity-bridge.mjs with isMain guard --------- Co-authored-by: Stavan <stavan794@gmail> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: steve25060 <steve25060@users.noreply.github.com> |
||
|
|
d600eba35b |
fix(cli): preflight the port before serving so a second instance cannot de-register the first (#12485)
* fix(cli): preflight the port before serving so a second instance cannot de-register the first
Starting `omniroute serve` against a port another OmniRoute already owns
produced three identical raw Node stack traces and no explanation:
Error: listen EADDRINUSE: address already in use 0.0.0.0:20128
The conflict was handed to the child process, so it surfaced only after the
child had been spawned and retried twice on the supervisor's restart budget,
and never named the process holding the port.
The damage was worse than the noise. Both spawns happen after
writePidFile("supervisor") and the failed child's cleanupPidFile("server"), so
a doomed second instance overwrites the pid files of the healthy instance that
owns the port: supervisor/.pid ends up pointing at the dead starter and
server/.pid is deleted, de-registering a server that is up and serving.
Observed live: healthy server 19348 under supervisor 11108, while
supervisor/.pid read 21440 (dead) and server/.pid was gone. `omniroute stop`
still worked, but only by falling through to its killByPort port fallback.
serve now resolves the port owner before spawning anything or touching a pid
file, and exits with a message naming the owning PID and the two ways out
(`omniroute stop`, or `serve --port <other>`). Discovery lives in
findListeningPids() in bin/cli/utils/pid.mjs (netstat on win32, lsof
elsewhere); it mirrors killByPort()'s discovery in stop.mjs, which is worth
consolidating next time that file is touched. A discovery failure reports the
port as free, since a false "busy" would block a legitimate start.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJf2dxEpiwZqyZujWk57T2
* chore(changelog): link the port-preflight fix to PR 12485
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJf2dxEpiwZqyZujWk57T2
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dmlanday <dmlanday@users.noreply.github.com>
|
||
|
|
1b2349de22 |
feat(sse): Claude OAuth lower-priority lane + weekly session-limit reset (#13074)
* feat(sse): Claude OAuth lower-priority lane + weekly session-limit reset
Mirror Claude Code's /low-priority and /limit-reset for OmniRoute-managed
Claude subscription accounts (wire contract captured from Claude Code 2.1.263).
Both are opt-in per connection (providerSpecificData.lowPriorityMode /
autoLimitReset, Edit connection -> Claude section, default off) and only act
on the 5-hour usage wall: a 429 carrying
anthropic-ratelimit-unified-status: rejected and, when eligible,
anthropic-ratelimit-unified-slow-offer: treatment. Nothing is sent before
that first wall 429.
- Lower-priority lane: on the wall the executor retries the SAME account
with `anthropic-usage-limit: slow` and keeps the header on every request
until anthropic-ratelimit-unified-reset (+60s). The intercepted 429 never
reaches chatCore, so the connection is not cooled down or rotated away.
slot_busy (429) / 529 wait slow-retry-after (20s default, 5-600s, +-30%
jitter) bounded by slow-max-wait (20min default, 1min-6h), then end +
10min cool-off. weekly_limit / budget_exhausted / off / ineligible, a
5h-window rollover, or ineligible + overage-in-use end the lane and let
the response flow to the normal cooldown path.
- Session-limit reset: GET /api/oauth/usage?at_wall=1&skip_spend=1 ->
juniper_tide block; when arm=reset and available, POST
/api/organizations/{org}/reset_rate_limits {program: "juniper_tide"} and
retry at full speed. already_used / not offered memoise next_available_at.
- State is in-memory per connection; the executor owns the abort-aware
sleep; the pure state machine and the HTTP client are separate modules
with unit tests; an executor-level test proves the header/retry wiring
end to end with a mocked upstream.
* fix(sse): make the Claude usage-wall handling race-safe for parallel requests
Two requests on the same Claude OAuth connection can hit the 5-hour wall in
the same instant.
- Lower-priority lane: the executor now tells the decider whether THIS
request carried `anthropic-usage-limit: slow`. A sibling built while the
lane was still idle (no header) whose 429 lands after the lane activated
is re-sent on the lane instead of being misread as a "wall" verdict that
would end it; its 2xx is not counted as lane telemetry either.
- Session-limit reset: concurrent wall hits share one in-flight status+claim
round trip (no duplicate POST reset_rate_limits), and for 60s after a
granted reset stale sibling walls are answered "reset" without touching
the network, so they retry at full speed instead of re-claiming or
falling into the slow lane.
Tests cover both races.
* fix(sse): address adversarial review of the Claude usage-wall handling
Three defects found by a 3-lens review of the two previous commits.
1. Lane wait could outlive the request (high). The slot_busy/529 sleep shares
the request's AbortSignal with chatCore's upstream-start timeout (10 min by
default), while the lane's own max-wait defaults to 20 min and can reach 6h
from the server header. A long slot_busy streak was therefore killed
mid-sleep with a TimeoutError instead of ending gracefully as max_wait with
its cool-off. The decision now takes a waitCeilingMs — what is left of the
executor's own timeout, minus a 5s margin — which caps the effective
max-wait and clamps each individual sleep.
2. A wall 429 surfacing only after a 400-driven intra-attempt retry was missed
(medium). The context-editing / thinking-budget / effort / auto-learn
fallbacks all re-fetch and REASSIGN `response`, and the wall check ran
before them, so such a 429 fell through to the generic path and cooled the
connection down. The check now runs after those retries, on the final
response of the attempt.
3. `ineligible` + `overage-in-use: true` ended the lane as plain `ineligible`
on a 429 (medium) because the status mapping ran first; only the non-429
tail produced `extra_usage`. Overage takeover now wins on every status.
Also bounds the module-level per-connection maps with the same FIFO policy as
the identity caches in claudeIdentity.ts: the state key falls back to the
access token when a connection id is absent, and OAuth tokens rotate on every
refresh, so the maps could grow for the process lifetime.
Tests cover all three fixes, including an executor-level regression for the
400-then-wall ordering.
* fix(i18n): add the Claude usage-wall toggle strings to pt-BR
`tests/unit/i18n-pt-br.test.ts` (#6695) requires pt-BR.json to carry every key
present in en.json; the four new `providers.claude{LowPriorityMode,AutoLimitReset}*`
keys were only added to en and it, so the gate failed on this branch.
* refactor(sse): keep the usage-wall change inside the frozen quality budgets
The three ratchets this PR tripped were all its own, not inherited:
- file-size (frozen, may only shrink): open-sse/executors/base.ts 1857 > 1751
and EditConnectionModal.tsx 1653 > 1631.
- complexity / cognitive-complexity (new-code mode): three functions over the
15 threshold — runClaudeLimitResetAttempt (27), handleClaudeUsageLimitResponse
(19 / cognitive 23) and observeClaudeLowPriorityResponse (17 / 17).
Extractions, all behavior-preserving:
- New open-sse/executors/claudeUsageLimit.ts owns the executor-side glue (header
injection, wait accounting, abort-aware sleep, timeout-derived wait ceiling and
the decision logging) behind a ClaudeUsageLimitGuard, so base.ts keeps a
three-line call site instead of ~100 lines of mechanics.
- Three long-standing Claude blocks leave base.ts for the modules they belong to:
mergeCcHeaders + applyStainlessHeaders into config/anthropicHeaders.ts and
stripClaudeSystemPrefixBlocks into executors/claudeIdentity.ts. base.ts is back
at its frozen 1750 lines.
- The modal's Claude section becomes ClaudeConnectionFields.tsx (mirroring
CcCompatibleRequestDefaultsFields) plus a claudeConnectionFields.ts helper that
de-duplicates the field defaults across the modal's two init sites; the file
drops to 1622, below its frozen 1631.
- The three over-threshold functions are split into focused helpers
(observeErrorResponse / observeSuccessResponse, shouldClaimLimitReset,
resolveLimitResetOffer / runLimitResetClaim / memoiseNotBefore).
Gates now: file-size OK, complexity 0 new violations, cognitive 0 new,
fetch-targets / error-helper / build-scope / deps OK, typecheck clean, ESLint 0,
Prettier clean, 155 unit tests green across the feature and its neighbours.
Still failing and NOT this branch's: pack-policy (unexpected
@omniroute/opencode-plugin-v2 files in the npm artifact) and
mutation-test-coverage (stryker tap.testFiles missing entries for
circuitBreaker.ts and comboStructure.ts) — both reproduce on the untouched base.
---------
Co-authored-by: davidebaraldo <davidebaraldo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
0fbd8854c5 |
fix(api): adapt TEI/Infinity request and response shapes on the /v1/rerank node path (#13733)
* feat(api): route /v1/rerank to remote provider nodes behind RERANK_REMOTE_PROVIDER_NODES POST /v1/rerank only ever dispatched to provider nodes whose base URL hostname was localhost, 127.0.0.1, or 172.16.0.0/12 — a filter hardcoded in the route. A rerank node on any other host (a LAN box or Tailscale peer running TEI, Infinity, vLLM, …) was silently dropped and the request fell through to "Invalid rerank model", even though the same node served /v1/embeddings without complaint and had already passed the provider outbound URL policy at creation time. The memory engine's rerank step calls this route over loopback, so `rerankProviderModel` could not reach such a node either. Mirror the audio routes (#3963): loopback nodes stay always-eligible and unchanged; remote nodes are opt-in via a new `RERANK_REMOTE_PROVIDER_NODES` feature flag (default off — routing to a remote host changes egress identity) AND must pass the provider outbound URL policy (`getProviderOutboundGuard()`, `public-only` deployments never route to private hosts. - src/shared/network/loopbackNodeHost.ts: one pure definition of the loopback host set, replacing three copies (rerank route, audioRegistry, localHealthCheck). The shared version also rejects `user@host` URLs, which the audio copy did not. - src/shared/network/providerNodeHost.ts: policy-aware remote-node eligibility that mirrors guardProviderNodeBaseUrl() on the creation path. - src/app/api/v1/_shared/rerankProviderNodes.ts: pure, testable selection step + loader, modelled on audioProviderNodes.ts. - Feature flag definition, FEATURE_FLAGS.md / ENVIRONMENT.md / .env.example rows, API_REFERENCE.md and MEMORY.md notes, changelog fragment. - tests/unit/rerank-remote-provider-nodes.test.ts covers the host classification, the three policy modes, the selection step, and the route end-to-end (flag off → 400 without contacting the node; flag on → forwarded to <base>/v1/rerank with the node credential; flag on + strict policy → still excluded). Feature-flag count test bumped to 56. * chore(changelog): name the #13732 fragment * fix(api): adapt TEI/Infinity request and response shapes on the /v1/rerank node path The provider-node branch of POST /v1/rerank already fell back from <base>/v1/rerank to <base>/rerank on 404 "for Infinity / TEI", but it kept sending the Cohere body and returned the upstream JSON verbatim. Against Hugging Face text-embeddings-inference that could never work: TEI requires the candidate list as `texts` (HTTP 422 otherwise), takes `return_text`, and answers a bare `[{index, score, text?}]` with no `results` envelope and `score` instead of `relevance_score`. Thin gateways in front of TEI/Infinity commonly emit `score` too. Either way the memory engine's applyRerank(), which reads `results[].relevance_score`, ended up with undefined scores. Add two pure adapters in src/app/api/v1/_shared/rerankLocalNodeShapes.ts: - buildLocalRerankRequestBody(): one upstream body carrying both spellings (`documents` + `texts`, `return_documents` + `return_text`). TEI's request struct is not deny_unknown_fields and the OpenAI-shaped servers (vLLM, llama.cpp, Infinity, oMLX) ignore extras, so a single body serves all. - normalizeLocalRerankResponse(): folds `{results:[…]}`, Voyage-style `{data:[…]}`, and TEI's bare array into the Cohere envelope, backfilling `relevance_score` from `score`, sorting by score, honouring `top_n`, attaching `document.text` when requested, dropping malformed entries, and preserving other top-level fields (`model`, `usage`, …). The route now uses both on the primary and fallback fetch. Cloud registry providers are untouched (they go through open-sse/handlers/rerank.ts). tests/unit/rerank-local-node-shapes.test.ts covers the adapters and the route end-to-end: 404 → /rerank with `texts`, bare TEI array normalized and top_n-capped; `score`-only gateway → `relevance_score` for the client. * chore(changelog): name the #13733 fragment * refactor(api): split the local rerank response normalizer into per-entry helpers The complexity ratchet (new-code mode) flagged normalizeLocalRerankResponse at 18/15 on both metrics. Pull the per-entry validation and the document resolution into toCohereResult() / resolveResultDocument(); behaviour and tests are unchanged. --------- Co-authored-by: seanford <seanford@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
55b6ca6573 |
fix(compression): fall back in-process when the compression worker fa… (#13637)
* fix(compression): fall back in-process when the compression worker fails (#13145) The worker pool resolved every worker fault with the *uncompressed* body instead of reporting it. `PendingJob` had no reject path at all, so a thread error, a worker exit, a dispatch timeout, or an engine error posted back as `type: "error"` all resolved as `{ compressed: false, stats: null }`. `applyCompressionAsync` then treated that as a legitimate "nothing to compress" result and returned it as-is, so the request reached the provider uncompressed while the response header still announced the selected plan ("stacked") — the header is emitted before the pipeline runs. Nothing was logged at any level, and `compression_analytics` stayed empty because rows are only written when a compressed result is reported. The net effect was compression silently disabled for every worker-eligible request. The worker is a throughput optimisation, not a behavioural variant, so a worker fault must degrade to the in-process pipeline rather than to no compression: - `PendingJob` gains `reject`; `fail()` delegates to a new `abort()` that clears the slot timeout and rejects with a diagnostic cause (thread error, exit code, or timeout budget). - An `error` message from the worker is propagated instead of being swallowed. - `applyCompressionAsync` catches the rejection and falls through to the in-process path, logging the cause. The logger is imported lazily and defensively: `compressionWorker.ts` imports this module, so a static import would pull the logger into the worker bundle, and a logging failure must never be able to break compression itself. `close()` keeps resolving with the unchanged body — shutdown is not a fault. The regression test drives a real worker fault via `OMNI_COMPRESSION_WORKER_TIMEOUT_MS` rather than mocking the module, since this project's tsx/ESM + node:test setup has no `mock.module()` support. Its options are fully populated on purpose: `runCompressionAsync` forwards them into `workerOptions`, and `isStrictlySerializable` rejects an object holding `undefined` values — which would route the test through the in-process path and assert nothing. Production requests always carry all of those fields, which is why the worker path is taken there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGJQT3E6iJZq4zNGwfjkPs * fix(compression): keep the timeout path uncompressed, retry only fast worker faults (#13145) Review follow-up: the in-process fallthrough ran the full pipeline on the main event loop for *every* worker fault, including a dispatch timeout. A timeout means the worker already spent its whole budget on that body, so re-running the same CPU-bound work inline would stall other in-flight requests — strictly worse than not compressing on a shared gateway. Faults are now typed by whether recovery is cheap: - `CompressionWorkerError.retryInProcess` distinguishes fast faults (thread error, worker exit, engine throw — no work was done, so the in-process path costs what the worker would have) from a dispatch timeout. - Timeouts keep the original degrade-to-uncompressed behaviour, but are now reported. The defect this PR fixes is the silent swallow, not the degrade. Also strips `reject` from the structured-clone wire job. It is a function, so leaving it on the object handed to `postMessage` threw `DataCloneError` before the worker ever saw the job — turning every dispatch into an immediate fault. Adds the missing `changelog.d/fixes/` fragment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(compression): narrow the worker thread-error type for typecheck:core @types/node 26 types the Worker "error" event payload as unknown, not Error, so `error?.message` failed typecheck:core (TS2339). Narrow with an instanceof check before reading .message. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: marcs7 <marcs7@users.noreply.github.com> |
||
|
|
1e8c913ca2 |
feat(services): add open-wa as a 6th embedded service (#13222)
* feat(services): add open-wa as a 6th embedded service Adds @open-wa/wa-automate (WhatsApp Web automation via headless Chromium) following the existing embedded-service pattern, mirroring Mux's lifecycle-managed-only shape (no Layer 4 executor — this is not a routing target). Flags/env verified directly against the installed 4.76.0 source rather than trusted from web docs, which mix this stable v4 line with an unreleased v5 alpha CLI surface. healthIntervalMs is set to 60s (vs. the usual 5s) for this service: open-wa's HTTP server does not start listening until the full WhatsApp handshake resolves, which blocks on a human scanning the pairing QR code on first pairing. At the default 5s interval the supervisor's 3-consecutive-failure threshold would declare "error" ~15s into every legitimate start. This is a local, single-service config change — a proper fix (a startup-grace knob distinct from the steady-state poll interval) belongs in ServiceSupervisor/HealthChecker as a follow-up affecting all embedded services. * fix(db): renumber open-wa seed migration to avoid collision with 163 Migration 163 was already taken by 163_radar_feed_cache_generated_at.sql on release/v3.8.51 (tip is at 179). Renumbered to 180, the next free slot. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(db): renumber open-wa seed migration to avoid collision with 180 Slot 180 was reused by 180_memory_fts_au_conditional_memory_id.sql (merged 2026-09-16), so this PR's seed migration moves to the owner's assigned slot 185. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: birdleandro-bit <birdleandro-bit@users.noreply.github.com> |
||
|
|
706dc75c13 |
fix(chatCore): stop executeWithUpstreamStartTimeout leaking its abortPromise listener (hedge-cancelled process exit) (#12406)
* fix(sse): stop mergeAbortSignals from leaking abort listeners
mergeAbortSignals() attached "abort" listeners to its primary/secondary
signals but never removed them once the merged signal settled. Every
executor fetch attempt calls this (fetchWithStartTimeout, once per
URL/retry), so a busy combo request accumulated one live listener per
call on the long-lived combo/client signal. A leaked listener still
fires when that signal is later aborted (e.g. a hedge cancellation
arriving after this merge's own caller already finished), for a merged
output nothing is watching anymore.
Mirrors the already-correct self-cleaning pattern in
open-sse/utils/directResponseStartTimeout.ts's local mergeAbortSignals.
Regression test measures listener growth across repeated merges of the
same long-lived signal: 25 merges leaked exactly 25 listeners pre-fix,
0 post-fix.
(cherry picked from commit 07969655147d3236969b38bfb41280ab4fb52b79)
* fix(server): stop the crash guard re-throwing combo abort reasons
Production crash 2026-08-31 (omniroute.log): on a client disconnect,
handleDisconnect aborted the combo controller and a late abort listener
threw the abort reason on an empty stack:
Error [AbortError]: hedge-cancelled
at ... AbortController.abort ... handleDisconnect
file:///.../src/shared/utils/httpClientAbortGuard.mjs:130 throw err;
isClientAbortError() only knew Node's stream codes and "aborted", so
shouldSwallowUncaught() said false and the guard re-threw, taking the
whole server down.
- Port upstream's AbortError line (name "AbortError" + abort-flavoured
message) so request_signal_aborted / DOMException aborts are absorbed.
- Add an exact-message match for the combo abort reasons from
open-sse/services/combo/comboAbortReasons.ts ("hedge-cancelled",
"combo-per-model-timeout"), name-agnostic because the raw reason is a
plain Error that only gets name="AbortError" stamped on the way out.
A losing hedge / stalled target is never a server fault. Inlined so
this .mjs stays dependency-free for scripts/dev/run-next.mjs.
Tests: port upstream's guard tests, add the exact crash shape, a
child-process replay of the crash (dies pre-fix, survives post-fix), a
genuine-error case that must still crash, and a sync check against
comboAbortReasons.ts. The child-process helper passes a file:// URL, not
a bare path, so the tests run on Windows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 90c9bce8c474b60c37cc63f4421d50feae4c0ad2)
* fix(chatCore): stop executeWithUpstreamStartTimeout leaking its abortPromise listener
Root cause of the 2026-08-31 production exit (Error [AbortError]:
hedge-cancelled), verified by mapping the crash frames in
.build/next/server/chunks/13721.js back to this file:
- The abortPromise abort listener registered on the long-lived client /
stream signal was never removed in the finally block (only abortListener
and timeoutAbortListener were), so every executor attempt (and every
retry) leaked one listener onto that signal.
- Promise.race only subscribes to abortPromise/timeoutPromise once the
array literal has been evaluated. When execute() threw synchronously the
race never ran, abortPromise was orphaned, and the next hedge
cancellation / client disconnect aborted the signal with the string
reason streamHandler.ts forwards; createAbortError() rebuilt it as an
AbortError-named Error and rejected a promise nothing awaited. That
unhandledRejection reached the process crash guard, which re-threw it as
an uncaughtException and exited with code 7.
Keep a handle to the listener and remove it with the others, and mark the
two race-loser promises as handled so a synchronous throw from execute()
can never orphan them. Race semantics are unchanged (the race still
observes their rejections).
Regression tests: (1) a resolving execute leaves the listener count on
the client signal unchanged; (2) a synchronously throwing execute leaks
no listener and a later abort with the string "hedge-cancelled" produces
no unhandledRejection. Both fail against the previous implementation.
Note: commit 079696551 (mergeAbortSignals cleanup) is correct listener
hygiene but is not on this crash path; this is the fix for the incident.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit e68a50ad854a1945c23e747da4ef15a820cc148d)
* fix(server): absorb raw string abort reasons in the crash guard; document the verified crash path
Follow-ups from the adversarial review of 90c9bce8c:
- open-sse/utils/streamHandler.ts aborts the stream controller with a raw
string reason (getClientAbortReason / handleDisconnect) and undici
rejects with signal.reason verbatim, so a cancellation can reach
process level as a bare string. isClientAbortError() returned false for
every non-object, which would still have exited the process. Absorb the
combo abort reasons and the stream-handler disconnect reasons when they
arrive as strings.
- Correct the mechanism comment: the 2026-08-31 exit was a leaked
upstreamTimeouts.ts abortPromise listener rejecting a promise nothing
awaited (unhandledRejection), escalated by this guard, not a listener
throwing synchronously. The leak is fixed at the source in the previous
commit; this guard remains the last-resort net.
- Reword the inlining rationale (plain node launcher, no reliance on
type-stripping for the .ts constants module).
- Tests: the child-process replay now also exercises the
unhandledRejection route with the exact production error shape and with
raw string reasons; add unit coverage for string reasons and non-object
look-alikes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 696fcc8fe1b9b9bc43e6e5f5f5e44b619a86e68a)
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Beexly <Beexly@users.noreply.github.com>
|
||
|
|
eb4e3be5dc |
fix(quota): keep Kiro active while any _freetrial pool has quota (#13088) (#13324)
* fix(quota): keep Kiro active while any _freetrial pool has quota (#13088) * fix(quota): rename changelog and remove blank line --------- Co-authored-by: giauphan <giauphan@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
0349627c86 |
fix(opencode): match the upstream free-tier request contract (#14013)
Match the upstream OpenCode free-tier request contract (issue #13935): canonical ses_/msg_ identity ids, versioned User-Agent, and the measured body requirements (stream:true + non-empty tools) with a learn-and-reuse tool-name cache, so no-auth oc/* requests stop being refused with 403 FreeTierError. Supersedes #13937 (session regex and minimum-version rule kept, credited below). Complements #14011 (refusal classification) and #13819 (stream_options strip), both already merged. Closes #13935 Co-authored-by: AStupidBear <16422976+AStupidBear@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
2ebafaedce |
fix(windows): hide supervised server console (#13992)
* fix(windows): hide supervised server console * fix(windows): port icon.ico fix from #13991 and add regression tests Adds a source-pattern test asserting the supervised server spawn() passes windowsHide: true (Hard Rule #8 gap noted in review), and ports the icon.ico-on-win32 fix from #13991 (credit @prabhtheone) with its own regression test, so both real fixes ship without #13991's unrelated comment purge. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
dfcc4baed8 |
fix(codex): preserve native custom tools in Responses WebSocket requests (#13864)
* fix(codex): preserve native custom tools in Responses WebSocket requests * test(codex): release per-turn WS leases in the custom-tools passthrough test The per-account WS lease (release/v3.8.51, added after this branch's fork point) is non-queued with a default maxConcurrent of 1. This test's bare prepare() helper never released its lease, and the reused-WebSocket case re-prepares (acquiring a fresh lease) per turn before releasing the previous one — both starve the single test connection once merged with the lease feature. Release after each bare prepare() and raise the test fixture's maxConcurrent to 2 so a session's sequential turns fit. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
e400cf9ac7 |
fix(db): resolve backup retention from persisted setting on health-check path (#13773)
* fix(db): resolve backup retention from persisted setting on health-check path #13404 fixed the missing prune call after health-check-repair backups but only resolved maxFiles/retentionDays from env vars, so the persisted Storage-page setting (honored for manual/API/auto backups via getDbBackupMaxFiles/getDbBackupRetentionDays) was silently ignored on this path. Extract that env->persisted->default precedence into resolveDbBackupRetention() in backupRetention.ts and share it between backup.ts and core.ts's createManagedDbBackup(). * docs: add changelog fragment for #13308 persisted-setting follow-up * fix(db): re-point backup retention fix at managedBackup.ts's prune call The base drifted since this branch was opened: the health-check-repair backup path (createManagedDbBackup) moved from core.ts into managedBackup.ts (writeManagedDbBackup), taking its env-only maxFiles/retentionDays resolution along with it. This branch's resolveDbBackupRetention() extraction and backup.ts delegation were already correct and unaffected; only the wiring that used to live in core.ts needed to move to managedBackup.ts's prune call so the persisted Storage-page setting is honored on this path too (#13308). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
27e0d9b5b7 |
fix(dashboard): refresh per-connection proxy badges after a proxy save (#13711)
* fix(dashboard): refresh per-connection proxy badges after a proxy save ProxyConfigModal persists an assignment through `PUT /api/settings/proxies/assignments`, but the provider page bound its `onSaved` callback to `fetchProxyConfig()`, which only refetches `GET /api/settings/proxy` into `proxyConfig`. The per-account proxy badges read `connProxyMap`, which is filled from a different endpoint (`GET /api/settings/proxy?resolve=<connectionId>`) by an effect keyed on `[loading, connections]`. A proxy save changes neither key, so the effect never re-ran and the saved (or cleared) proxy stayed invisible until a manual page reload. Account- and combo-level saves refreshed nothing visible at all; a provider-level save refreshed only the toolbar chip while the rows that inherit that proxy stayed stale. Adds `refreshProxyState()`, which re-reads both sources together, and binds the modal's `onSaved` to it. The callback reads the latest connections from a ref so it stays referentially stable and does not re-render consumers on every connections fetch. Fixes #13710 Also removes the now-unused `no-unused-vars` suppression entry for useProviderConnections.ts. That entry was already stale on the base branch (the same eslint invocation reports it on an unmodified tree), and the pre-commit ratchet refuses to pass while a touched file carries one. Only that single exact entry was pruned; no baseline was widened. * docs(changelog): add fragment for #13711 * test(providers): raise proxySaveRefresh test timeout to 30s The it() case dynamically imports ProviderModalsPanel, the first test in the repo to pull in that module's ~15 modal components, which alone consumes most of Vitest's default 5000ms testTimeout and made the test flake under CI/runner load. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
80ea176022 | fix: feat(providers): add kimi/qwen/deepseek/gpt auto-routing families (#13214) (#13709) | ||
|
|
2374bbf1ee |
fix(mitm): bound SSE transcript retention and cancel abandoned upstream reads (#13702)
Handler-side collected strings grew without bound before the inspector clamp; abandoned streams kept the reader alive for the full upstream lifetime. createBoundedCollector caps retention at 1 MiB while keeping true responseSize; pipeSSE and server.cjs cancel on downstream close. Fixes #13395. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> |
||
|
|
994245a476 |
fix(translator): prevent schema property name collision in Gemini sanitizer (#13057, #13477) (#13690)
* fix(translator): prevent schema property name collision in Gemini sanitizer (#13057, #13477) * refactor(translator): reuse SCHEMA_MAP_KEYS in forEachSubschema and add changelog fragment * test(translator): avoid explicit any in gemini schema collision regression test Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: zcrew0x <zcrew0x@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
44e32a1995 |
deps: bump electron from 44.0.0 to 44.3.0 in /electron (#13664)
Bumps [electron](https://github.com/electron/electron) from 44.0.0 to 44.3.0. - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v44.0.0...v44.3.0) --- updated-dependencies: - dependency-name: electron dependency-version: 44.3.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
cc7d19daa9 |
fix(routing): skip redundant parseAutoPrefix for recognized built-in auto variants (#13647)
* Change hasFree from true to false for pioneer.ai
Pioneer.ai removed the free tier.
Before:
https://web.archive.org/web/20260516140358/https://pioneer.ai/pricing
After:
https://pioneer.ai/pricing
* fix(sse): skip parseAutoPrefix invalid-prefix warning for recognized built-in auto variants
resolveAutoRoutingState() already classifies auto/best-* variants correctly via
classifyAutoModel() before applyAutoPrefix() runs, and the old early-return
preserved that state — so the routing variant was never broken. The real,
observable defect was the spurious 'Invalid auto prefix format' warning logged
on every auto/best-* request, because parseAutoPrefix() only knows the short
aliases (VALID_VARIANTS) and returns valid:false for the best-* built-ins that
AUTO_TEMPLATE_VARIANTS recognizes.
Skip the warning (and the pointless early-return) for any model already present
in AUTO_TEMPLATE_VARIANTS. Add a regression test asserting the warning no
longer fires for auto/best-coding while an genuinely unknown auto/* variant
still warns (proving the log probe detects the message).
* fix(providers): drop out-of-scope Pioneer AI hasFree change from this PR
The Pioneer AI hasFree=false commit (
|
||
|
|
31ea46934e |
fix(sse): recognize reasoning_effort in the reactive 400 field-strip retry (#13642)
* fix(sse): recognize reasoning_effort in the reactive 400 field-strip retry Strict OpenAI-compatible gateways that don't implement the reasoning-effort knob reject requests with 400 "Unsupported parameter: reasoning_effort". findOffendingField() did not list it in KNOWN_OFFENDING_FIELDS, so the generic strip-and-retry in base.ts never fired and the 400 surfaced to the client — the request died instead of being retried once without the field. Add "reasoning_effort" to KNOWN_OFFENDING_FIELDS (sibling of the existing reasoning_budget entry, same FCC/NIM-style recovery) and pin the new match in provider-field-strips.test.ts. * chore(changelog): add fix fragment for the reasoning_effort field-strip retry (#13642) |
||
|
|
1f8bfe52c5 |
feat(docker): add self-host compose + 5-minute deploy doc (RIC-739) (#13639)
KISS self-host carrier for the 零月费 + 自托管 product form. One command brings up the published image + Redis on loopback — no profile choice, no build step, no multi-tenant anything. - docker-compose.selfhost.yml: pulls diegosouzapw/omniroute:latest + redis, all app ports 127.0.0.1-only by default, Redis not published to host, depends_on healthy, healthcheck wired. - .env.selfhost.example: minimal env (2 EDIT ME lines), no secrets baked in. - docs/getting-started/SELF_HOST_GUIDE.md: 5-minute deploy, sizing, exposing, data/backups, common issues, security checklist, what-it-is-NOT. - meta.json + DOCKER_GUIDE cross-link. Graduates to the full docker-compose.yml profiles when the user needs CLI tools / web-cookie Chromium / sidecars. Co-authored-by: Ant Rich <ant@richants.com> |
||
|
|
639d7dac44 |
docs(resilience): describe the queue-wait and execution deadlines as they are (#13624)
* docs(resilience): describe the queue-wait and execution deadlines as they are
Two docs still describe `requestQueue.maxWaitMs` the way it behaved before
the split, in two different and both incorrect ways:
RESILIENCE_GUIDE.md "a legacy persisted name for execution expiration
... bounds limiter-managed execution, not time
spent in the local queue ... Queue residence has
no time deadline"
ENVIRONMENT.md "Max time to wait on a 429 before failing the
request"
`rateLimitManager.ts` does the opposite of the first and has nothing to do
with the second. `maxWaitMs` is the queue-wait budget: it covers the slot
wait plus QUEUED residence, and its timer is cleared the moment the job
starts executing (`wrappedFn`). Bottleneck's `expiration` is fed by
`executionMaxWaitMs` (default 600000ms), "never by the queue-wait budget"
in the source's own words, and is raised to the executor's fetch-start
timeout when that is longer.
Issue #13592 is that misreading in practice: the reporter concluded the
deadline "measures execution time after dispatch rather than queue wait"
-- which is what the guide says.
Also corrected while here:
* The guide's "1-30000ms UI ceiling" is a different setting's bound
(`comboCooldownWaitSettings`). `requestQueueSettingsSchema.maxWaitMs`
is `min(1)` with no max, normalised to 1ms-24h.
* Precedence is documented for the first time: the env var supplies the
DEFAULT only, a persisted `resilienceSettings.requestQueue` value wins
over it, and a per-connection `rateLimitOverrides` value wins over
that. #13592 reports exactly this surprise -- setting
`RATE_LIMIT_MAX_WAIT_MS` on a deployment that already has a persisted
value changes nothing.
Documentation only; no behaviour change.
Refs #13592
* docs(changelog): add fragment for the resilience-deadline doc correction
|
||
|
|
d574826f37 |
fix(usage): detach completed request previews (#13623)
* fix(usage): bound completed request retention Completed request previews used V8 sliced strings that kept multi-megabyte request backing stores alive. Detach and byte-bound cached details, and add a credential-free profiler with cleanup and physical-retention assertions. * fix(usage): give the JON-562 memory-profile canary realistic timeouts The 100k-token worker step alone takes ~230s (tsx/esm boot of the full route/handler module graph plus the real request lifecycle), well past the driver's hardcoded 180s spawnSync timeout — the resulting SIGKILL surfaces as `worker.status === null`, indistinguishable from a real crash. Bump the worker timeout to 300s and the test's own outer/inner timeouts to match the measured ~230-330s real runtime. Also make git-branch provenance detached-HEAD safe: `git branch --show-current` is empty on a detached HEAD (the normal state for a CI PR checkout, and for this fix worktree itself), which made the canary throw "git branch is empty" deterministically outside a regular branch checkout. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
9b84531e4c |
fix(cli): pass --legacy-peer-deps to npm install -g in omniroute update (#13579)
* fix(cli): pass --legacy-peer-deps to npm install -g in omniroute update Problem ------- Running \ pm install -g omniroute\ prints a wall of ERESOLVE / peer-dependency warnings because marked-terminal@7.3.0 declares a peer range of marked>=1 <16, while omniroute ships marked@18. npm's strict peer-resolution mode (the default since npm 7) flags this mismatch loudly even though the packages work correctly together at runtime. Fix --- Pass --legacy-peer-deps to the npm install -g call that \omniroute update\ issues so that every user who upgrades through the built-in updater gets a clean, warning-free output. The dry-run log line is updated to match. Why --legacy-peer-deps is safe here ------------------------------------ The repo already ships .npmrc with legacy-peer-deps=true (added in #11544) so the published package documents this as its supported install mode. This commit simply applies the same flag programmatically in the updater so the flag is always honoured regardless of the caller's local npm config. Changes ------- - bin/cli/commands/update.mjs: append --legacy-peer-deps to execSync npm call and to the dry-run console.log so output matches the real command - docs/guides/TROUBLESHOOTING.md: add a supported install snippet and clarify that residual deprecation notices come from third-party transitive packages - tests/unit/cli-update-npm-win32-11335.test.ts: regression test asserting both the dry-run string and execSync call carry --legacy-peer-deps - changelog.d/fixes/: add fragment (number updated after PR is opened) * chore(changelog): rename fragment to PR #13579 |
||
|
|
f24c3665df |
fix(proxy): skip bare TCP health probe for SOCKS5 data plane (#13571)
* fix(proxy): skip bare TCP health probe for SOCKS5 data plane * docs(changelog): add fragment for SOCKS5 bare TCP probe skip fix Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
70eebe9adb |
fix(arena+analytics): atomic ELO sync (fetch-first) + flatRateAsZero in compression writer (#13446)
* fix(analytics+arena): flatRateAsZero in compression writer; atomic arena sync redesign * docs(changelog): add fragments for arena ELO sync and compression flat-rate fixes Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: CrashCartCapital <crashcartcapital@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
a9c62ba83b |
fix(playground): improve Compare response scrolling and copy actions (#13317)
* feat(playground): copy individual compare responses * docs(changelog): add fragment for Compare column copy-to-clipboard Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
4dc73fb36a |
chore: add Windows helpers to run OmniRoute and Claude Code from a source checkout (#13312)
* chore: add Windows helpers to run OmniRoute and Claude Code from a source checkout Adds contrib/windows/ with two double-clickable scripts for Windows users who cloned the repo instead of installing the npm package: - start-omniroute.bat -> npm run dev (resolves the repo root from its own path) - launch-claude.bat -> node bin/omniroute.mjs launch, forwarding extra args The README documents a fresh-clone gotcha on Windows with npm >= 11: the optional better-sqlite3 dependency is silently skipped, the server falls back to node:sqlite and logs "Module not found: Can't resolve 'better-sqlite3'". Since better-sqlite3@13 ships win32-x64 prebuilds inside the package, extracting the npm pack into node_modules fixes it without a compiler. Verified on Windows 11, Node 24.14.0, npm 11.6.1. * chore(contrib): start Claude Code in the project folder, not the OmniRoute checkout launch-claude.bat used to cd into the repo root before exec'ing `omniroute launch`, so Claude Code always started inside the OmniRoute checkout and loaded this repo's CLAUDE.md/AGENTS.md (~60k chars) into the user's own coding session. Take the project folder as the first argument (or prompt for it on double-click) and resolve bin/omniroute.mjs from the script's own path instead. Remaining args are still forwarded to `omniroute launch`. |
||
|
|
fbe195d69e |
fix(opencode): preserve catalog display names (#13168)
* fix(opencode): preserve catalog display names * test(cli): cover OpenCode catalog display-name precedence Adds the automated unit test the PR body's manual smoke check (Auto Chat / DeepSeek V4 Pro) was standing in for, covering all four name-precedence branches: existing custom name, catalog display_name, native catalog name with owned_by prefix stripped, and the auto/* readable fallback. Also adds the changelog.d/fixes/ fragment. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: ginettododo <117327638+ginettododo@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
93be9d544c |
fix(responses): count input tokens locally for Codex OAuth (#13167)
The ChatGPT subscription backend does not serve /backend-api/codex/responses/input_tokens for the affected account. Native requests to that path are intercepted by an OpenAI Cloudflare managed challenge, while the same path over the bundled Chrome transport returns 404 Not Found. Forwarding the client preflight can therefore never return a useful count and, before the companion classifier fix, permanently disabled the healthy Codex connection on the first challenge. Add a static /v1/responses/input_tokens route that shadows the generic Responses passthrough, uses the existing offline o200k_base token counter, and returns the standard response.input_tokens contract without issuing any upstream request. Count instructions, structured input, tool definitions and config; apply a conservative five-percent margin so the failure mode is earlier client compaction rather than a context-window overflow. Preserve the API-key and model-policy boundary from the catch-all Responses path. Tests pin the public schema, prove fetch is never called, cover text, instructions, structured input, tools, non-text parts, server-held context ids, invalid JSON, OPTIONS, and the conservative lower bound. Co-authored-by: anhth2 <anhth2@vng.com.vn> |
||
|
|
6614780c36 |
fix(deps): declare remark-gfm as a direct dependency (#13162)
MarkdownMessage.tsx and other client components import remark-gfm directly, but it was only present transitively via fumadocs-mdx (a devDependency). npm's hoisting happens to resolve it, masking the issue, but pnpm's strict node_modules isolation fails with "Module not found: Can't resolve 'remark-gfm'" since the package was never declared as a direct dependency of the app. Add it explicitly to package.json/package-lock.json and the supply-chain dependency allowlist. Co-authored-by: marioschoenert-code <291579285+marioschoenert-code@users.noreply.github.com> |