Commit Graph

1609 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
4e94a45f0d Merge branch 'release/v3.8.50' into feat/agentic-conversation-tracking 2026-08-05 16:24:40 -03:00
diegosouzapw
7589c9f71c fix(docs): repair the #7786 squash contamination on release/v3.8.50
The #7786 squash accidentally committed its worktree copy
(.claude/worktrees/feat-7786/**, since untracked) and leaked probe tests
(repro-8522/probe-9033/repro-8956 — each now green via #9355/#9385/#9354)
plus a stray changelog.d/fixes/9159-fix.plan.md describing an UNMERGED fix
(would fabricate a changelog entry at release time — removed; #9159's own
PR ships its fragment).

This restores the PR's actual deliverable at the right paths: the
management-auth terminology guide (now with the required MDX frontmatter),
its docs test (3/3 green) and its changelog fragment.
2026-08-05 16:16:33 -03:00
Diego Rodrigues de Sa e Souza
9e3126828e fix(auto-update): skip synthetic Next.js standalone package.json without name field in resolveProjectRoot (#8956) (#9354)
A Next.js standalone build writes a synthetic .build/next/package.json
({"type":"commonjs"}) that lacks a "name" field. The resolveProjectRoot()
walk-up was stopping at this marker instead of continuing to the real repo
root, making PROJECT_ROOT point at .build/next where no .git exists, which
caused the source-mode validation to report "Not a git repository."

Fix: only accept a package.json as a project-root marker when its parsed
content has a non-empty "name" field. Keep .git as a hard marker.
Add isValidPackageMarker() helper for testability.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-05 16:07:31 -03:00
Diego Rodrigues de Sa e Souza
5e344a3a99 fix(auth): IP blacklist now blocks on direct connections via trusted peer stamp and re-reads config without restart (#9033) (#9385)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-05 16:07:21 -03:00
diegosouzapw
569fab2a94 chore: merge release/v3.8.50 (compose hardened estimateSizeFast with configurable earlyExitAt) 2026-08-05 13:15:56 -03:00
Diego Rodrigues de Sa e Souza
7d5e8235da fix(quality): add base-relative file-size check so inherited drift does not red innocent PRs (#8522) (#9355)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-05 12:53:05 -03:00
Diego Rodrigues de Sa e Souza
ef3f554665 fix(tests): clear the two base-reds on release/v3.8.50 (#9488)
* fix(tests): clear the two base-reds on release/v3.8.50

Both sat on the release itself and turned every open PR red as soon as it
merged the release, independently of the PR's own content.

- tests/snapshots/provider/translate-path.json: #9064 (b0501642dd) added the
  code-execution-2025-08-25 and skills-2025-10-02 beta flags to the Anthropic
  header but did not regenerate the golden. provider-translate-path-golden
  failed on the bare release tip — 2 pass / 1 fail with zero PRs boarded.
  Regenerated; the diff is 24 lines, all the same header in 6 variants.

- tests/unit/v1-models-auth-leak-9320.test.ts:83: shipped a (k: any) in a file
  new enough that eslint-suppressions.json does not cover it. With
  @typescript-eslint/no-explicit-any as error under tests/, that one cast
  failed 'No new ESLint warnings' for every PR. The callback parameter infers
  correctly, so the cast was redundant.

Verified on the fix branch: eslint exit 0, typecheck:core exit 0, and both
test files green (5/5).

* fix(tests): drop a third unsuppressed any (gemini-web validation test)

A full-repo lint on this branch surfaced one more file in the same class:
tests/unit/issue-9407-gemini-web-validation-false-positive.test.ts:50 casts
(executor as any).testConnection. The file entered the release at f1ea77fd04
(23:28), newer than the frozen eslint-suppressions.json, so the cast is not
covered — and it alone kept 'No new ESLint warnings' red on this very PR.

testConnection is a declared public method on GeminiWebExecutor
(open-sse/executors/gemini-web.ts:359), so the cast was redundant rather than
load-bearing; removed outright.

eslint exit 0, typecheck:core exit 0, 15/15 across the three touched tests.
2026-08-05 11:36:19 -03:00
diegosouzapw
6b0e11e378 refactor: update quality baseline and test masking allowlist
- Updated the quality baseline to set eslintWarnings value to 5000, reflecting the migration to TypeScript 7 and the new warning thresholds.
- Modified the test masking allowlist to account for removed tests and sources, ensuring proper tracking of deprecated features.
- Enhanced ESLint configuration to ignore additional directories containing non-source files.
- Removed the .npmignore file as its contents are now managed in package.json.
- Adjusted KimiWeb model configuration to correctly map K3 to the K2D5 scenario, reflecting changes in the underlying logic.
- Updated artifact packing policy to prevent nested node_modules from being published, ensuring a leaner package size.
- Added tests to verify the exclusion of node_modules from published artifacts and to ensure the integrity of the package.json files array.
2026-08-05 08:46:22 -03:00
diegosouzapw
f4e93f339d docs: add management authentication terminology guide (#7786) 2026-08-05 08:45:00 -03:00
Xiangzhe
8ca40e7971 feat(api): wire shared admission across LLM routes
Acquire admission once after API-key policy, preserve lazy raw-request snapshots, and bind lease settlement to JSON, SSE, abort, deadline, and failure lifecycles. Expose a low-cardinality health summary and preserve non-SSE Ollama errors unchanged.
2026-08-05 08:44:59 -03:00
Xiangzhe
a61020153c feat(admission): add adaptive overload and pressure controls
Add bounded weighted admission with fair queuing, deadline and cancellation handling, exact lease accounting, and a default-shadow runtime. Keep asynchronous resource-pressure shedding as an independent safety fuse and bound request feature estimation.
2026-08-05 08:32:38 -03:00
Xiangzhe
ce764bc6f3 fix(combo): classify local target timeouts as gateway timeouts
Return a typed HTTP 504 for OmniRoute's per-target timer, keep fallback active, and classify the local timeout as request-scoped so it cannot degrade provider connection health.
2026-08-05 08:32:38 -03:00
Markus Hartung
1f7c9a9cb2 fix(responses-api): fold in #9183's index-collision, truncation, and reasoning-replay fixes
Closed #9183 in favor of this branch at the operator's request. Folds in
the exact PR diff (verified byte-identical via `gh pr diff 9183`, applied
cleanly with `git apply`):

- Fixed output_index collisions between reasoning/message/tool-call items
  in Responses API streaming, which caused clients to ignore tool calls.
- Fixed a race in chunk processing where tool calls and finish signals
  were lost when they arrived in the same chunk as reasoning/text content.
- Enabled full processing of multi-choice chunks (previously truncated to
  a single choice).
- Added reasoning_content capture/replay for DeepSeek-based models
  (big-pickle), keyed off the real position in the next turn's replayed
  messages array instead of a hardcoded index 0 — prevents history
  corruption that caused models to lose context and stop prematurely.
- Added big-pickle to models recognized for native textual reasoning tags
  so <think> blocks convert to reasoning items correctly.
- Improved Responses-to-Chat translation to group reasoning, content, and
  tool calls into a single assistant turn, as strict OpenAI-compatible
  upstreams require.
- Added a descriptive placeholder for encrypted Responses API reasoning
  blocks so downgraded Chat API requests keep context.

Test plan:
- All 82 tests across reasoning-cache.test.ts, translator-helper-branches.test.ts,
  translator-request-openai-responses.test.ts, and the 3 new test files
  (responses-api-truncation.test.ts, responses-replay-fixes.test.ts,
  responses-request-translation.test.ts) pass
- npm run typecheck:core — clean
- npm run check:file-size — chatCore.ts (5024->5032) and
  translator/response/openai-responses.ts (1174->1180) rebaselined,
  both cohesive additions at the two reasoning-cache capture call sites and
  the turn-grouping fix; tests/unit/reasoning-cache.test.ts rebaselined at
  1035 (crosses the 1000-line new-file test cap, entirely this fold's diff)
2026-08-05 12:08:03 +02:00
Markus Hartung
d7db2d1a56 feat(logging): make the chat-log truncation limit configurable, bumped default 128x
The 8KB cap on logged request/response bodies (open-sse/handlers/chatCore/
logTruncation.ts::truncateForLog()) was hardcoded — trivially exceeded by
any real multi-turn agentic conversation, meaning the dashboard's "Full
Conversation" panel could only ever show a placeholder instead of the
actual messages for nearly every logged row of any conversation with real
substance.

- Added CHAT_LOG_MAX_BODY_KB env var (src/lib/logEnv.ts::
  getChatLogMaxBodyBytes()), default 1024 KB (1MB) — a 128x bump from the
  old hardcoded 8KB — following the same configurable-limit pattern as the
  sibling CHAT_LOG_TEXT_LIMIT/CHAT_LOG_ARRAY_TAIL_ITEMS/etc. vars.
- Documented in .env.example and docs/reference/ENVIRONMENT.md (both
  required — tests/unit/issue-7793-env-doc-sync-repro.test.ts and
  check-env-doc-sync.test.ts enforce this pairing).

Found and fixed a real bug while wiring this up: estimateSize.ts's
estimateSizeFast() had its own hardcoded 256KB early-exit optimization
("stop walking once bytes clearly exceeds the caller's threshold"), so it
could never report a size above ~256KB regardless of the object's true
size — meaning any caller threshold configured above 256KB (like the new
1MB default) was silently unreachable; every payload would look "under
threshold" and truncation would never fire, letting arbitrarily large
bodies through unbounded (the opposite of intended, and a real memory-
protection regression). Fixed by giving estimateSizeFast() a parameterized
earlyExitAt (default unchanged at 262144, so isSmallEnoughForSemanticCache's
existing behavior is untouched), with truncateForLog() now passing its own
configured getChatLogMaxBodyBytes() value through.

Also adds a "Conversation" field to the request detail panel's metadata
grid (last, after "Combo"), showing the request's conversation id
(sessionTag) for quick reference/copy.

Test plan:
- New TDD tests for both fixes (Responses API messageCount capture — from
  the previous commit — and the estimateSizeFast earlyExitAt parameter),
  confirmed failing before each fix and passing after
- Bumped the two truncateForLog test fixtures that were sized against the
  old 8KB threshold so they still genuinely exceed the new ~1MB default
- npm run typecheck:core / npm run lint / npm run check:file-size — clean
- npm run test:unit — 27132 tests, same 4 pre-existing/unrelated failures
  as the last confirmed-clean run (no new regressions) — including the two
  env/doc-sync contract tests that initially caught the missing
  CHAT_LOG_MAX_BODY_KB documentation, now fixed
- npm run test:vitest — 291/291 passed
- Rebuilt and redeployed to omniroute-dev
2026-08-05 11:14:12 +02:00
Markus Hartung
12228626c1 fix(conversation): show a placeholder for truncated bodies with no known message count
Follow-up to the earlier truncated-request-body transcript fix, found by
re-checking the live dashboard: a specific /v1/responses request still
showed nothing for its own turn even though its body genuinely was
truncated by logTruncation.ts's truncateForLog().

Root cause (two parts):

1. truncateForLog() only counted messages[] (Chat Completions) and
   contents[] (Gemini) — never input[] (Responses API) — so a truncated
   Responses API request's summary carried NO count field at all.

2. buildMultiRowConversation()'s earlier fix defaulted an unknown count to
   0, which silently produced "0 new turns" instead of surfacing that the
   count was simply unavailable — same end symptom as the original bug
   (nothing shown) despite the row being genuinely truncated.

Fixes:
- logTruncation.ts now also sets messageCount for input[] bodies (root
  fix, only helps requests logged from here forward).
- multiRowConversation.ts now distinguishes "known count" (existing
  specific "N messages not shown" placeholder + correct bookkeeping) from
  "unknown count" (a generic placeholder, since we can't safely diff
  against previousTotal without a real number) — needed for the
  already-persisted historical data on omniroute-dev that will never
  retroactively get a messageCount.

Test plan:
- New TDD tests for both gaps (Responses API count capture in
  logTruncation, unknown-count placeholder in multiRowConversation),
  confirmed failing before each fix and passing after
- npm run typecheck:core / npm run lint / npm run check:file-size — clean
- npm run test:unit — 27223 tests, same 4 pre-existing/unrelated failures
  as the last confirmed-clean run (no new regressions)
- npm run test:vitest — 291/291 passed
- Rebuilt and redeployed to omniroute-dev
2026-08-05 11:14:11 +02:00
Markus Hartung
b954a1cf76 fix(ci): resolve migration collision, file-size gate, and truncated-body transcript bug
CI failures on PR #9439:

1. Migration version collision: upstream/release/v3.8.50 landed
   134_proxy_logs_egress_ip.sql (#9291) after this branch's original rebase,
   colliding with this branch's own 134_agentic_conversations.sql. Renamed
   to 135_agentic_conversations.sql (re-rebased onto the current tip first).

2. check:file-size: rebaselined the files this PR's own feature growth pushed
   over their frozen/cap thresholds (RequestLoggerDetail.tsx, RequestTimeline.tsx,
   RequestLoggerV2.tsx, chat.ts, chatCore.ts — see the new
   _rebaseline_2026_08_04_9439 entry for the itemized justification) plus
   open-sse/executors/base.ts, which was already over its own frozen baseline
   on release/v3.8.50 independent of this branch (confirmed via `git diff
   upstream/release/v3.8.50 HEAD -- open-sse/executors/base.ts` — empty).

3. A third real bug, found by re-checking the live dashboard after the
   previous round's fixes: a request with a long real conversation chain
   showed only its own response in the "Full Conversation" panel. Root
   cause: open-sse/handlers/chatCore/logTruncation.ts's truncateForLog()
   replaces any request body over ~8KB with a bare {_truncated,
   _originalBytes, messageCount, ...} summary, dropping messages/input
   entirely — the norm, not the exception, for any conversation with real
   substance. buildRequestTurns() legitimately found nothing to parse, so
   the transcript silently rendered only that row's response, and (more
   subtly) every subsequent row's delta-slicing bookkeeping was computed
   against the wrong running total (0 instead of the row's real turn
   count), which would have corrupted the rest of the reconstruction too
   for any longer chain built on top of a truncated row.

   Fix: buildMultiRowConversation now detects a truncated request body via
   its messageCount field, uses that count for delta bookkeeping instead of
   silently treating it as zero, and renders one explicit placeholder turn
   ("N messages not shown — the request body was too large to log")
   instead of just disappearing.

Test plan:
- New regression tests for the truncation case (single truncated row, and
  a truncated row followed by a real row to verify bookkeeping stays
  correct)
- npm run check:migration-numbering / check:file-size — clean
- npm run typecheck:core / npm run lint — clean
- npm run test:unit — 27086 tests, only 4 failures remain (down from 18 —
  2 were fixed by the newer upstream commits pulled in by this re-rebase),
  all independently pre-existing/unrelated (ServiceSupervisor timing,
  monaco-editor path, npm-pack)
- npm run test:vitest — 291/291 passed
2026-08-05 11:14:11 +02:00
Markus Hartung
52671130a7 fix(sse): fix stale provider-response summaries and broken conversation continuation
Two independent bugs found during further live verification of the
conversation-tracking feature:

1. (#9315) The dashboard's "Provider Response" panel showed a stale,
   incomplete snapshot for long streamed responses. Root cause:
   open-sse/utils/stream.ts reconstructed the summary from
   buildStreamSummaryFromEvents(providerPayloadCollector.getEvents(), ...)
   — but getEvents() only returns whatever survived the collector's
   maxEvents/maxBytes cap, so once a stream exceeded it (easy with a
   reasoning + tool-calling model), everything after the cutoff (final
   finish_reason, tool_calls, rest of reasoning_content, usage) was
   silently dropped from the reconstruction, even though the client
   actually received the correct, complete response.

   Fix: streamPayloadCollector.ts's per-format summary builders
   (buildOpenAISummary/buildResponsesSummary/buildClaudeSummary/
   buildGeminiSummary) are now also available as incremental reducers
   (createXReducer: ingest one chunk at a time, finalize at the end).
   createStructuredSSECollector accepts a format + fallbackModel and feeds
   the reducer on every push() — including chunks that get dropped from
   the retained event array once the cap is hit — via a new getSummary()
   method. stream.ts's 3 call sites now use collector.getSummary() instead
   of reconstructing from the (possibly truncated) getEvents().

2. Conversation continuation never actually worked for real agentic CLI
   traffic. Root cause: computeFingerprintHash/hashTurnsBounded anchored
   conversation identity partly on the system message's text — but real
   coding-agent CLIs (Claude Code, opencode, etc.) commonly regenerate the
   system prompt on every single request with live context (timestamp,
   cwd, git status...). That volatility alone broke both the fingerprint
   bucket lookup and the prefix-hash continuation check, so every request
   minted a brand new conversation id even though apiKeyId/model/toolNames
   and the actual user/assistant history were an unbroken, growing
   continuation. Confirmed live: 28 consecutive requests from one real,
   growing session, each recorded as its own turn_count=1 conversation —
   which is also why /dashboard/conversations appeared empty (nothing ever
   reached turn_count >= 2) and why an individual timeline/log entry only
   ever showed a single turn.

   Fix: both computeFingerprintHash's identity anchor and
   hashTurnsBounded's head/tail projection now exclude the system message
   entirely, so a regenerated-every-turn system prompt can no longer break
   continuation detection. New regression test reproduces the exact
   scenario (system prompt differs each turn, everything else constant)
   and confirms the second request is now recognized as a continuation.

Also fixed while touching hashTurnsBounded: an accidental stray control
character (SOH, 0x01) in the internal join() separator — cosmetic (any
consistent separator produces a valid hash) but worth cleaning up since it
was already being edited; no stored data depended on the old format since
the continuation bug meant turn_count never reached 2 in production.

Test plan:
- New TDD regression tests for both bugs (stream-payload-collector.test.ts,
  conversationTracker.test.ts), confirmed failing before the fix and
  passing after
- npm run typecheck:core / npm run lint — clean
- npm run test:unit — 26983 tests, 18 failures, all independently confirmed
  pre-existing on release/v3.8.50 (reproduced identically against the
  clean base commit)
- npm run test:vitest — 291/291 passed
- Rebuilt and redeployed to omniroute-dev; health check + DB migration
  verified
2026-08-05 11:14:11 +02:00
Markus Hartung
08aa557efd test: update sidebar tests and Vietnamese translations for the new Conversations nav item
The new "Conversations" sidebar entry and "Conversation" logs column needed
their Vietnamese strings (Vietnamese is the reference locale requiring full
translation, no __MISSING__ placeholders) and the sidebar structure
assertions needed the new item added to their expected lists.
2026-08-05 11:14:11 +02:00
Markus Hartung
47b4a55f2d feat(dashboard): add agentic conversation tracking with live transcript view
Every agentic chat request now gets a conversation id (X-ConversationId
response header), and OmniRoute detects when a follow-up request continues
the same conversation via fingerprint + bounded prefix-hash matching, with a
strict-growth invariant to prevent false merges between independent
single-shot requests.

Dashboard changes:
- /dashboard/logs: toggleable Conversation column
- /dashboard/logs/timeline: same-conversation requests share a timeline lane,
  connected by an arrow, with a configurable lane-reuse window
- Request detail panel: new "Full Conversation" transcript above the raw SSE
  event stream, with Markdown rendering, per-turn timestamps, turn-relative
  view (only turns up to the one you opened, with a jump-to-next link),
  click-any-turn-to-open-its-log navigation, and live auto-refresh (with an
  auto-follow toggle, matching the event stream's autoscroll pattern) that
  rebuilds the transcript in real time from the in-flight SSE chunk buffer
  while a request is still streaming
- New /dashboard/conversations page listing only conversations with 2+ turns
- Configurable auto-refresh intervals on both the timeline and conversations
  list pages

Also fixes a pre-existing bug where the timeline view never showed SSE/
stream-chunk events or respected email-masking, because RequestTimeline.tsx
hardcoded debugEnabled/emailsVisible instead of reading the same
server-side/store state RequestLoggerV2.tsx already used, and makes the
request detail panel and conversations list responsive on mobile.
2026-08-05 11:14:11 +02:00
Diego Rodrigues de Sa e Souza
2cb7567d66 fix(providers): treat claude-web 429 as unhealthy and forward Retry-After (#9406) 2026-08-04 23:28:41 -03:00
Diego Rodrigues de Sa e Souza
b840628de8 fix(providers): add tool_use handling to claude-web stream parser (#9408) 2026-08-04 23:28:37 -03:00
Diego Rodrigues de Sa e Souza
d969555417 fix(security): require explicit tool envelope to prevent bare JSON tool_calls (#9343) 2026-08-04 23:28:33 -03:00
Diego Rodrigues de Sa e Souza
f1ea77fd04 fix(providers): detect expired gemini-web sessions and add testConnection (#9407) 2026-08-04 23:28:30 -03:00
Diego Rodrigues de Sa e Souza
85f30d4da8 fix(api): fall back to slugified provider name when prefix is empty (#9416) 2026-08-04 23:28:26 -03:00
Diego Rodrigues de Sa e Souza
ab560cce7b fix(providers): map kimi-web/K3 to K2D5 scenario to fix resource_exhausted (#9338) 2026-08-04 23:28:21 -03:00
Diego Rodrigues de Sa e Souza
6b531fbacd fix(claude): remove unconditional always-mode return in claudeClassifierCompat (#9276) 2026-08-04 21:36:54 -03:00
Diego Rodrigues de Sa e Souza
b07182c72a fix(security): require auth for /v1/models when management auth is configured (#9320) 2026-08-04 21:36:41 -03:00
Diego Rodrigues de Sa e Souza
7e55abbc41 fix(vision-bridge): do not select unreachable describe-model when no vision provider is connected (#8430) 2026-08-04 21:36:34 -03:00
Diego Rodrigues de Sa e Souza
b0501642dd fix(providers): anthropic strips code-execution/skills beta flag, causing container rejection (#9064) 2026-08-04 21:36:18 -03:00
Diego Rodrigues de Sa e Souza
7d46d4039f fix(perplexity-web): update catalog to use 'copilot' mode and fix model IDs (#8989) 2026-08-04 21:36:13 -03:00
Diego Rodrigues de Sa e Souza
d502f144b9 fix(providers): copilot-m365-web enterprise turns send disconnectBehavior=continue (#8971) 2026-08-04 21:36:09 -03:00
Diego Rodrigues de Sa e Souza
eaea0347ac fix(executor): guard claude/anthropic buildHeaders against empty credentials and extend dual-Bearer parity for third-party baseUrls (#8653) 2026-08-04 21:36:04 -03:00
Diego Rodrigues de Sa e Souza
37edd74f2d fix(proxy-health): include credentials in proxy health check URLs (#8853) 2026-08-04 21:36:00 -03:00
Diego Rodrigues de Sa e Souza
0b70a14a3b fix(auth): setting first dashboard login password no longer fails with HTTP 400 PASSWORD_REQUIRED (#8950) 2026-08-04 21:35:29 -03:00
Nick Sullivan
7b8055c7f8 fix(resilience): count STREAM_EARLY_EOF as a provider failure in combo routing (#9251)
* fix(resilience): count STREAM_EARLY_EOF as a provider failure in combo routing

A STREAM_EARLY_EOF is an upstream that accepted the request (HTTP 200), opened
the SSE stream, then closed it without emitting a single non-ping event. The
combo path classified it together with STREAM_READINESS_TIMEOUT through
isStreamReadinessFailureErrorBody(), and the readiness exemption in
shouldRecordProviderBreakerFailure meant the whole-provider circuit breaker
never saw it.

During a provider-wide outage that makes the breaker blind. Over a 7-day window
on our router we recorded 311 of these events, 302 of them on one model, 265
inside the upstream's published incident window — and the provider breaker sat
at CLOSED / failure_count=0 the entire time. Every request kept being dispatched
to the failing provider instead of shedding to the next combo target.

The two codes are different signals. The readiness probe is a pre-flight
liveness check on a connection we have not committed to, so failing it means
"this connection looks stale". An early EOF means the provider took the request
and then failed to serve it. The single-model path already treats it that way:
shouldTripProviderBreakerForResult has no readiness exemption, so a 502 early
EOF trips the breaker there. This makes the combo path consistent.

isStreamReadinessFailureErrorBody keeps matching both codes, because the
transient-retry and round-robin semaphore-cooldown paths in combo.ts do want
identical treatment for both. Only the breaker needs to tell them apart, so the
distinction is added as a narrow predicate and an optional argument rather than
by changing the shared classifier. Omitting the new argument reproduces the
previous behaviour exactly.

Follows the additive-override pattern established by the isProxyUnreachable
work, and leaves the existing exclusions for client aborts and plain 429s
untouched.

* test: register stream-early-eof-breaker in stryker tap.testFiles

The mutation test-coverage gate (check:mutation-test-coverage --strict)
detects unit tests that cover a mutated module but are missing from
stryker.conf.json tap.testFiles, so their mutant kills would not count.

comboPredicates.ts is one of the mutated modules, and the new
stream-early-eof-breaker.test.ts covers it, so the gate correctly flagged
the omission. 8376-econnrefused-breaker.test.ts -- the test this one is
modeled on -- is already registered; this just brings the new file in line.

No production code change.

---------

Co-authored-by: Nick Sullivan <nick@technick.ai>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-04 18:07:09 -03:00
Xiangzhe
3440c118e0 feat(usage): show Grok Build billing limits (#9205)
* feat(usage): show Grok Build billing limits

* test(usage): keep Grok quota reset fixture in the future

* fix(i18n): add Grok billing labels to pt-BR

* fix(i18n): add Grok billing labels to Vietnamese
2026-08-04 18:07:02 -03:00
nguyenha935
712910612b fix(db): bundle and verify the sql.js fallback (#9044)
Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
2026-08-04 18:06:55 -03:00
Bob.Hou
8027c60726 test(sse): expect the trailing period in the no-credentials message (#9392)
#9275 started appending a candidate-alias hint to the zero-active-credentials
error and terminated the provider name with a period, so the two sentences read
as one message. The two vscode tokenized-route tests still assert the old
unterminated string and now fail on every pull request opened against this
branch.

The Quality Gates workflow only runs on pull_request to release/**, never on
push, so the branch itself never re-runs these shards and the drift stayed
invisible after the merge.

Assert what the handler actually produces. Keeping the comparison exact rather
than loosening it to a prefix match is deliberate -- the exact form is what
caught the drift.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-08-04 17:08:14 -03:00
Diego Rodrigues de Sa e Souza
4a3dcf6b0b fix(routing): only let Codex-native bare ids preempt a provider when codex is active (#9447)
* fix(routing): only let Codex-native bare ids preempt a provider when codex is active

#9275 widened CODEX_NATIVE_UNPREFIXED_MODELS from a single id to gpt-5.5 plus the
gpt-5.6-sol/terra/luna tiers, so bare Codex CLI ids would reach the ChatGPT
subscription instead of fanning out to whichever provider won the inference race.
The early return it added never consulted the active-provider set, which made the
codex-only guard 30 lines below unreachable for every id in the set:

  if (CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId)) return { provider: "codex", ... }

An OpenAI-only install therefore had bare gpt-5.5 routed to codex and failed with
'no active credentials for provider: codex' on a model OpenAI serves, and an install
whose codex connection was merely inactive failed identically. This also silently
reverted #5887's compatibility boundary.

The preference now only PREEMPTS another provider when a codex connection is active.
Ids that no other provider catalogs (codex-auto-review) still resolve to codex with no
connection at all — there is nothing to preempt and 'no codex credentials' is the
honest error. With codex active the preference still beats OpenAI, which is the point
of #9275, and an explicit openai/ prefix overrides it either way.

Tests: the three assertions that encode the intended #9275 change now expect codex
(plus a new one pinning the explicit-prefix override); the rest were already correct
and pass again untouched. Adds a regression test for the OpenAI-only case.

* docs(changelog): correct fragment id to #9447

* test(routing): seed an active codex connection in the bare-precedence guards

The two files #9275 added assert that bare gpt-5.5 / gpt-5.6-sol reach codex, but
they ran against an empty database — so they also pinned 'codex wins with no codex
connection at all', which is the regression #9447 removes. That put them in direct
contradiction with plan3-p0 / chat-helpers / codex-gpt55-routing-5887, which assert
openai for the very same input: no implementation could satisfy both, which is why
the release could not go green.

Seeding an active codex connection keeps the contract these files were written to
guard (codex beats openai for a Codex-native bare id) while dropping the accidental
'even with no codex configured' half. Cases that need no connection are left as they
were: the tier-only ids and codex-auto-review have no alternative provider to preempt,
and the explicit-prefix overrides are unaffected.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-04 17:08:08 -03:00
Diego Rodrigues de Sa e Souza
16ed707148 feat(providers): filter detail connections server-side (#9247)
* feat(providers): filter detail connections server-side

Filter provider detail requests at the database boundary while preserving
the full per-provider connection set needed by search, pagination, and bulk
actions. Alias-backed provider pages keep their existing aggregate behavior.

Co-authored-by: RobertsXML <RobertsXML@proton.me>
Inspired-by: https://github.com/decolua/9router/pull/2998

* chore(changelog): fragment for #9247

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: RobertsXML <RobertsXML@proton.me>
2026-08-04 14:34:22 -03:00
Dizzle
8b97ef99aa fix(db): persist account egress IP into proxy_logs (#9291)
* fix(db): persist account egress IP into proxy_logs

The account egress IP (outbound IP the upstream saw, resolved via proxyEgress.ts
echo-IP probe with 5-min cache) was computed and surfaced in the proxy_logs
console and ring buffer, but never persisted: proxy_logs.egress_ip did not
exist, so the value was lost on restart and real traffic could not be
attributed to the node/IP active at that instant.

- migration 134 adds proxy_logs.egress_ip (nullable, backward-compatible)
- schemaColumns.ensureProxyLogsColumns() idempotent reconciler
- proxyLogger self-heals the schema in loadFromDb(), persists egress_ip on
  INSERT, and matches it in search
Follows the session_tag (#8249) migration + schemaColumns reconciler pattern;
base SCHEMA_SQL untouched.

* docs(changelog): add 9291 fragment for proxy_logs egress_ip

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-08-04 14:34:17 -03:00
Dizzle
e50f2329dc fix(lib): memoize catalog pricing/capability lookups to fix cold /v1/models freeze (#8697) (#8987)
Root cause: a cold GET /v1/models catalog rebuild froze the entire server 41-54s.
node --prof profiling found a systemic missing-memoization pattern — a per-model
function rescanning a static or synced data structure with Object.entries()/
Object.keys() (or hitting SQLite) on every call instead of once per rebuild. Fixed
6 instances of the same pattern, found by iteratively re-profiling the full catalog
sweep after each fix (plus a whitebox review pass) until no further hotspot of this
shape remained:

1. getModelsDevPricing() (modelsDevSync.ts) — re-ran a synchronous SQLite query and
   re-JSON.parse'd ~180 blobs on every call (up to ~6091x instead of once per
   request). Memoized via the existing modelCatalogCacheVersion invalidation signal
   (same pattern as getCachedRawProviderConnections/getCachedProviderNodes in
   db/readCache.ts). Dominant cost of the original 41-54s freeze.

2. findInsensitive() (modelMetadataRegistry.ts, resolveCatalogPricing) — rebuilt a
   full Object.entries() scan on every case-insensitive lookup miss, twice per
   model. Replaced with a lowercase-key index built once per distinct pricing
   object and cached by identity (WeakMap). Warns once at index-build time on a
   case-insensitive key collision instead of silently discarding the second value.

3. getSyncedCapability() (modelsDevSync.ts) — ran a per-model SQLite SELECT on cold
   cache instead of self-warming the whole-table cache; no caller in the
   /v1/models build path ever primed it, so a cold rebuild ran one SQLite
   round-trip per model per call site. Now self-warms via the existing bulk
   getSyncedCapabilities() on first miss. Measured as the dominant remaining cost
   after fixes 1-2 (~70% of a full catalog sweep).

4. getCanonicalModelSpecId() (shared/constants/modelSpecs.ts) — up to 3 separate
   linear scans over the static MODEL_SPECS table per call (exact ci, alias ci,
   prefix). Replaced with a lazy, lowercase-key index built once (MODEL_SPECS never
   changes at runtime); prefix-match iteration order preserved exactly so
   resolution outcomes are unchanged.

5. getStaticSpecCanonicalModelId() (modelCapabilities.ts) — duplicated the same
   exact+alias scan as (4) in a second, separate rescan. Now reuses the shared
   index via a new exported helper (findModelSpecIdByExactOrAlias) instead of
   maintaining a second cache over the same static table.
   reverseModelsDevProviders() (modelCapabilities.ts) — rescanned
   Object.entries(MODELS_DEV_PROVIDER_MAP) (also static) on every call; memoized
   by provider key. Result is frozen (readonly) since it is now shared across
   calls instead of freshly allocated each time.

6. resolveModelAlias() (shared/constants/modelSpecs.ts) — rescanned
   Object.entries(MODEL_SPECS) unconditionally once per model (verified 1:1 call
   ratio, no short-circuit). Case-sensitive exact match (Array.includes(), no
   .toLowerCase()) — uses a dedicated exact-match index, deliberately not the
   case-insensitive alias index from fix 4/5 (would silently broaden matches).

Measured on a 1940-pair real-catalog sample (static PROVIDER_MODELS registry):
cold sweep 828ms -> 356ms after fixes 3-5 on top of 1-2, extrapolating to roughly
1s on the real ~6091-model catalog, down from the original 41-54s freeze.

Complementary to the stale-serve fix in #8801 (upstream) — neither alone
eliminates the freeze.

Tests: call-count regression guards for every fix (DB prepare / Object.entries /
Object.keys call counts staying constant instead of scaling with iteration count),
plus correctness coverage for case-insensitive/case-sensitive resolution. All
pre-existing consumer suites re-verified passing (96 tests total across 19 files).

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-04 14:34:10 -03:00
Xiangzhe
455906c181 fix(reasoning): forward Ollama Cloud thinking (#9290) 2026-08-04 14:34:02 -03:00
Bob.Hou
b6bcc491bc fix(token-refresh): exempt transient errors from exponential backoff (#9242)
* fix(token-refresh): exempt transient errors from exponential backoff

A refresh that failed on a network timeout was treated exactly like one
that failed on a revoked token: the streak incremented and the circuit
backed off exponentially, up to four hours. A brief upstream blip could
therefore park a healthy account for the rest of the day.

Transient failures now take a flat two-minute retry window instead of
advancing the streak. Classification checks structured signals first
(err.name for AbortError/TimeoutError, then err.code and err.cause.code)
and only falls back to matching the message text, so it does not depend
on upstream wording. Everything else keeps the existing exponential path.

Two properties worth preserving on sight:

  - A transient failure never shortens a longer permanent backoff. The
    new window is only adopted when the existing one is not already
    further out.
  - testStatus is preserved on both paths, so a connection whose access
    token is still valid keeps serving requests while its refresh
    retries.

Only a successful refresh clears the circuit. A successful request does
not, because requests do not refresh tokens.

* chore(quality): rebaseline file-size for tokenHealthCheck.ts

src/lib/tokenHealthCheck.ts lands at 1021 lines, above the 1000 cap. The
file consolidates token-refresh health checking that was previously split
across auth.ts and tokenRefresh.ts, and the refresh circuit state machine
does not divide cleanly, so splitting it to satisfy the cap would cost
more than it buys.

Scoped to this file only. Baseline entries for files this branch does not
touch are left at their upstream values.
2026-08-04 14:33:54 -03:00
NOXX - Commiter
45d375aa0b fix(api): defer media body size limits to providers (#8843)
Image and video payloads vary by provider and base64 encoding adds substantial overhead. Exempt media routes from OmniRoute's global request-body cap so provider-specific validation determines whether a request is too large. Keep finite body limits for non-media routes and cover both header and streamed-body admission paths.
2026-08-04 14:33:48 -03:00
Diego Rodrigues de Sa e Souza
f11d883f22 fix(cli-tools): enable Apply for compatible providers (#9250)
* fix(cli-tools): resolve models for compatible providers

Keep the CLI tools Apply flow usable when a dynamic OpenAI-compatible or
Anthropic-compatible connection has no static catalog entry. Resolve its
public prefix, connection default model, and prefix-backed catalog entries
before gating the cards.

Co-authored-by: lazysaltyfish <7127935+lazysaltyfish@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2995

* chore(changelog): fragment for #9250

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: lazysaltyfish <7127935+lazysaltyfish@users.noreply.github.com>
2026-08-04 10:06:37 -03:00
Diego Rodrigues de Sa e Souza
2cb77bbca7 fix(translator): harden Claude format detection for model validation (#9253)
* fix(translator): harden Claude format detection for model validation

Co-authored-by: Ervareza Naurian <rianskp644@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2949

* chore(changelog): fragment for #9253

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Ervareza Naurian <rianskp644@gmail.com>
2026-08-04 10:06:31 -03:00
Shixi Li
a8216c92fe fix(sse): preserve error-only stream diagnostics (#9022)
* fix(sse): preserve error-only stream diagnostics

* test(ci): register stream readiness mutation coverage

* chore(changelog): finalize PR 9022 fragment
2026-08-04 10:06:25 -03:00
ikelvingo
c790b57af8 fix(translator): pass output_config.effort=max through verbatim (#9053)
The claude->openai translator was unconditionally rewriting max to xhigh, which broke any OpenAI-shape upstream that accepts max literally (e.g. ollama-cloud, opencode-go deepseek, moonshot k3, native Claude). Provider-aware effort policy is owned by sanitizeReasoningEffortForProvider in the executor; the translator should only do form conversion.

Regression guard: tests/unit/base-executor-sanitize-effort.test.ts end-to-end case (claude -> ollama-cloud preserves max).
2026-08-04 10:06:18 -03:00
Bob.Hou
9ee6435f0e fix(classify): recognize Modal 'usage limit reached' as quota exhausted (#9079)
Modal-hosted OpenAI-compatible endpoints (self-hosted Kimi K3 via
Modal free tier) return HTTP 429 with body {"error":"usage limit
reached"} when the account's credit is exhausted. Previously no
QUOTA_PATTERNS regex matched this bare-string error shape, so the 429
fell through to rate_limit (60s short cooldown). Combined with combo
round-robin's per-conversation session stickiness (#3825), this kept
re-targeting the same exhausted connection every turn instead of
locking it out and failing over to an account with remaining credit.

Add a substring pattern matching the JSON key/value pair
"error":"usage limit reached" with tolerance for trailing
punctuation and whitespace. Only the exact "error" key matches;
different keys or qualified transient messages like "Per-minute usage
limit reached" stay classified as rate_limit.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-08-04 10:05:56 -03:00