mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
fda9ef78b1686c4d258db2a861b8a7c72fd948cc
1237 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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 |
||
|
|
db5ae3c33d |
docs(dependencies): clarify socket.yml is registry-side scan, not CI gate (#12664)
* docs(dependencies): clarify socket.yml is registry-side scan, not CI gate * docs(dependencies): add changelog fragment for socket.yml scope note Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
d8be3b1a77 |
feat(sse): reserve the Antigravity account for the request's stream lifecycle (re-land of #10011) (#13929)
* feat(sse): reserve the Antigravity account for the request's stream lifecycle Re-land of the account-lease half of #10011 on the current release branch. Its exact-model-scoping half had already shipped in #8050 and its quota half lost to the tip's aggregate-family design (selectAntigravityQuotaWindowNames / antigravityQuotaFamily.ts); none of that is reintroduced here. The lease is a concurrency reservation only and never reads or writes quota state. The Antigravity account selected for a request is reserved for the whole streaming lifecycle of that request, so a concurrent retry — or the credential handoff inside getProviderCredentialsWithQuotaPreflight — cannot re-pick an account already committed to an in-flight upstream stream. The reservation is scoped to (connection, callable upstream model) rather than the whole account, so one account can still serve two different models at once; catalog ids that resolve to the same upstream id (the gemini-3.7-flash tiers, all gemini-3.7-flash-tiered) share one lease. When every eligible account is leased for that model the request returns a structured 503 antigravity_pool_busy with a bounded Retry-After instead of piling onto a busy account. Opt-in behind ANTIGRAVITY_ACCOUNT_LEASE_ENABLED (runtime, default false). With the flag off no reservation is taken, credentials carry no routing descriptor, every release/hold is a no-op on an undefined lease id, and account selection and dispatch behave exactly as before. #10011's original test suite asserted family semantics for a lease that was exact-model scoped and failed deterministically on its own head; the model ids it used (gemini-3.5-flash / gemini-3-flash-agent) no longer exist in the catalog. The contradiction is resolved in favour of one coherent semantic — exact callable upstream model — and the tests assert it against the alias tables as they are on this branch. Co-authored-by: Ardem2025 <openclaw-auto@example.invalid> * fix(sse): widen the Antigravity lease reservation result so auth.ts narrows it The discriminated-union form of reserveAntigravityLeaseForSelection's return type did not narrow under tsconfig.typecheck-api.json, so reading `reserved.lease` after the `reserved.busy` early return raised TS2339 in the API Route Typecheck gate. A single optional-property shape carries the same information and type-checks everywhere. Co-authored-by: Ardem2025 <openclaw-auto@example.invalid> --------- Co-authored-by: Ardem2025 <openclaw-auto@example.invalid> |
||
|
|
6788de8ef9 |
feat(providers): update Openference free models and add Deyin to compatible agents (#13378)
* feat(providers): update Openference free models and add Deyin to compatible agents * docs(providers): regenerate PROVIDER_REFERENCE.md against the current tip Post-merge regeneration so the diff only reflects the Openference free-model addition, not stale eurouter/greenpt/count churn from an out-of-date local generation. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): soften unconfirmed Openference free-forever claim The Openference pricing page (openference.com/pricing) currently lists five paid plans ($15-$120/mo) and no $0 tier in its structured pricing data, so neither the old "3-day trial" note nor a "free forever" claim can be verified against the source. Point readers to the pricing page instead of asserting a specific duration. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: AnhLead <AnhLead@users.noreply.github.com> |
||
|
|
5a82da7084 |
feat(routing): deterministic routing strategies for self-hosted entry (RIC-740) (#13611)
* feat(routing): self-hosted unified OpenAI-compatible entry (RIC-738) Divert /v1/chat/completions through the self-hosted provider adapters when OMNIROUTE_SELF_HOSTED_PROVIDERS / OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE is set: one OpenAI-compatible contract in, auto-route to the selected provider (x-omniroute-provider header, provider/model prefix, or first provider), standard OpenAI error shape out. Optional OMNIROUTE_SELF_HOSTED_API_KEY guards the entry (D5 reserved); unset = open loopback route. Upstream credentials stay runtime-only and are stripped from echoed responses. Brings in the provider-adapters baseline from sibling branch (RIC-737) that this entry depends on. Includes 21 passing unit tests (provider selection, model-prefix forwarding, header hygiene, auth, error normalization, SSE passthrough, fall-through/misconfig), docs, env example, changelog fragment. * feat(routing): deterministic routing strategies for self-hosted entry (RIC-740) Add the M2 deterministic routing strategy engine (D3 可审计路由) to the self-hosted unified entry: a declarative `strategy:` block expressing five explainable, non-predictive policies — blacklist/whitelist hard filters, cooldown circuit breaker, cost-priority, latency-aware ordering, and an explicit fallback chain. The ordered candidate list is the fallback chain: a failed primary (network or non-2xx) falls through to the next candidate and each failure feeds the breaker. Every response carries an x-omniroute-route-decision header answering "why this model / why not that one". A pinned provider rejected by a hard filter returns 400 (never a silent re-route); no eligible providers returns 503 with the full explainable decision. No ML/predict dependency. Covers the RIC-740 acceptance: 5 strategy types with unit tests + HTTP fault-injection tests (primary down -> fallback works), config matching docs, and no predict/ML deps. Adds docs, .env.example entries, and a changelog fragment. * refactor(routing): reduce complexity-ratchet violations in new self-hosted routing files Extract cost/id validation, pin-blocked resolution, ordering, and env/file source resolution into small helpers so routingStrategies.ts and selfHostedEntry.ts stay under the complexity-ratchets cap. No behavior change — the same 51 routing-strategies/self-hosted-entry/provider-adapters tests pass unmodified. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(routing): document the 5 self-hosted env vars in ENVIRONMENT.md check:env-doc-sync failed because OMNIROUTE_SELF_HOSTED_PROVIDERS(_FILE), OMNIROUTE_SELF_HOSTED_API_KEY and OMNIROUTE_SELF_HOSTED_STRATEGY(_FILE) were present in .env.example but missing from docs/reference/ENVIRONMENT.md. Add them under "6. Tool & Routing Policies". 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> Co-authored-by: luyuehm <luyuehm@users.noreply.github.com> |
||
|
|
3ebea07278 |
fix(sse): replay reasoning for Responses-API targets on plain turns and Anthropic clients (#13031)
* fix(sse): replay reasoning for Responses-API targets on plain turns and Anthropic clients
DeepSeek thinking mode requires the reasoning of every prior assistant turn
to be passed back once the request carries `tools`, including turns that
made no tool call. Since #10540 routed opencode-go/deepseek-v4-* to
`/responses`, the reasoning replay cache had two gaps on Responses-API
targets, and clients that drop `reasoning_content` hit intermittent
`400 The reasoning_text in the thinking mode must be passed back`.
1. Plain (non-tool-call) turns are keyed on a digest of the normalized
OpenAI transcript. Both capture sites used `translatedBody.messages` as
the history, which a Responses body (`input`) does not carry, so the
write-time digest never matched the read side. translateRequest now
reports the pivot transcript it digested via `onReasoningReplayHistory`,
and the streaming / non-streaming capture sites digest that transcript.
2. The Responses replay pass was gated on `sourceFormat === "openai"`, so
Anthropic Messages clients (Claude -> OpenAI -> Responses) got no replay
at all. The pass now runs on the OpenAI pivot for every source format,
right before the Responses conversion discards `messages`.
The reported transcript is a shallow snapshot of the digested fields only
and travels through a callback, not the body, so nothing new reaches the
upstream payload.
* docs(changelog): add fragment for #13031
* fix(sse): guard the Responses capture sites and skip plain-turn writes with no history
Review follow-ups for #13031:
- Add tests/unit/chatcore-reasoning-cache-write-guard-responses.test.ts:
runs the real handleChatCore against a mocked opencode-go/deepseek-v4-flash
Responses upstream (JSON and SSE), then asserts the next turn's upstream
body carries the replayed `reasoning` input item. Removing either capture
site fallback turns both cases red.
- Project the reported transcript down to the digested fields only
(tool_calls keep type/name/arguments, ids are dropped) and document that
`content` is shared by reference.
- Skip the plain-turn cache write when the history is empty: a real request
always has a prior user turn, so an empty history means the transcript
could not be recovered and a one-message digest can never match.
- Changelog wording: the pre-fix write digested only the assistant message.
* test(sse): select the /responses dispatch by URL in the Responses replay guard
Review follow-ups for #13031: the guard picks the upstream body by URL
(`/responses`) and asserts exactly one such dispatch per turn instead of
taking the last fetch, the streaming case asserts the same body shape as the
non-streaming one, and the `historyMessages` doc on
NonStreamingClientTranslateInput names the Responses-shaped fallback.
* docs(routing): name the replay-history hand-off without tripping the hook heuristic
The fabricated-docs gate treats any `onXxx` token in prose as a plugin hook
name and flagged `onReasoningReplayHistory` (a translateRequest option, not a
hook). Point at the option's home file instead.
* chore(quality): freeze chatCore.ts at 6159 for the Responses replay wiring
check:file-size in PR mode caps a frozen file at max(frozen, base). The rebase onto
the v3.8.51 tip (
|
||
|
|
6fec29ca2d |
fix(copilot): fallback to copilot-chat on 403 identity denial for standard provider (#13705)
* fix(copilot): fallback to copilot-chat on 403 identity denial for standard provider * fix(copilot): document COPILOT_INTEGRATION_ID, extract identity fallback, add changelog Adds the missing COPILOT_INTEGRATION_ID entry to .env.example (fixes tests/unit/issue-7793-env-doc-sync-repro.test.ts), extracts the GitHub Copilot 403 identity fallback out of open-sse/executors/base.ts into its own module (open-sse/executors/copilotIdentityFallback.ts) to bring the file back under the frozen file-size ratchet, and adds a changelog.d/fixes fragment for the PR. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: tuandinh0801 <tuandinh0801@users.noreply.github.com> |
||
|
|
01b2467d61 |
feat(sse): add LLM Gateway DevPass quota tracking (#12462)
* feat(sse): add LLM Gateway DevPass quota tracking Surface the LLM Gateway DevPass allowance (GET /v1/key) in OmniRoute's quota telemetry, mirroring the OpenRouter API-key fetcher pattern. - llmgatewayQuotaFetcher.ts: fetch + parse the DevPass /v1/key response (decimal-string USD values), exposing two windows — monthly plan credits and the 7-day premium-model window — with a 45s TTL cache. Pay-as-you-go keys (devPlan "none") and 401/403 fail open (no quota). - Register in chat.ts before registerGenericQuotaFetchers + register the named windows for the dashboard cutoff modal. - usage/llmgateway.ts leaf + usage.ts dispatch case so the Limits page renders the monthly + weekly premium rows. - Add "llmgateway" to USAGE_FETCHER_PROVIDERS, USAGE_SUPPORTED_PROVIDERS, PROVIDER_LIMITS_APIKEY_PROVIDERS, and the dashboard label/order map. - tests: 21 cases covering the parser, auth fail-open, cache TTL, window exhaustion, preflight proceed/block, registration, and the usage leaf. * docs(sse): add changelog fragment + codebase-doc entry for llmgateway quota * refactor(sse): register llmgateway quota via quotaTrackersBatch Move the LLM Gateway fetcher registration out of chat.ts (a frozen file-size-baseline chokepoint) into quotaTrackersBatch.ts, the dedicated side-effect module that exists precisely so new fetchers don't grow chat.ts. The batch import runs at module load, before registerGenericQuotaFetchers(), so the bespoke fetcher still wins over the generic path. Fixes the file-size gate (chat.ts must not grow). --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
f1eabd8885 |
fix(sse): stop direct fetch retry reusing pooled flat response-start budget (#13703) (#14047)
resolveDirectHeadersTimeoutMs() (open-sse/utils/directResponseStartTimeout.ts) now bounds only the pooled dispatcher attempt (attempt 0) with the flat OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS watchdog. The fresh-socket retry (attempt 1) is by construction a brand-new socket with no zombie-socket risk (#10214's rationale only applies to the pooled attempt), so when the caller already attached its own deadline signal it now defers to a generous, configurable backstop (OMNIROUTE_DIRECT_RESPONSE_RETRY_TIMEOUT_MS, default 600s) instead of reusing the identical short flat window — fixing spurious 504s on healthy slow-TTFB reasoning models that need well over 60s total for both attempts. Regression test: tests/unit/proxyfetch-direct-response-start-flat-retry-budget-13703.test.ts (RED against unmodified code: retry cut at 81ms against an 80ms flat budget with ~1920ms of caller deadline unused; GREEN after the fix). New env var documented in .env.example and docs/reference/ENVIRONMENT.md. tlsProfileForProvider's return type in proxyFetch.ts is pulled into a named alias so its signature stays on one line under prettier's canonical formatting -- otherwise prettier's mandatory lint-staged reformat of this frozen file grows it past the check:file-size baseline on every future touch. base-red inherited: #14004 (docs env/docs contract, fixed separately in #14022; chatHelpers file-size drift) |
||
|
|
733f4c1d0a |
fix(providers): refresh uncloseai free-model roster after upstream rotation (#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> |
||
|
|
b45e0c69e6 |
feat(security): warn at boot when the inference server is exposed anonymously (#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. #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 #13695 * docs(changelog): add changelog.d entry for #13820 |
||
|
|
010250cf08 |
fix(memory): list provider-node models in the Embedding and Rerank selectors (#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 #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 #13740 fragment |
||
|
|
e20f5f34ea | fix(cursor): preserve native Claude effort model IDs before executor dispatch (#12838) | ||
|
|
190c80dd1b |
fix(sse): prefer cgroup PSI for chat admission (#12562)
Chat admission sampled host-wide /proc/pressure/memory, so a swapping Docker host 503'd idle containers with resource_pressure. Prefer this unit's cgroup memory.pressure and keep the host file as fallback. |
||
|
|
9956f13b35 |
fix(db): never TRUNCATE-checkpoint a live WAL (SIGBUS under traffic) (#14005)
* fix(db): never TRUNCATE-checkpoint a live WAL A live TRUNCATE checkpoint rewrites the shared wal-index while other processes hold it mapped; dereferencing the stale mapping SIGBUSes the process. Two production crashes six hours apart, coredump stack in better-sqlite3 native memcpy (issue #13973). Remove the periodic TRUNCATE scheduler. Runtime checkpoints are PASSIVE-only, which move pages without changing the wal-index geometry, while TRUNCATE stays on the shutdown path where reclaiming the file is safe. The 256MB size guard now warns instead of escalating to a live TRUNCATE, busy PASSIVE ticks feed the persisted busy telemetry that the TRUNCATE tick used to carry, and a positive OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS logs a one-time deprecation warning. Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(db): RESTART the WAL when it exceeds the size guard The 256MB size guard only warned, so a live WAL could keep growing until the next restart. wal_checkpoint(RESTART) starts a new WAL file without rewriting the mapped wal-index, which is what SIGBUS'd the process when we used TRUNCATE under traffic. Related to #13973. Signed-off-by: Minxi Hou <houminxi@gmail.com> * docs: drop a fake TRUNCATE env name from the WAL guard row Backticks around TRUNCATE made the env/docs checker treat it as a variable. The VACUUM rows next to it were never part of this change and are not in the base docs. Signed-off-by: Minxi Hou <houminxi@gmail.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> |
||
|
|
94aa978c2a |
fix(ci): document OMNIROUTE_STRIP_SYSTEM_PREAMBLE — the env/docs base red blocking every PR (#14022)
* fix(ci): document OMNIROUTE_STRIP_SYSTEM_PREAMBLE (env/docs contract base-red) * fix(ci): allowlist COMBO_LOOP_SAFETY_TIMEOUT_MS as a doc-only source constant The env/docs contract gate had a SECOND violation on the release tip, added after this branch was cut: #13857's comboTimeoutMs narrative in ENVIRONMENT.md cites COMBO_LOOP_SAFETY_TIMEOUT_MS, which is a source constant (open-sse/services/combo/comboPredicates.ts:35 — `10 * 60 * 1000`), not an operator-facing env var. The doc regex captured the SHOUTY_NAME and reported it as documented-but-missing-from-.env.example. DOC_ONLY_ALLOWLIST already exists for exactly this class (see CLI_COMPAT_OMITTED_PROVIDER_IDS, LOCAL_ONLY_API_PREFIXES, VACUUM). Gate now reports all three directions in sync. |
||
|
|
4be37d149b |
feat(dashboard): expose comboTimeoutMs next to Target timeout (#13857)
* feat(dashboard): expose comboTimeoutMs next to Target timeout The runtime already applied config.comboTimeoutMs as the whole-combo wall-clock budget (0 = 10-minute hang-stop). Schema treated it as an unknown passthrough key and the dashboard only painted Target timeout, so operators could not raise the 15-step failover ceiling from the UI. Declare comboTimeoutMs on comboRuntimeConfigSchema, mount both knobs in the combo editor Advanced panel and Combo defaults, and keep comboTimeoutMs longer than targetTimeoutMs so failover still has time. Signed-off-by: Minxi Hou <houminxi@gmail.com> * docs(changelog): attach #13857 to comboTimeoutMs fragment Signed-off-by: Minxi Hou <houminxi@gmail.com> * test(dashboard): name comboTimeoutMs store as milliseconds The input is seconds; the stored config field is milliseconds. The old title said "in seconds" while asserting 1_200_000. Signed-off-by: Minxi Hou <houminxi@gmail.com> * chore(i18n,changelog): translate #13857 keys into all locales and drop the CHANGELOG hunk --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
ec780ef2dc |
feat(compression): make Lite tool-result truncation length configurable (#13915)
* feat(compression): make Lite tool-result truncation length configurable Lite truncated tool results at a hardcoded 2000 characters. Coding-agent payloads (file reads, crash dumps) lost the middle of the content with no supported way to raise the cap. Honor lite.maxToolLength from settings, then OMNIROUTE_LITE_MAX_TOOL_LENGTH, then 2000. Existing installs keep the old length. Related to #13178. Signed-off-by: Minxi Hou <houminxi@gmail.com> #13178 stays open. * compression/lite: keep a stored cap when a step or toggle write is incomplete An out-of-range step maxToolLength was still a number, so it hid a valid global cap and fell through to env. A toggle-only settings PUT replaced the whole lite row and dropped the stored cap. Save treated an out-of-range number like a cleared field. Reject the bad Save, merge omitted caps, and use null to clear. Related to #13178. Signed-off-by: Minxi Hou <houminxi@gmail.com> * compression/lite: stop dashboard copy from hard-coding a 2000-char cap The page overlays schema descriptions from i18n. Updating only LITE_SCHEMA left operators seeing "over 2,000 characters" after the cap became configurable. Also assert the Save error string, not the Save button. Related to #13178. Signed-off-by: Minxi Hou <houminxi@gmail.com> * chore(changelog): move #13915 entry to a changelog.d fragment --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
e7872e57c3 |
fix(db): reclaim freed pages incrementally instead of a blocking VACUUM in the cleanup scheduler (#12821) (#12830)
* fix(db): reclaim freed pages incrementally instead of a blocking VACUUM in the cleanup scheduler (#12821) startCleanupScheduler() ran a synchronous whole-database VACUUM on the event loop whenever a cleanup pass deleted at least one row - 30 s after every start and every 6 h. With node:sqlite that blocks every route (/healthz included) for the duration: 7 min 55 s on a 540 MB storage.sqlite to reclaim six rows. It also bypassed vacuumScheduler, the app-level owner of full VACUUMs and the operator's scheduledVacuum / vacuumHour settings. cleanup.ts no longer issues a full VACUUM. After each pass reclaimFreedPages() branches on PRAGMA auto_vacuum: - INCREMENTAL: drain the freelist with PRAGMA incremental_vacuum(N) in ~1 MiB batches (N from page_size), pausing between batches for as long as the last one took (<=250 ms), PASSIVE checkpoint every 64 batches and a TRUNCATE checkpoint at the end so the main file shrinks in WAL mode; hard caps of 2048 batches / 30 s per pass, the remainder waits for the next pass. - FULL: nothing to do, SQLite reclaims on commit. - NONE: incremental_vacuum is a no-op, so record a request via the new vacuumScheduler.requestFullVacuum(); the rebuild runs in the configured window (or via the Storage page button). scheduledVacuum=never is honored. vacuumScheduler persists fullVacuumRequestedAt / fullVacuumRequestReason, clears them on the next successful runNow(), and hydrates from key_value before an early request so it cannot clobber a persisted lastRunAt. Loop robustness: db.exec() rather than pragma() (bun:sqlite's all() steps a zero-column pragma once), SQLITE_BUSY/LOCKED and a handle closed under the pass stop it quietly, other errors stop it with partial progress logged. Also drops the duplicate cleanupProxyLogs() call in the scheduled pass - runAutoCleanup() already covers proxy_logs. Tests: new tests/unit/db/cleanup-reclaim-freed-pages.test.ts (INCREMENTAL drain/pause/checkpoint, page_size-derived batch, caps, FULL no-op, NONE defers and leaves page_count untouched, runScheduledCleanupPass() path); vacuum-scheduler.test.ts covers requestFullVacuum persistence, restart survival and clearing; cleanup-column-fix.test.mjs now asserts incremental_vacuum and the absence of a full VACUUM statement. * chore(changelog): name the #12821 fragment after its PR (#12830) * fix(db): extract reclaimFreedPages into its own module and fix full-suite regressions Split the #12821 incremental-vacuum reclamation logic out of cleanup.ts into src/lib/db/reclaimFreedPages.ts (re-exported for callers/tests) so cleanup.ts stays under the file-size cap after the #13011 reconciliation merge grew it past the 1200-line threshold. Also fixes two full-suite failures surfaced by running the cleanup/vacuumScheduler/db-health suite post-merge (not just this PR's own 3 test files, per the plan-file's mandatory item): - tests/unit/cleanup-column-fix.test.mjs scanned cleanup.ts's raw source for the PRAGMA incremental_vacuum invariant, which now lives in the extracted module — updated to scan both files. - tests/unit/db/cleanup-reclaim-freed-pages.test.ts asserted the freelist count is byte-for-byte unchanged when auto_vacuum=NONE. The tip's runAutoCleanup() now also runs cleanupCompressionRunTelemetry(), which lazily creates its table on first use (ensureCompressionRunTelemetryTable) — a legitimate one-time page cost from a freshly migrated DB, unrelated to reclaimFreedPages()'s own behavior. Loosened the assertion to a small tolerance while keeping the page_count assertion that actually guards against a full rebuild. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(db): drop the reclaimable-bytes VACUUM gate test superseded by incremental reclaim tests/unit/vacuum-reclaimable-threshold.test.ts pinned cleanup.ts's vacuumAfterCleanup()/getReclaimableBytes()/getVacuumMinReclaimableBytes() (#13079). This branch removes the inline post-cleanup full VACUUM entirely in favour of reclaimFreedPages() (#12821), which reads the same freelist_count / page_size signal and defers a full VACUUM to the vacuum scheduler when auto_vacuum=NONE. With those three exports gone the file cannot compile, and the behaviour it guarded no longer exists. --------- Co-authored-by: insoln <is@careerum.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
dd70dbdaa0 |
fix(sse): Anthropic OAuth 403 "Request not allowed" is a per-request refusal — cooldown with backoff instead of an instant ban (#12859) (#12864)
* fix(sse): Anthropic OAuth 403 "Request not allowed" is a per-request refusal, not a ban
A single upstream 403 on the `claude` OAuth connection was classified
FORBIDDEN and written as the terminal `banned` connection state
(chatCore -> writeTerminalStatus). From then on every request to that
provider was short-circuited with "All 1 connection(s) banned by
upstream - please reconnect in the dashboard" without touching Anthropic,
until an operator reconnected.
Anthropic's OAuth surface answers a small fraction of otherwise-valid
requests with 403 {"type":"permission_error","message":"Request not
allowed"}. On the reporting install the same token returned 200 forty
seconds before the 403 and again right after the connection was
re-enabled; a revoked or expired token is a 401 authentication_error, not
this. It is a refusal of one request, not of the credential.
Classify it as the new non-terminal PROVIDER_ERROR_TYPES.REQUEST_REJECTED
(scoped to provider `claude` and the "Request not allowed" body) and list
that type in authTerminalStatus.isNonTerminalProviderError, mirroring the
Cloudflare FINGERPRINT_REJECTION precedent. The combo layer still falls
through to the next target for the failing request; the connection stays
active for the next one. Any other claude 403 keeps its previous
classification.
Tests: error-classifier.test.ts covers the Anthropic body, the
gateway-flattened "[403]: Request not allowed" message, the same body from
a non-Anthropic provider (still FORBIDDEN), other claude 403s (unchanged),
and the helpers; anthropic-request-not-allowed-not-a-ban.test.ts pins
resolveTerminalConnectionStatus() -> null for the new type even with a
`permanent` fallback verdict, and `banned` for a generic claude 403.
* fix(sse): cooldown with backoff and streak escalation for REQUEST_REJECTED (#12859)
Not "ignore the 403" either: if Anthropic ever made "Request not allowed"
systematic, re-sending every request into it would be the wrong thing to
do to an OAuth account. chatCore now handles REQUEST_REJECTED explicitly:
- exclude the connection via setConnectionRateLimitUntil for a growing
cooldown (5 -> 15 -> 45 min) so a sporadic refusal costs minutes, not a
reconnect, and a systematic one cannot become a stream of 403s;
- escalate to the terminal `banned` state only for 3 refusals within a
60-minute window (services/requestRejectedStreak.ts, in-memory per
connection; a restart forgets the streak, erring towards more cooldowns
rather than an operator-undone ban), with a last_error that says so;
- probe-origin failures record but never cool down or ban (#9817).
The existing "request not allowed" text rule (5 s) is unaffected:
markAccountUnavailable skips a connection that already has a future
rateLimitedUntil, so the minute-scale cooldown written here wins.
Tests: request-rejected-streak.test.ts pins the window/threshold/backoff
arithmetic; anthropic-request-not-allowed-cooldown-escalation.test.ts drives
the real chat route against a mocked 403 upstream on a `claude` OAuth
connection: 300 s cooldown, then 900 s, then banned on the third refusal;
a different claude 403 body still bans on the first response.
* chore(changelog): name the #12859 fragment after its PR (#12864)
* refactor(sse): move the REQUEST_REJECTED branch into a chatCore leaf; register its tests for mutation coverage
chatCore.ts is frozen at 5984 lines by the file-size ratchet; the branch
body now lives in open-sse/handlers/chatCore/requestRejectedFailure.ts
(chatCore: 5974 -> 5983). stryker.conf.json tap.testFiles gains the two new
DB-backed tests so their mutant kills count (check:mutation-test-coverage).
* fix(sse): count refusal episodes, reset on success, keep the dashboard honest (#12859 review)
Review findings on the first cut of the REQUEST_REJECTED handling:
- A burst of in-flight requests that all got the 403 within seconds
produced streak 1, 2, 3 and a ban from one upstream event. The streak
now counts cooldown *episodes*: a refusal that lands while the
connection is already excluded is the same event and is not counted.
- Nothing reset the streak on a healthy response, so sporadic refusals
on a busy install could still accumulate to a ban. chatHelpers'
onRequestSuccess now clears it (only a real success does - the recovery
tick's clearAccountError is an elapsed cooldown, not a success).
Clearing the cooldown by hand in the dashboard clears it too.
- The third rung of the ladder was unreachable (the third refusal
escalates): the ladder is now 5 -> 15 min, sourced from COOLDOWN_MS next
to the existing 5 s "request not allowed" rule, with a note on why that
rule is superseded for claude. The 60-min window becomes a 24 h
staleness bound - "consecutive" is defined by successes, not by time.
- Probe-origin refusals no longer touch the streak (#9817).
- The cooldown is written like every other connection-level cooldown:
ISO rateLimitedUntil + testStatus "unavailable" (+ lastErrorAt), so the
dashboard shows the countdown and the recovery tick restores "active".
- One refusal is re-seeded from the persisted row after a restart so a
crash loop cannot reset the count on every boot.
Docs: RESILIENCE_GUIDE terminal states + CODEBASE_DOCUMENTATION resilience
row mention the streak module. Tests cover the burst, the success reset,
the seed, and the ISO/unavailable shape end-to-end through the chat route.
* chore(sse): drop unrelated Prettier churn in auth.ts / providers route
* style(api): keep providers route Prettier-clean
* refactor(sse): share the "exclude connection for a cooldown" leaf between GEO_BLOCKED, GCP_PROJECT_REQUIRED and the new branch
The release tip moved chatCore.ts to its frozen 5984 lines, so the
REQUEST_REJECTED branch cannot add a single net line. The GEO_BLOCKED and
GCP_PROJECT_REQUIRED branches were the same eight statements with different
constants and log wording; both now call
open-sse/handlers/chatCore/connectionCooldown.ts::excludeConnectionForCooldown
(behaviour, probe guard and log lines preserved verbatim). chatCore.ts ends
9 lines below the base it branched from.
* chore(chatCore): tighten the cooldown comments to keep the file under its size ceiling
After merging release/v3.8.51, chatCore.ts sat at 6150 lines against a
frozen ceiling of 6146. Condense the explanatory comments this PR added
to the GEO_BLOCKED and GCP_PROJECT_REQUIRED branches; no code change.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: insoln <is@careerum.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
47159ed56b |
fix(combo): answer 503 + Retry-After, not 404, when a weighted pool is only cooling down (#12956)
* fix(combo): answer 503 + Retry-After, not 404, when a weighted pool is only cooling down The weighted strategy filters targets before dispatch (open circuit breaker, provider cooldown, model lockout, availability probe) and drops them silently. When that emptied the pool the host returned the 404 "Combo has no executable targets" with the "switch combo / reconnect the missing providers" recovery hint — for a pool that was configured, connected and merely cooling down. Claude Code renders a 404 from /v1/messages as "this model may not exist". - targetResolution.ts: the eligibility predicate now reports which gate excluded a target and, for the resilience gates, the remaining time; the exclusions of fully-excluded steps travel out of resolveWeightedSelection. When the weighted pool ends empty and at least one exclusion is a resilience timer, the pipeline returns an early 503 and logs the reasons at warn level. - pinRecovery.ts: buildAllTargetsCoolingDownResponse() — 503 `all_targets_cooling_down`, Retry-After = earliest exclusion to lapse, every excluded target in diagnostics.excluded, `wait` recovery hint with retry_after_seconds; formatPreDispatchExclusions() for the log line. - error.ts: `all_targets_cooling_down` joins the public error identifiers. - A pool emptied only by the availability probe keeps the 404. - docs: RESILIENCE_GUIDE debugging entry; changelog fragment. * chore(changelog): name the fragment after PR #12956 and link issue #12954 * refactor(combo): keep weighted exhaustion below complexity ratchet --------- Co-authored-by: insoln <is@careerum.com> |
||
|
|
bc7f68fb91 |
fix(resilience): lock the exact model, not the quota family, on 5xx model-lockout failures (#12957)
* fix(resilience): lock the exact model, not the quota family, on 5xx model-lockout failures A 5xx model-lockout failure — a transport error (terminated, EHOSTUNREACH, connect timeout), an upstream server error, or OmniRoute's own synthesized 502 from quality validation — is evidence about one model endpoint at that moment, not about the account's quota family. recordModelLockoutFailure() wrote it under the quota-family key regardless, so for codex (whose family key is the whole `codex` scope, i.e. every gpt-5* model) one empty stream on gpt-5.6-luna removed gpt-5.6-sol and gpt-5.6-terra from routing too, for 2–30 min with exponential escalation, while the quota was untouched. - exactModelLock.ts: resolveLockoutScope(status, explicit) — 429/403/402 (and 404, already narrowed by getModelLockKey) keep the family key; any other status uses the exact provider/connection/model key. An explicit `scope` option still wins. - recordModelLockoutFailure() resolves the scope once for key + lock fn. - decayModelFailureCount() now walks every key shape (family, not_found, exact) so success-decay reaches exact-scope locks; null model stays a no-op. - getAllModelLockouts() parses the `exact:` marker out of the key so the Model Cooldowns card lists the bare model and can clear it by that name. - docs: RESILIENCE_GUIDE §3 key-scope-by-status; changelog fragment. * chore(changelog): name the fragment after PR #12957 and link issue #12955 --------- Co-authored-by: insoln <is@careerum.com> |
||
|
|
7e0c9f526a |
feat(sse): allow disabling conversation tracking (#13150)
* feat(sse): allow disabling conversation tracking * docs: document OMNIROUTE_DISABLE_CONVERSATION_TRACKING |
||
|
|
2fa6ef0bdd |
security(runtime): harden TLS provenance, lifecycle, and public error boundaries (#11742)
* security(deps): pin and verify tls-client native artifacts * docs(changelog): link tls-client provenance PR * security(runtime): harden TLS and public error boundaries * security(runtime): resolve CodeQL error-boundary findings * security(lmarena): close public stream error boundary * fix(lmarena): normalize public error statuses * chore(quality): rebaseline chatCore.ts for the surviving log-boundary hardening open-sse/handlers/chatCore.ts 6219 -> 6287. This is the one part of #11742 that survived the rebase: sanitizeErrorMessage on the plugin onError hook, on the semaphore-timeout path and on failureMessage before it reaches console.log and the call log, sanitizeUpstreamDetails on the malformed-response log, and getSafeErrorMetadata + try/catch where hostile (Proxy) metadata could throw. That is the LOG boundary, which is broader than Hard Rule #12 (responses). The rest of the PR was dropped as already landed on the tip. |
||
|
|
83fa4328f3 |
feat(providers): add xKiro (#12648)
* test(catalog): pin the 2026-09-02 free-tier re-audit facts for gemini, ollama-cloud, groq, nara and mistral * feat(providers): add xKiro (5M tokens/day free plan, 39 pinned free models) * fix(catalog): re-audit gemini, ollama-cloud, groq, nara and mistral against official pages * docs(providers): xKiro in the provider reference, counts and free-tier headline (~1.66B) * fix(catalog): restore the console-verified Mistral 1B pool and harden its regression test * docs(free-tiers): move headline to the re-audited ~1.50B and refresh pool counts * chore(free-tiers): retire stale Groq free-tier text and preset model; fix catalog header * docs(providers): align the remaining visible provider/executor counts with the catalog * docs(providers): align remaining free-tier count chips and metadata * docs(free-tiers): state the evidence-comment rule honestly and retire the last "14.4K RPD" Groq texts * docs(free-tiers): retire the stale Gemini onboarding quota text * docs(free-tier): refresh catalog-entry counts to 442 after base sync * docs(providers): re-sync provider and free-tier counts after merging release/v3.8.51 * docs(providers): re-sync residual counts after the base merge * docs(free-tiers): restore README spacing lost in the merge and re-sync the guide counts * docs(free-tiers): re-sync numbers after merging release/v3.8.51 (Cerebras reclassified upstream) * fix(docs): keep the NaraRouter plans endpoint out of the API-path checker; rebaseline gateways.ts (+3) * chore(quality): rebaseline gateways.ts file-size cap for the xKiro entry (+20) --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
b6975537c1 |
fix(providers): remove the chipotle/pepper provider (#13131) (#13913)
* fix(providers): remove the chipotle/pepper provider (#13131) amelia.chipotle.com (the reverse-engineered Amelia chat-widget backend chipotle/pepper-1 talked to) now returns 404 on every route, including root, from its Azure Application Gateway — confirmed live 2026-09-15. This regressed from a WS handshake timeout (#4037, June 2026) to a fully decommissioned host, so the upstream protocol cannot be fixed. Owner decided to retire the provider entirely (Option B), following the phind/kluster quiet-removal precedent: no REMOVED_PROVIDERS.md entry (reserved for operator takedowns), just a one-line note under FREE_TIERS.md "Removed / no free tier". Removed every surface: executor, registry entry, executors/index.ts and providers/index.ts wiring, noauth provider catalog entry, ProviderIcon generic-fallback set, the autoCombo exclusion-list comment, the chipotle_error code from the sanitizer allowlist, PROVIDER_REFERENCE.md (regenerated), and every doc/test reference. Regression test: tests/unit/issue-13131-chipotle-provider-removed.test.ts asserts the provider is fully gone from the executor registry, the provider REGISTRY and the noauth catalog, and that the executor module no longer resolves — not a live-network repro (flaky/third-party). Several existing tests used "chipotle" only as a generic noAuth-provider example (proxy scoping, error classification, onboarding, fallback text) with no chipotle-specific behavior under test; those were re-pointed at another still-existing noAuth provider (cloudflare-playground / duckduckgo-web) rather than weakened. * test(providers): document the agnes-cn/chipotle count coincidence (#13131) provider-node-reserved-prefix.test.ts's REGISTRY id+alias walk was already red on the base tip (414 vs. expected 412) from agnes-cn (#13399, +id/+alias). Removing chipotle's REGISTRY id/alias in this PR nets it back to 412, making the test pass again without a numeric edit — record why in a comment so it doesn't read as an untracked coincidence later. |
||
|
|
d6f720bceb |
feat(i18n): new-key gate rejects __MISSING__ markers; skills translate new keys in parallel (#13996)
On 2026-09-16 eight feature PRs added 61 keys to src/i18n/messages/en.json and stamped `__MISSING__:<en>` into all 65 locales instead of translating. check-new-key-coverage accepted the marker as "the key reached the locale", so nothing blocked the PRs, and the blocking real-translation ratio gate then failed on the release tip for everybody (pt-BR 3.2 % > 2.5 % + 0.5). - scripts/i18n/check-new-key-coverage.mjs: a leaf whose value starts with `__MISSING__:` is judged exactly like an absent leaf; the FAIL message names the marker as the cause and prints the per-locale sync-ui-keys command and the parallel runner. Header/JSDoc updated. - tests/unit/i18n-new-key-coverage.test.ts: "a new key that only carries a __MISSING__ marker is flagged" (was the inverse case, which encoded the old contract); the other six cases unchanged and green. - scripts/i18n/translate-new-keys.sh (+ `npm run i18n:translate-new-keys`): committed, detached-safe runner — flock queue, N workers (default 5), 3 attempts per locale of `sync-ui-keys.mjs --translate-markers --batch-size=40`, per-locale logs/.exit + batch.log/batch.status/batch.rc/batch.pid under _artifacts/i18n-new-keys/, non-zero exit while any locale still carries a marker, refuses to start (exit 2, names the five OMNIROUTE_TRANSLATION_* vars) when the backend env is absent. Reads only the OMNIROUTE_TRANSLATION_* lines of the repo .env; kills nothing, matches nothing by name. - docs: QUALITY_GATES.md (gate table + check-new-key-coverage section) and I18N.md (gate table + "Translating the keys a branch adds" subsection). The implementation/port/merge skills reference the new shared snippet `.agents/skills/_shared/i18n-translate-new-keys.md` (skills repo, separate). |
||
|
|
c3e966eeb9 |
docs(changelog): reconcile the v3.8.51 living section — round 2 (2026-09-15) (#13731)
Second `npm run release:reconcile` pass on `release/v3.8.50..release/v3.8.51` (091589089c..c0f92ec98a, 916 non-merge commits, 877 merged PRs): - fold the 173 changelog.d fragments accumulated since #12971 under `## [3.8.51]` and delete them - generate bullets for the 58 cycle commits that had no fragment (4 features / 42 fixes / 12 maintenance), each with the merged PR link and `— thanks @author` - link 137 fragment bullets to the PR of the commit that added them and credit the author; two prefix/origin mismatches reviewed (#12945→#13392, #13001→#13379, both maintainer rebaselines of other people's PRs) - refresh "Release by the numbers" + Top-25 and regenerate the `### 🙌 Contributors` hall (112 external contributors + maintainer; every non-bot author of the 877 merged PRs present) - closed-PR credit audit for the window: nothing to add (#13215→#13361 and #13059→#13690 are still open, #12998 was independently fixed earlier by #12853); no human co-author trailers, no commits without a PR - resync the 58 i18n CHANGELOG mirrors Gates: check:changelog-integrity OK, check:docs-sync PASS. |
||
|
|
21772f40f3 |
fix(security): generate a random per-install CLI token salt (#13679) (#13909)
Both src/lib/machineToken.ts::getActiveSalt() and its mirror in
bin/cli/utils/cliToken.mjs derived the CLI/management bearer token as
HMAC-SHA256(raw machine-id, salt) with a checked-in literal default salt
("omniroute-cli-auth-v1"). Since /etc/machine-id is commonly world-readable,
any local user who never set OMNIROUTE_CLI_SALT could derive the same
bearer token as the server.
getActiveSalt() now generates a random 64-char-hex salt on first use and
persists it under <DATA_DIR>/cli-token-salt.json (falling back to the
literal only when neither the env override nor a persisted/writable salt
can be established). Both implementations use the same resolution order
and the same wx-flag create-race handling so the CLI and server keep
deriving the same token. OMNIROUTE_CLI_SALT stays the explicit operator
override, unchanged.
Regression test: tests/unit/machine-token-random-salt-13679.test.ts
|
||
|
|
3e080877f2 |
fix(sse): bound active streams without terminal events (#12913)
* fix(sse): bound active streams without terminal events * fix(sse): derive the active-stream ceiling from the largest registered model budget The watchdog is a hard lifetime cap that never resets on bytes, so a flat 15-minute default killed models the registry already allows to run for 20 minutes (the Codex entries declare timeoutMs: 1_200_000). The default is now that maximum plus a one-minute margin, and a new test re-derives the maximum from the registry so a future larger budget fails the gate instead of silently re-opening the bug. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
0d089e7e39 |
fix(quality): clear the release/v3.8.51 base-reds (#13947)
* fix(quality): clear the release/v3.8.51 base-reds
19 failing unit tests plus the API Route Typecheck and mutation-test-coverage
gates, all reproduced on the clean tip before touching anything.
Ten of the failures share one cause. #13452/#13798 made `*-compatible-*`
buildUrl() refuse a connection with no baseUrl instead of quietly defaulting to
the real OpenAI/Anthropic API — which would ship the operator's stored key to a
public third party. The guard is right; three fixtures still built those
connections unhydrated, and one of them put baseUrl at the top level of
credentials, where the chat path never reads it.
The rest:
- modelDiscovery.ts missed the VertexModelMetadataProvenance cast that its
read-path twin in db/models/synced.ts already had — both written by #12471.
- A provider-test regexp carried raw 0x00/0x1f bytes, which makes git, GitHub
and ripgrep treat the file as binary. Same character class, written
with escapes instead of the bytes themselves.
- #13399 (Agnes AI China) adds "agnes-cn" + "agnescn": the only two provider
prefixes since the count was last set (412 -> 414). Everything else added in
that range is model ids.
- The free-tier budget card SVG was stale (443 -> 452 models); regenerated by
its own script.
- Three new tests were missing from stryker.conf.json tap.testFiles, so the
mutants they kill did not count.
Three guards asserted syntax rather than the invariant they protect, and broke
when the source legitimately changed. Each was re-expressed and then verified by
mutating the source back:
- #2331 required modelEffort to head the rawEffort chain; #13556 deliberately
put the server-selected force rule first. The real invariant is relative —
modelEffort outranks the defaults a client injects — and it still trips when
explicitReasoning is moved ahead of it.
- The OAuth loopback guard matched the isLocalhost arm literally; #9944 added
`&& !opts?.manualLoopback`. It now matches the arm whatever guards it, and
still fails when the hint stops being built.
- The i18n scanner flagged dynamically-built keys — t("effort." + mode) reaches
it as a literal prefix, never a string. It now accepts a prefix that resolves
to a namespace holding messages, and still fails when the namespace is gone.
tests/unit/sse-auth.test.ts (#12080) expected a bare null where #13879 now
returns the key-policy diagnostic — the same sentinel shape the terminal-state
path has used since #12441. The assertion was rewritten to the constraint #12080
actually protects: nothing usable comes back and neither connection leaks. The
contract risk that remains — those sentinels are truthy, and executeWebSearch
treats any truthy value as a credential — is filed as #13945 rather than
widened into this PR.
Refs #13866
* fix(quality): clear the second wave of release/v3.8.51 base-reds
The tip moved 13 commits while the first pass was running and brought its own
reds. All reproduced locally on the merged tree first.
vitest 4.1.11 -> 5.0.0 in the #13661 development-group bump is a major, and
vitest 5 moved `vite` from a dependency to a peerDependency. This repo only ever
declared `vite` under `overrides`, which pins a version but installs nothing, so
`npm ci` stopped providing it and the Vitest job died at startup with
ERR_MODULE_NOT_FOUND. Declared as the devDependency it actually is — the same
^8.0.16 the override already pinned, and what @vitejs/plugin-react asks for as a
peer — and regenerated the lockfile: 684 lines added, none changed.
#12909 filtered a mapped array with `toolCall is JsonRecord`, but the element
type is the tool-call literal or null, and a predicate's type has to be
assignable to the parameter's (TS2677). Narrowed by the element's own type
instead; the literal still satisfies JsonRecord at the return.
#12906 added `|| result.errorCode === "empty_response"` to the stream-failure
condition and Prettier rewrapped it, so the #8928 probe — which located the
branch by an exact four-line string — stopped finding it. It now matches on what
the branch tests rather than how it is typeset, and still fails when the
eviction call is removed.
probe-7293 is the visible half of a real conflict, filed as #13948. #7293 merges
a mid-array system into index 0; #12908, landed later, demotes it to "user" in
place instead. Both target the same constraint and only one can win, and the
combination also reorders: the pre-translation hoist moves the turn forward
expecting it to stay a system message, then the demotion converts it where it
now sits, ahead of the conversation. Choosing between the two strategies is a
product call, not a base-red one, so the test was realigned to assert the half
that protects the caller — the instruction survives, as a user turn — and pins
the current ordering with a pointer to the issue, so the eventual decision shows
up as a deliberate test change instead of a silent regression.
Refs #13866, #13948
* fix(quality): allowlist vite, rebaseline tip growth, drop a dead import
Third pass on the release/v3.8.51 base-reds. Declaring `vite` in the previous
commit was correct but incomplete: check-deps is a human review point against
typosquatting, so a newly declared package has to be vouched for by name.
Recorded in dependency-allowlist.json with why it is needed — the official Vite
build tool, already pinned through overrides, and a required peer of both
vitest 5 and @vitejs/plugin-react. That also turns check-deps.test.ts green.
check-file-size went red on nine files. One is mine: sse-auth.test.ts grew when
the #12080 assertion was rewritten. Three of the four assertions I had added
were redundant with the strict deepEqual that follows them, so they are gone and
the file grows by 4 lines instead of 8; the cap absorbs the rest.
The other eight are production and test files this PR does not touch, grown by
other work and never rebaselined — which is the whole reason a base-red drain
exists. Each is attributed to the commit that grew it: #12906 (chat.ts,
chatHelpers.ts, proxyFetch.ts, stream.ts), #12904 + #12910 (chatCore.ts), and
batch_api.test.ts from the same wave. Two of them predate the wave entirely and
were already over cap on
|
||
|
|
b637350680 |
fix(docs): re-sync the 65 documentation mirror sets; section-level docs pipeline; drift gate blocking (#13940)
1,104 mirrors rewritten over five passes of run-translation on the 22-source core set: the 14 sources edited since their translation, the 322 mirrors that were still English copies, and the frontmatter the old extractor leaked into the newer locales' bodies. The pipeline now caches per-`## `-section hashes and retranslates only changed sections, never reuses a section that is still English, rebuilds English-copy / leaked mirrors even when the source is unchanged, merges the state on save (parallel runs), and the drift gate (scoped to the core set) is blocking. Final audit: 0 stale, 0 English copies, 0 leaked frontmatter across 1,430 core mirrors. ⚠️ base-red inherited: #12732 |
||
|
|
f3acf4f811 |
fix(sse): inject global system prompt once, post-translation, across all target shapes (#12904)
* fix(sse): inject global system prompt post-translation for codex/Responses path codex/Responses requests carry input[]+instructions, not messages[]. The existing injectSystemPrompt runs PRE-translation (chatCore.ts) and only handles messages[]/system fields, so the Global System Prompt (After Prompt = suffixPrompt) never reached the provider for codex — verified 0/84 call logs while the catalog base_instructions reached 84/84. Add injectSystemPromptPostTranslation() and call it after prepareUpstreamBody on the resolved messages[]. With multiple system/developer messages (codex normalises its per-item developer roles to system), prefix goes on the FIRST and suffix on the LAST so the After Prompt retains the highest recency position — the semantics injectSystemPrompt's single-findIndex buries. Also wire OMNIROUTE_SYSTEM_INSTRUCTION_APPEND on the /v1/messages (Claude Messages -> OpenAI Chat Completions) translation path. The directive was previously only wired on the Responses API path, so DeepSeek-V4 kept leaking English planning/chain-of-thought into the content field on Claude Code sessions that route through /v1/messages. Mirror the openai-responses.ts pattern: append to string system, append a text block for array content, or unshift a new system message when none exists. Tests: 23/23 (19 system-prompt incl. 6 postTranslation + codex regression; 4 claude-to-openai directive append). typecheck:core clean. * test(sse): reproduce global prompt double injection — single-injection contract tests * fix(sse): unify global prompt injection to single post-translation pass * fix(sse): carry single global-prompt injection across claude/gemini/responses target shapes * fix(sse): restore global-prompt coverage for carrier-less targets via gated pre-translation pass * fix(sse): cover codex/gemini source shapes in the carrier-less pre-translation gate * fix(types): preserve generic system prompt return * fix(sse): correct file reference in claude-to-openai.ts comment and add changelog fragment Points the #reasoning-bilingual comment at the real companion file (translator/response/openai-to-claude.ts's directivePreambleStripper.ts from #12905) instead of the nonexistent "openai-responses.ts", and adds the changelog fragment referenced in the PR body but missing from the diff. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Jihyun Son <jihyun.son@sk.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
5acac8021d |
fix(redis): namespace warmup circuit-breaker keys with REDIS_KEY_PREFIX (#13328)
The warmup scheduler's circuit-breaker keys were written to Redis without `REDIS_KEY_PREFIX`, so they escaped OmniRoute's namespace and could collide with another app sharing the instance — the one Redis surface the prefix wasn't reaching. Probe: 2/2 pass in `tests/unit/lib/warmupScheduler/redisCircuitBreakerStorePrefix.test.ts`, covering both the prefixed case and the unset/blank case where keys must stay unchanged.
**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.
- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
**Reconciled** — this PR was `CONFLICTING`. The conflict was in `docs/reference/ENVIRONMENT.md` and purely additive: the release tip had inserted `APP_BIND_HOST` / `QDRANT_BIND_HOST` / `BIFROST_BIND_HOST` rows directly above the `REDIS_KEY_PREFIX` row you edited. Kept both sides — the tip's three new rows and your updated description naming the warmup circuit breaker — then merged the current release branch in (
|
||
|
|
af2002a493 |
chore: reconcile the JxnLexn merge wave with the release tip (#13921)
Lands the combined-board reconciliation of today's JxnLexn wave as one follow-up: i18n fill for #12471/#13555's new keys (real vi translations), free-tier count 446→452, file-size rebaseline for #13556. check-new-key-coverage PASS; only the three pre-existing file-size reds remain. |
||
|
|
1cf8e4bcc6 |
fix(db): auto-clean terminal batch checkpoints and expired file content (#12999)
Merged after a maintainer rework that kept every one of @hartmark's commits intact. **What the rework added:** the auto-clean of terminal batch checkpoints and expired file content is gated behind a default-off feature flag (`BATCH_AND_FILE_AUTO_CLEANUP_ENABLED`, `defaultValue: "false"`, documented in `docs/reference/FEATURE_FLAGS.md` and described in all 66 locales) so the release default keeps today's behaviour and operators opt in; the DB handle leak in the test was fixed so the Node runner exits cleanly. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you — the cleanup itself is exactly the kind of maintenance that stops a data dir from growing forever. |
||
|
|
23c5772ccb |
feat: adaptive reasoning effort (auto) — gateway-resolved, per-turn pinned, all harnesses (#13448)
Merged after a maintainer rework that kept every one of @patrykkopycinski's commits intact — including the two refactors you pushed later (extracting the adaptive-effort wiring out of `chatCore.ts` and reading `x-omniroute-effort` inside the wiring module), which were merged into the rework rather than overwritten. **What the rework added:** the adaptive-effort wiring is scoped to OpenAI-dispatch requests only (the claim in `docs/routing` was corrected to match), and `defaultReasoningEffort` was widened to accept `auto` explicitly instead of relying on a loose string. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you — gateway-resolved, per-turn pinned effort is a real feature, and the header contract makes it usable from every harness. |
||
|
|
88d5e0cde6 |
fix(db): gate the auto-cleanup VACUUM on reclaimable space, not row count (#13079)
Merged after a maintainer rework that kept every one of @hartmark's commits intact. **What the rework added:** the reclaimable-space gate for the auto-cleanup VACUUM sits behind a default-off feature flag so the release default is unchanged, with the flag documented in `docs/reference/FEATURE_FLAGS.md` and described in all 66 locales; the rest is your change as submitted. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you — gating VACUUM on reclaimable pages instead of row count is the right signal. |
||
|
|
b269821c83 |
fix(cursor): kv_after_text must not settle away a trailing exec_mcp tool call (#13627)
Merged. Settling on `kv_after_text` while a trailing `exec_mcp` call is still pending drops the tool call entirely — the client then sees a finished turn that never ran the tool. Correct place to fix it. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you. |
||
|
|
4621842d93 |
feat(reasoning): opt-in min output budget floor for thinking models (#12742)
Merged. Opt-in is what makes this safe to ship: thinking models that need a floor get one, everyone else sees no change in behaviour, and the env var is documented in `.env.example` and `ENVIRONMENT.md` rather than being folklore. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you. |
||
|
|
e7999c477b |
fix(db): bound health scans and isolate native diagnostics (#13717)
Merged after a maintainer rework that kept every one of @HouMinXi's commits intact. **What the rework added on top of the contribution:** the new DB health-check behaviour is gated behind a default-off feature flag (`src/shared/constants/featureFlagDefinitions.ts`, `defaultValue: "false"`), documented in `docs/reference/FEATURE_FLAGS.md` with the description key carried into all 66 locales, so the release default is unchanged and the new bounds only apply when an operator opts in. The optional-FTS5 migration set was reconciled by hand with the "180" entry that landed meanwhile (`src/lib/db/migrationRunner/constants.ts`). **Carried from your rebased head:** the `/api/db/health` local-only classification in `src/server/authz/routeGuard.ts` plus its `routeGuard` assertion — `runManagedDbHealthCheck()` forks native diagnostics into a child process, so Hard Rules #15/#17 apply. Re-verified here: 37 pass / 0 fail. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you for the depth of this one — the resource-bounds suite and the sql.js startup/backup coverage are the kind of tests that keep a database layer honest. |
||
|
|
9d9284417c |
fix(cli): translate the CLI for every locale (38 catalogs were nearly empty) (#13892)
sync-ui-keys --catalog=cli + blocking i18n:check-keys:cli gate; 65 CLI catalogs synced (52,000 strings, 0 __MISSING__, all 830 keys, placeholders verified). ⚠️ base-red inherited: #12732 |
||
|
|
5faf44f975 |
fix(docs): restore the env/docs contract broken by the #13679 vars (#13875)
`check:env-doc-sync` is failing on the release tip, which fails "Docs Gates (fast-path)" on every open PR against release/v3.8.51 (base-red #13866). Both gaps come from #13679: - `OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE` is read in src/lib/cloudSync.ts but was in neither .env.example nor ENVIRONMENT.md. Documented with the behaviour the code actually implements: opt-in rejection of an UNSIGNED response when no local secret is configured, default off for v3.8.x back-compat, and a present signature always verified — and always rejected when OMNIROUTE_CLOUD_SYNC_SECRET is unset — regardless of the flag. - `CDP_PROXY_TOKEN` was in .env.example but missing from ENVIRONMENT.md. Added to the ChatGPT Web (Codex) table next to CHATGPT_WEB_CODEX_CDP_URL, in that section's language, describing the X-Omni-Cdp-Token header the sidecar expects and the compose-network isolation that applies when it is unset. Docs only, no code change. Verified on this branch: check:env-doc-sync reports all three directions in sync (817 vars in .env.example, 834 in ENVIRONMENT.md); check:docs-sync passes; check:docs-counts reports only pre-existing soft drift. |
||
|
|
54f19c7742 | feat(providers): fetch live xAI catalog for xai-oauth (#13518) |