mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 12:22:34 +03:00
docs/dedup-hash-codeql-note
7633 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3638adeeae |
feat(dashboard): Faro chat with push-to-talk voice on the Conductor panel (PRD RF4) (#8222)
* feat(a2a): conductor bridge core — event mapping with canceled->cancelled
* feat(a2a): incremental SSE parser for conductor bridge
* feat(a2a): conductor bridge connection loop with persisted cursor and backoff
* feat(a2a): start conductor bridge at boot behind CONDUCTOR_HUB_URL
* fix(a2a): conductor bridge parses the hub's real SSE wire format (id/type in frame, data={ts,payload})
* docs(reference): document CONDUCTOR_HUB_URL/TOKEN in ENVIRONMENT.md (env-doc-sync gate)
* feat(a2a): fleet skills derived from the Conductor hub for the agent card
* feat(a2a): agent card announces Conductor fleet skills
* feat(dashboard): server-side hub proxy for the Conductor panel (whitelisted shapes, fail-open)
* feat(dashboard): /api/conductor proxy routes (fleet, task detail, cancel) behind management auth
* feat(dashboard): Conductor panel — fleet live view, task detail and cancel over /api/conductor proxy
* feat(dashboard): /api/conductor/ask — server-side proxy to Faro (spokesperson) with whitelisted {text,pending}
* chore(env): CONDUCTOR_SPOKESPERSON_URL declared in schema, .env.example and ENVIRONMENT.md
* feat(dashboard): Faro chat with push-to-talk voice on the Conductor panel
---------
Co-authored-by: backryun <bakryun0718@proton.me>
|
||
|
|
3db785dc41 |
feat(dashboard): Conductor panel — fleet, tasks and cancel over server-side proxy (PRD RF3) (#8221)
* feat(a2a): conductor bridge core — event mapping with canceled->cancelled
* feat(a2a): incremental SSE parser for conductor bridge
* feat(a2a): conductor bridge connection loop with persisted cursor and backoff
* feat(a2a): start conductor bridge at boot behind CONDUCTOR_HUB_URL
* fix(a2a): conductor bridge parses the hub's real SSE wire format (id/type in frame, data={ts,payload})
* docs(reference): document CONDUCTOR_HUB_URL/TOKEN in ENVIRONMENT.md (env-doc-sync gate)
* feat(a2a): fleet skills derived from the Conductor hub for the agent card
* feat(a2a): agent card announces Conductor fleet skills
* feat(dashboard): server-side hub proxy for the Conductor panel (whitelisted shapes, fail-open)
* feat(dashboard): /api/conductor proxy routes (fleet, task detail, cancel) behind management auth
* feat(dashboard): Conductor panel — fleet live view, task detail and cancel over /api/conductor proxy
---------
Co-authored-by: backryun <bakryun0718@proton.me>
|
||
|
|
0b158209ee |
feat(a2a): Agent Card announces Conductor fleet skills (PRD RF2) (#8119)
* feat(a2a): conductor bridge core — event mapping with canceled->cancelled
* feat(a2a): incremental SSE parser for conductor bridge
* feat(a2a): conductor bridge connection loop with persisted cursor and backoff
* feat(a2a): start conductor bridge at boot behind CONDUCTOR_HUB_URL
* fix(a2a): conductor bridge parses the hub's real SSE wire format (id/type in frame, data={ts,payload})
* docs(reference): document CONDUCTOR_HUB_URL/TOKEN in ENVIRONMENT.md (env-doc-sync gate)
* feat(a2a): fleet skills derived from the Conductor hub for the agent card
* feat(a2a): agent card announces Conductor fleet skills
---------
Co-authored-by: backryun <bakryun0718@proton.me>
|
||
|
|
ce70df6003 |
feat(a2a): Conductor bridge — mirror OmniConductor hub tasks into the A2A TaskManager (PRD RF1) (#8080)
* fix(api): enforce model permissions on gateway mirrors (#9854) Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> * cherry-pick(pr-9787): fix(sse): apply Azure param rules on azure-ai and clamp gpt-4o-mini output tokens (#9855) * fix(sse): apply Azure request-param rules on the azure-ai wire path Azure rejects several stock Chat Completions params on its newer deployments and returns HTTP 400 rather than ignoring them: max_tokens -> 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead. reasoning_effort -> Function tools with reasoning_effort are not supported. Those rules lived inline in AzureOpenAIExecutor, so they only covered the azure-openai provider. azure-ai (Azure AI Foundry) had no executor entry and fell through to the bare DefaultExecutor, so the SAME Azure deployment succeeded on one connection and 400'd on the other. Every agentic client sends tools on every turn, so azure-ai failed on the first request. Extract the rules to open-sse/executors/azureParamRules.ts, add an AzureAiExecutor that inherits DefaultExecutor's azure-ai URL/header/apiType handling unchanged and applies the shared rules, and register it for azure-ai. Also widen the deployment pattern to cover gpt-chat-latest: it is a moving alias that resolves to a GPT-5-era model and rejects max_tokens, but carries no version number for the token-boundary pattern to key on. Verified against the base regex - gpt-chat-latest did not match, which is exactly the observed 400. Regression guard: tests/unit/azure-param-rules.test.ts, including an assertion that getExecutor("azure-ai") no longer resolves to a bare DefaultExecutor. * fix(sse): clamp Azure gpt-4o-mini completion tokens to its 16384 ceiling Azure gpt-4o-mini deployments accept at most 16384 completion tokens and 400 on anything larger: max_tokens is too large: 32000. This model supports at most 16384 completion tokens, whereas you provided 32000. The 32000 is OmniRoute's own doing: adjustMaxTokens raises any smaller max_tokens to DEFAULT_MIN_TOKENS (32000) whenever tools are present, to avoid truncated tool arguments. That floor has no upper bound, so an agentic client asking for far less still trips the model ceiling on its first turn. Add scoped maxOutputCap rules in paramSupport.ts for both Azure wire paths. PROVIDER_MAX_TOKENS is the wrong lever here - it is provider-wide, and the same Azure resource also serves GPT-5 deployments with a much higher ceiling. Regression guard: tests/unit/azure-max-output-clamp.test.ts, which also pins that the clamp does not leak to gpt-5.1 or to gpt-4o-mini on other providers. --------- Co-authored-by: Mihaly Bodo <michael@proton-quantum.com> * maint: final follow-up cherry-pick #9783 (#9904) * fix(deps): bump transitive deps for 6 Dependabot + remaining audit vulns on main Same overrides as #9464 (ip-address, hono, fast-uri, socket.io-parser, undici) applied directly to main. Also covers brace-expansion (scoped), js-yaml v4 copies, and mermaid. npm audit: 6→0 vulnerabilities. Closes Dependabot #161-#166. * fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190) Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13 (with monaco-editor scoped override). Closes Dependabot #189, #190. Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge — awaiting Dependabot re-scan. npm audit → 0 vulnerabilities. * fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) _tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential _tasks symlink can slip in via git add -A and, once pulled, checkout materializes it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks ignores the symlink too, preventing re-capture. * fix(translator): keep Responses namespace identity across the hub-and-spoke pivot Step 1 of the pivot (openai-responses -> openai) flattens namespace sub-tools to a qualified wire name (#8295) and records the `{namespace, name}` pair on a non-enumerable `_toolNameMap`. Step 2 (openai -> target) returns a brand-new object, so the property was dropped for every non-OpenAI target. chatCore then handed `null` to the #7936 response seam and namespace sub-tool calls reached the client under their flattened name, which Codex rejects with `unsupported call: <name>` — the symptom #7936 was opened to fix. Copying `_toolNameMap` through is not viable: openai-to-claude and openai-to-gemini publish their own `Map<string, string>` alias map on that same property during step 2, so it carries two incompatible types. This adds a dedicated `_namespaceToolIdentityMap`, propagated by translateRequest across the pivot; chatCore prefers it and falls back to `_toolNameMap` for the non-pivot producers. Both keys are stripped from the cliproxyapi wire body. Fixes #9780 * fix(chat): reduce file size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(chat): reduce combined file size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(chat): reduce combined file size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: VXNCXNX <vincent@preuve.ai> * fix(sse): route claude/<provider>/<model> aliases for catalog-only providers (#9856) The /v1/models catalog mirrors `claude/<provider>/<model>` ids purely from the alias gate -- ccAliasPredicate.ts consults no provider registry. The request path additionally required the prefix to be an open-sse REGISTRY entry or an operator-defined custom node. Enterprise-cloud providers such as azure-ai / azure-openai live only in the provider catalog (src/shared/constants/providers/apikey/enterprise-cloud.ts). They route fine directly -- `azure-ai/Phi-4` returns 200 -- but have no open-sse registry entry, so the two sides disagreed: the catalog advertised `claude/azure-ai/<model>` while stripCcDiscoveryAlias refused to strip it. The unstripped id then fell through to normal resolution, which splits on the first / and parsed `claude` as the provider. Every Claude Code request for an Azure model was routed to the Claude provider instead: ROUTING: Provider: claude, Model: azure-ai/DeepSeek-V4-Flash Extract the predicate as `isRoutableProviderPrefix()` and widen it to the provider catalog (id + alias) alongside the open-sse registry, so the request path recognises exactly what the catalog can advertise. Regression guard: tests/unit/cc-discovery-alias-routable-prefix.test.ts pins azure-ai/azure-openai/azure as routable, keeps openai/anthropic routable, and keeps an unknown prefix non-routable. Verified failing before the widening. Co-authored-by: Mihaly Bodo <michael@proton-quantum.com> * fix(i18n): translate validation model keys in 34 locales (#9857) The provider-connection dialog (AddApiKeyModal / EditConnectionModal) rendered humanized key names instead of real copy for providers.validationModelId{Label,Placeholder,Hint} in 34 of 43 locales — the values read "Validation Model Id Label", "Validation Model Id Placeholder" and "Validation Model Id Hint" verbatim. Each translation follows the terminology and register already used by the neighbouring provider keys in its own file — e.g. de Anbieter/API-Schlüssel with formal Sie, fr fournisseur/clé API, ru провайдер/ключ API — and each locale's own "e.g." convention (z. B., 例:, напр., ör., cth., hal.). Source of truth is en.json, which labels the field "Validation Model" (no "ID"); a few older locales say "validation model ID" and were left untouched rather than propagating that divergence. Co-authored-by: Mihaly Bodo <michael@proton-quantum.com> * cherry-pick(pr-9770): chore(repo): ignore Electron build output unpacked into repo root (#9858) * chore(repo): ignore Electron build output unpacked into repo root electron-builder (squirrel-windows target) unpacks the packaged app -- the entire Chromium runtime, ~24k files -- directly into the repository root: OmniRoute.exe, chrome_*.pak, *.dll, locales/, resources/, icudtl.dat, snapshot blobs and the Chromium license files. None of it was covered by .gitignore, so `git add -A` would commit the whole runtime. Every rule is root-anchored (leading `/`) because a bare `locales/` or `resources/` would also swallow tracked sources -- notably the CLI translations in bin/cli/locales/*.json. Verified with `git check-ignore`: all artifact paths ignored, and bin/cli/locales/{en,de}.json remain tracked. * chore(electron): sync package-lock for windows installer deps Adds the lockfile entries for the Windows installer/signing toolchain that the electron build now pulls in: electron-builder-squirrel-windows, electron-winstaller and @electron/windows-sign (plus their transitive fs-extra/jsonfile/universalify/mkdirp pins), and bumps app-builder-lib and builder-util-runtime. Lockfile-only change; no source or runtime behaviour is affected. --------- Co-authored-by: Mihaly Bodo <michael@proton-quantum.com> * fix(skills): normalize web fetch credentials (#9859) Co-authored-by: backryun <bakryun0718@proton.me> * fix(types): narrow DeepSeek tool calls (#9860) Co-authored-by: backryun <bakryun0718@proton.me> * fix(perf): memoize synced pricing reads (#9861) Co-authored-by: chloeassistant <279834366+chloeassistant@users.noreply.github.com> * cherry-pick(pr-9744): test(integration): add general live-test tool for the real "default" combo + rootless wire capture (#9862) * test(integration): add general live-test tool for the real "default" combo Temporary WIP commit on this deferred branch — lands in its own separate PR once the bug-fix extraction batch is done (never bundled into a bug-fix PR). Unlike liveGeminiShared.ts (provisions its own narrow 2-model Gemini-only combo), this reads the REAL "default" combo currently configured on the target instance directly from the DB and exercises every provider/model step in it directly, bypassing combo routing, so live-test coverage always matches whatever is actually configured instead of a hardcoded snapshot. Live-verified against omniroute-beta (seeded with the real 18-model, 5-provider default combo): 14/18 models pass consistently across non-streaming + streaming Chat Completions and streaming Responses API. The 4 consistent failures are real external state (cerebras credits_exhausted, one deprecated openrouter free-tier model), not code regressions. (cherry picked from commit c40b13a48fd897259c56f5122e9e57a3dc7654ba) * test(integration): add rootless wire-capture correlation to the live-test tool Temporary WIP commit on this deferred branch — lands in the same final live-test-tool PR as the general default-combo suite, never bundled into a bug-fix PR. liveContainerHarness.ts spins up a dedicated, throwaway podman container (same runner-base image target as the operator's local dev/beta containers) so wire-capture tests are fully self-contained: builds the image if missing, starts the container with a persistent data dir, waits for health, seeds the real "default" combo + provider connections from the operator's local omniroute-dev instance (idempotent — only runs once per data dir), and provisions API keys via the running instance's own auth flow. wireCapture.ts captures the container's actual network traffic via `podman unshare nsenter --net=<container netns> -- tcpdump` — no root needed, verified working live (this generalizes the root-requiring `sudo nsenter -t $PID` command scripts/sre/tcp-close-analyzer.py already documented for the same rootless-Podman netns problem; that script's docstring now documents both). Capture and analysis needed two real fixes found only by running the pipeline live: `-U` (unbuffered tcpdump writes) plus a `pkill -f <pcap path>` fallback, since `podman unshare -> nsenter -> tcpdump` is a 3-level subprocess chain and SIGTERM to the top-level process doesn't reach the tcpdump grandchild, leaving an orphaned process and a truncated/unreadable pcap; and filtering on the container's internal listening port (20128) rather than the dynamically-assigned host port, since capture happens inside the container's own network namespace where only the internal port is meaningful. live-default-combo-wire-capture.test.ts (gated on RUN_LIVE_WIRE_CAPTURE=1) ties it together: sends a small representative sample of requests through the real default combo, then cross-checks each one's app-level JSON status against the actual HTTP status line observed on the wire via scripts/sre/tcp-close-analyzer.py's stream reassembly — catching bugs where the app layer claims success but the wire shows a truncated/reset stream, not just what liveDefaultComboShared.ts's existing breadth suite already covers. Live-verified end-to-end: 4/4 sampled requests correlated correctly across 8 captured TCP streams, container + capture process fully torn down afterward (verified no orphaned podman container or tcpdump process left running). sendModelRequest/filterActiveModelTargets (liveDefaultComboShared.ts) gain optional baseUrl/apiKey overrides, defaulting to the existing module-level omniroute-beta target, so the wire-capture suite can point the same request-sending logic at its own dedicated container instead. (cherry picked from commit 914a7e42cbe914f257db9f72eedc902ee1532083) --------- Co-authored-by: Markus Hartung <mail@hartmark.se> * maint: follow-up cherry-pick fix-in-place #9741 (conflict-resolved fallback) (#9895) * fix(responses-api): sync reasoning-cache write index with the fixed read side The turn-index-hardcoding fix updated the reasoning-cache read side (translator/index.ts's main replay loop) to key lookups by the assistant message's real position in the messages array, but two other spots still used the old hardcoded convention: - chatCore.ts's write side (both the streaming and non-streaming completion paths) still cached every response under a hardcoded messageIndex: 0. - translator/index.ts's own plain-turn (non-tool-call) cache-key lookup ALSO still hardcoded messageIndex 0 at its call site — a second, previously undiscovered instance of the same class of bug, found while re-verifying this fix against the current upstream tip (the original fix only addressed the write side). Past the first assistant turn these conventions no longer matched, so DeepSeek/Xiaomi-mimo plain-turn reasoning replay silently missed the cache and fell back to the placeholder (or, once #9573 removed the placeholder fallback, to an absent field) in ordinary multi-turn conversations. Compute the write-side index from the incoming request's message count instead, and use the real loop-provided messageIndex on the read-side lookup, both matching the position the response occupies once the client appends it to history for the next turn. Note: this was originally part of a larger squashed fix (output_index collision prevention across reasoning/message/tool_call items, reasoning-content-alias generalization) that has since been superseded by upstream's own independent fix — translator/response/openai-responses.ts now has its own dense-output-index-sort + getReadableReasoningValue implementation (own comment: "mirrors upstream PR #721"). Only this narrower, still-genuinely-broken write/read index sync survives as a distinct bug. Test plan: - TDD: tests/unit/reasoning-cache.test.ts's new end-to-end "write side (chatCore's messageIndex) and read side (translateRequest) agree on the same key end-to-end" test, plus the pre-existing "should inject placeholder for a plain (non-tool-call) DeepSeek turn" and "should replay cached reasoning for a plain (non-tool-call) DeepSeek turn when available" tests — confirmed failing against the pre-fix code on a clean release/v3.8.50 checkout (both the hardcoded-0 write side AND the hardcoded-0 read-side lookup independently reproduce the mismatch), passing after both fixes - npm run typecheck:core — clean - npm run lint — clean - npm run check:file-size — clean (chatCore.ts rebaselined 5034->5042 for the messageIndex computation at both call sites; reasoning-cache.test.ts frozen at 1035, matching the original fix's own rebaseline) - 2 pre-existing, unrelated test failures in the same file ("should replace empty-string reasoning_content with NON_ANTHROPIC_THINKING_PLACEHOLDER on cache miss", "should inject placeholder for a plain (non-tool-call) DeepSeek turn missing reasoning_content") confirmed present on a completely clean, untouched release/v3.8.50 checkout — these test obsolete placeholder-injection behavior the code deliberately removed per #9573 (see the code's own comment); not touched by this PR * fix(chat): reduce file size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(chat): reconcile file-size baseline Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Markus Hartung <mail@hartmark.se> * cherry-pick(pr-9738): feat(logging): make the chat-log truncation limit configurable, bumped default 128x (#9863) * 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. estimateSizeFast() (open-sse/utils/estimateSize.ts) has been substantially rewritten upstream since this bug was first found (now an iterative Frame-based walker with a separate node-visit budget, not the simple stack loop originally patched) — re-implemented the fix against the current algorithm rather than porting the old diff: the byte early-exit was unconditionally the module-level ESTIMATE_SIZE_BYTE_LIMIT (256 KiB) with no way for a caller to raise it, so any caller comparing against a bigger configured threshold could never see a size above ~256 KiB — every payload between 256 KiB and the caller's real limit looked "under threshold" and truncation never fired, the opposite of intended. Added an optional byteLimit parameter (default unchanged at ESTIMATE_SIZE_BYTE_LIMIT, so isSmallEnoughForSemanticCache's existing behavior is untouched) threaded through both the byte-check early-exit and the node-budget-exhaustion fail-closed fallback, with truncateForLog() now passing its own configured getChatLogMaxBodyBytes() value through. * feat(dashboard): show conversation session tag in request detail metadata Adds a "Conversation" field to the request detail panel's metadata grid (after "Combo"), showing the request's conversation id (sessionTag) for quick reference/copy. --------- Co-authored-by: Markus Hartung <mail@hartmark.se> * cherry-pick(pr-9735): feat(logging): bump CHAT_LOG_ARRAY_TAIL_ITEMS default 24 -> 128 (#9864) * feat(logging): bump CHAT_LOG_ARRAY_TAIL_ITEMS default 24 -> 128 Real agentic CLIs with many MCP servers routinely declare 40-50+ tools in a single request — a live OpenClaw session logged 47. The tail-24 default silently dropped the array's earlier entries behind an _omniroute_truncated_array marker, so investigating why a specific tool call (apply_patch) behaved oddly turned up nothing: its declared shape (function vs custom type) was unrecoverable from the call log across 40 recent requests, even though the calls themselves succeeded. Bumped the configurable default to comfortably cover real large tool lists with headroom. Updated .env.example and docs/reference/ ENVIRONMENT.md to match (env-doc-sync check passes). * test(logging): pin CHAT_LOG_ARRAY_TAIL_ITEMS default at 128 The bump commit had no dedicated test asserting the literal default value; the existing chatcore-log-truncation.test.ts derives its expectations from getChatLogArrayTailItems() itself, so it can't discriminate a regression back toward the old, too-small 24 default. --------- Co-authored-by: Markus Hartung <mail@hartmark.se> * fix(logging): use configurable max-depth when bounding logged tool_calls (#9865) requestLogger.ts's cloneBoundedForLog had its own hardcoded depth cap of 6, independent of the existing configurable getChatLogMaxDepth(). A typical Chat Completions response body's responseBody.choices[0].message.tool_calls[0].function sits at exactly depth 6, so every logged tool call's function field (name+arguments) was silently replaced with the literal string "[MaxDepth]" before ever being stored — corrupting the data, not just how it renders. Bumped the shared default 6->20 and switched requestLogger.ts to read it instead of using its own literal. (cherry picked from commit a2df6cf289cbab7cd618b8e55272434812f7a4a7) Co-authored-by: Markus Hartung <mail@hartmark.se> * fix(executors): repair DuckDuckGo AI Chat challenge solver (418 ERR_CHALLENGE) (#9866) Every duckduckgo-web chat request failed with HTTP 418 ERR_CHALLENGE while duck.ai worked normally in a browser from the same IP. Ground truth was established by driving a real headful Chromium at duck.ai from that IP (it returned 200), so the environment was never the problem — the anti-abuse challenge solver was. Six independent defects were found; the first alone disabled the solver completely. 1. Module syntax inside the vm sandbox source. CHALLENGE_STUBS is executed with vm.runInContext, which compiles in SCRIPT mode. A refactor mass-added `export` to the five `function` declarations inside that template literal (they read as ordinary top-level TS functions), so every solve threw SyntaxError. The executor swallows solve failures and posts the raw unsolved challenge, which upstream answers with 418. 2. Double-escaped regex in a String.raw template. `\\s` in __parseCssDisplay reached the sandbox as a literal backslash, so the display regex never matched and a getComputedStyle probe silently read empty. 3. buildHtmlLookup undercounted descendants by one. `count` backs el.querySelectorAll('*').length; that returns DESCENDANTS and countHtmlElements already skips the #document-fragment root, so the `- 1` was wrong. Chromium reports 3 for '<li><div></li><li></div'; we reported 2, and a variant multiplies innerHTML.length by that count. 4. Browser-fidelity probes. Newer challenge variants assert JS/DOM invariants a flat stub cannot satisfy: real prototype chains (HTMLDivElement -> HTMLElement -> Element), NodeList identity, a live body.children HTMLCollection, native-code toString, and sloppy-mode `this === window`. Nine of thirteen failed. Notably Math must NOT be sealed — Chromium reports Object.isSealed(Math) === false, and sealing it made our vector differ by one. 5. The solved payload dropped meta.origin / meta.stack / meta.duration. The duck.ai bundle always sends all three; captured browser requests confirm it. Without them upstream returns 418 even when every client_hash is correct. 6. reasoningEffort is now mandatory on duckchat/v1/chat. An otherwise byte-identical payload returns 200 with the field and 400 ERR_BAD_REQUEST without it (A/B verified live, repeated). Also removes the throwaway "seed" chat POST that ran before every real request. It existed to coax a usable challenge out of the upstream while the solver was broken; it only doubled chat calls against an IP-rate-limited endpoint, showing up as spurious 429 ERR_RATE_LIMIT. Verification: the solver now reproduces real Chromium's probe vectors exactly for all 8 captured challenge variants, and the executor returns 200 end-to-end live (non-streaming, streaming, claude-haiku-4-5, and a math prompt returning "42"). Tests: tests/unit/duckduckgo-challenge-solver-regression.test.ts (32 tests) and tests/unit/duckduckgo-reasoning-effort-required.test.ts (5 tests), backed by tests/fixtures/duckduckgo/challenge-variants.json — real captured challenge programs plus the probe vectors a real browser produced for them, so the suite asserts against recorded browser behaviour rather than our own output. Each fix was confirmed to fail its test when individually reverted. Co-authored-by: Mynacol <git@mynacol.xyz> * cherry-pick(pr-9730): fix(compression): persist RTK renderer configuration (#9867) * fix(compression): persist RTK renderer configuration * docs(changelog): add fragment for #9730 Adds the changelog.d/fixes/9730-persist-rtk-renderers.md fragment required by check:changelog-integrity for the RTK enableRenderers persistence fix in PR #9730. --------- Co-authored-by: Isaac <isaaclyons98@gmail.com> * fix(dashboard): unregister leftover service workers in dev mode (#9868) A phone that previously loaded a production build on this origin (or an old dev build from before the registration was gated) kept an active service worker across dev restarts. It intercepted every navigation/asset fetch, occasionally serving a JS chunk that didn't match the running dev server, which tripped Next's dev-client chunk-mismatch auto-reload — visible as an unexplained, unstoppable refresh loop on that device only (confirmed via a clean private tab on the same phone/URL not looping). PwaRegister now actively unregisters any existing service worker registrations and clears their caches outside production, instead of just skipping a new registration. (cherry picked from commit 66a2515cbce7a6132639614d88d48349a83bdcde) Co-authored-by: Markus Hartung <mail@hartmark.se> * fix(combo): remove stray brace from #9630 error handling (#9894) Co-authored-by: Zartharas <1402357+Zartharas@users.noreply.github.com> * feat(oauth): add Openference OAuth and API key provider integration (#9869) Wire Openference as a first-party OAuth gateway (PKCE, rotating refresh) and an API-key catalog entry on api.openference.com, with live model discovery, connection testing, free-tier badges, and regression tests. Co-authored-by: Anh Tran <anhlead@outlook.com> * maint: follow-up cherry-pick fix-in-place #9719 (conflict-resolved fallback) (#9893) * fix(db): clear combo pins when connections are deleted * docs: add changelog entry for #9719 --------- Co-authored-by: Zartharas <1402357+Zartharas@users.noreply.github.com> * cherry-pick(pr-9718): feat(src): proxy-pool-toolbar-minor-improvements (#9870) * feat(proxy-pool): streamline pool actions * test(proxy-pool): cover toolbar layout * refactor(settings): extract proxy registry helpers Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(settings): reduce proxy registry component size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Agnes <linkscrazy2@gmail.com> * feat(resilience): expose providerQuotaOverrides via /api/resilience (#9871) Co-authored-by: herjarsa <herjarsa@users.noreply.github.com> * maint: follow-up cherry-pick fix-in-place #9712 (conflict-resolved fallback) (#9892) * fix(build): colocateLlmlinguaOptionals skip-check treated a Next-traced stub as fully copied Debugging the omniroute-beta Docker rebuild: `npm run build` (and the Dockerfile's own post-build verification) failed with `Cannot find module '.../node_modules/@atjsh/llmlingua-2/dist/index.js'`. Root cause, reproduced directly (both against a live Docker builder image and in a unit test): Next.js's own standalone trace creates a stub directory for `@atjsh/llmlingua-2` containing only `package.json` — it references the package (a dynamically-imported optional dependency) but can't fully bundle it. colocateLlmlinguaOptionals's skip checks (both the closure-level early return and the per-package loop) only tested `existsSync(dest)`, so that stub was indistinguishable from "already fully co-located" — the function skipped copying the real `dist/` output entirely, silently shipping a package with a manifest but no code. Fix: check for the package's declared `main` entry file when it has one (the real-world case for every actual SLM optional). Packages with no `main` field fall back to comparing the destination's top-level entries against the source's — correct both for genuinely multi-file packages and for a metadata-only source (package.json is then its complete, faithfully- copied contents), which the existing idempotency test exercises. Covered by tests/unit/colocate-optionals.test.ts's new stub-reproduction case (fails against the pre-fix code, passes after — confirmed directly) plus the 6 pre-existing cases, all still green. (cherry picked from commit 359aba59c7b362a5efaa4f0cd4d482d48ee2df66) * fix(build): register onnxruntime-node's native bin/ as a standalone asset (#9687) Docker/standalone builds of the LLMLingua SLM compression tier failed at runtime with "Error: libonnxruntime.so.1: cannot open shared object file: No such file or directory" (open-sse/services/compression/engines/llmlingua's worker, via @huggingface/transformers -> onnxruntime-node). onnxruntime-node's dist/binding.js is a normal JS file Next.js's standalone trace bundles correctly, but binding.js dlopen()s a platform-specific native library shipped under bin/napi-v3/<platform>/<arch>/libonnxruntime.so.1 — a dynamic native load static file tracing can't see (same blind-spot class as the separate colocateLlmlinguaOptionals stub bug, just for a .so instead of a JS import, via NATIVE_ASSET_ENTRIES instead). That directory was simply never registered, unlike better-sqlite3's native binary, which already goes through the exact same mechanism correctly. Fix: add an entry for onnxruntime-node/bin, mirroring the existing better-sqlite3 entry. Confirmed against a real Docker build of the Dockerfile's own post-build verification step: this was the very next failure once the separate llmlingua-2 stub bug was fixed and the build progressed far enough to reach it. Covered by tests/unit/assemble-standalone-onnxruntime-native-asset.test.ts (fails against the pre-fix code on both assertions, passes after). (cherry picked from commit 8c98a59f26a27e844678e673b31c1c21aaf72b0e) --------- Co-authored-by: Markus Hartung <mail@hartmark.se> * maint: follow-up cherry-pick fix-in-place #9707 (conflict-resolved fallback) (#9890) * fix(db): renumber ccr_blocks migration 134 -> 139 134 was taken by 134_proxy_logs_egress_ip, so two migrations shared the same numeric prefix and check-migration-numbering failed. Move ccr_blocks to the next free slot and add the retroactive isSchemaAlreadyApplied guard so a DB that already applied it under 134 skips the re-run. * fix(combo): restore missing preferAntigravityConnectionsWithStoredProject quotaStrategies imported the reset-aware pool filter from ../antigravityProjectPersistence.ts, a module that does not exist — the helper belongs in antigravityProjectPersist.ts and was never added there, breaking typecheck. Add the helper alongside the persist path, point the import at the real module, and cover the filter with unit tests. * chore: add Makefile wrapping the canonical npm scripts * fix(compression): remove duplicate Antigravity project helper The release branch already includes the generic project-aware connection selection helper. Keep that implementation and remove the duplicate introduced while cherry-picking #9707. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Matias Baglieri <168452313+matiasbaglieri@users.noreply.github.com> * cherry-pick(pr-9695): fix(docker): make the webpack build-arg escape hatch actually work (#9872) * build(docker): make the bundler build-arg actually take effect A bare ENV shadows a same-named ARG for the rest of the stage, so --build-arg OMNIROUTE_USE_TURBOPACK=0 was silently ignored and the webpack escape hatch the surrounding comment advertises only ever worked through -e at runtime, never at build time. That mattered because Turbopack compiles in native Rust memory living outside the V8 heap, so OMNIROUTE_BUILD_MEMORY_MB cannot bound it. A build host with a memory ceiling gets SIGKILLed by the cgroup OOM killer with no error text at all, which reads like a hung build rather than an out-of-memory one. * docs(docker): correct the builder stage facts and document its cost The stage table described a builder that no longer exists: it named node:24.15.0-trixie-slim where every stage now derives from node:26-trixie-slim, and said the stage runs `npm run build -- --webpack` where it runs plain `npm run build`, which is Turbopack by default. That second one is worse than stale. A reader who needs the webpack fallback would conclude the Docker build already uses it and never look for the switch. Adds a Build-time resources section covering the two build args, why the V8 heap arg cannot bound Turbopack, and measured ceilings for both bundlers. The runtime paragraphs that followed get their own heading so they no longer read as part of the build-time story. * docs(docker): correct the runtime heap defaults Same drift as the builder stage, in the paragraphs just below it. The image exports OMNIROUTE_MEMORY_MB=1024 and derives NODE_OPTIONS from it, but the guide reported 512 in three places, including the environment variable table. The "if unset, the launcher uses 512" line was misleading in both readings: the image always sets the variable so that branch cannot fire under Docker, and outside Docker the launcher calibrates from host RAM rather than using a flat 512. * docs(changelog): add fragment for #9695 --------- Co-authored-by: Minxi Hou <houminxi@gmail.com> * maint: follow-up cherry-pick fix-in-place #9693 (conflict-resolved fallback) (#9887) * fix(web-tools): anchor tool contract at prompt tail + user-turn reminder The <tool> contract from prepareToolMessages was prepended as the first system message. Web executors fold all system messages into one block, so with agentic clients whose system prompts exceed ~28K chars the contract sat at the head of a huge block and web models ignored it, refusing tool calls with "tool X is not in my tool set" (chatgpt-web, 0/3 at 30K chars). Two changes, both required in testing: - Dual placement: the full contract now rides as a trailing system message (folds to the tail of the system block) and a one-line reminder naming the tools is appended to the latest user message. - Rewording: the contract now frames injected tools as client tools invoked via a plain-text protocol, distinct from the model's native tool registry (web.run, python.exec, ...), and instructs the model to never claim they are unavailable. Without this the model resolved tool names against its native registry and refused even when it had seen the contract. Measured on cgpt-web gpt-5.5-thinking/gpt-5.6-thinking/o3: prepend 0/3 tool calls at 30K chars; dual placement 16/17 across 30K-250K system prompts, 30-tool sets, multi-turn tool history, streaming, and 3-way concurrency, with no spurious calls on no-tool prompts. Known limit: ~40K-char single user messages still flake (2/3) due to the upstream model's own injection heuristics. All prepareToolMessages consumers parse system messages position-independently and select the current user turn by role scan, so the trailing system message is shape-safe for every web executor. * test(web-tools): cover contract placement edge cases --------- Co-authored-by: Ryan Brosas <ryanjoserbrosas@gmail.com> * maint: follow-up cherry-pick fix-in-place #9631 (conflict-resolved fallback) (#9886) * feat(db): add a job registry for scheduled background work Background jobs each ship their own timer today, so there is no list of what is scheduled, no history of what ran, and no way to pause one without an environment variable and a restart. The registry gives them one home: a jobs table holding the schedule, a job_runs table holding the outcomes, and a loopback-only API to inspect and control both. Cron jobs read their expression through an optional cronGetter rather than the stored column, so an operator changing OMNIROUTE_WARMUP_CRON does not need the row rewritten. register() is an idempotent upsert that refreshes the schedule but never overwrites `enabled` or `created_at`, which is what lets a job be re-registered on every boot without discarding the operator's toggle. Run history is pruned per job rather than globally, and safeRun records a failure for a handler that throws as well as one that returns success:false, so a crashing job leaves a trail instead of a gap. The API is under /api/jobs and gated to loopback in the route guard. It can trigger a run and flip a job off, which is runtime administration and does not belong on a remotely reachable surface. Signed-off-by: Minxi Hou <houminxi@gmail.com> * feat(jobs): move the budget reset and token health check onto the registry Both jobs owned their own timer and started themselves as an import side effect, so nothing could report whether they were running, when they last ran, or why a run failed. They now register with the job registry and are started from it, which also means their schedule and run history are visible through /api/jobs. startAll() runs each interval job's first tick synchronously, so both entry points start the registry only after initializeCloudSync() has been awaited. The old wiring reached that ordering two different ways: the budget reset was started after the init call, and the health check's first sweep sat behind a 10s timer. Replacing both with one startAll() would otherwise have moved the two handlers in front of the initialisation they run against. Both entry points also register the same pair of jobs. Registering one and not the other is how a background job goes missing without anything failing. sweep() now returns how many connections it swept, so the health check can record a real records_affected the way the budget reset does. The migration documents that column as a per-job count, and hardcoding zero would have left one of the two jobs reporting a number the schema promises but the code never produces. A skipped or empty sweep reports zero. Every existing caller ignores the return value. The token health check keeps its own disable semantics: the handler still calls isHealthCheckDisabled() before sweeping, so OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK, the production-build phase and the automated-test guard behave as before. Its registry adapter lives in src/lib/jobs/ next to the budget reset rather than in tokenHealthCheck.ts, which is already above its frozen size ceiling on the base branch and should not grow further. The adapter lets a failing sweep throw rather than reporting it itself, matching the budget reset: safeRun records a thrown error as a failure run with its message. The warmup job is seeded disabled. Its handler arrives with the warmup scheduler, and startAll() filters on enabled before it looks for a handler, so seeding it enabled here would warn about the missing handler on every boot. * fix: allowlist cron-parser dep and document OMNIROUTE_RUNNOW_TIMEOUT_MS env var Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> * fix: pass max reasoning effort through by default, add global model registry fallback (#8057) (#9883) Co-authored-by: Mo'men Qatr <momen.qatr04@eng-st.cu.edu.eg> * cherry-pick(pr-9605): ci(test): route orphaned Vitest tests through blocking CI (#9875) * ci(test): route orphaned Vitest tests through blocking CI * docs: fix advisory status in AGENTS.md and refresh baseline note * fix(changelog): fix fragment format for #9415 * fix(changelog): preserve upstream fragment format --------- Co-authored-by: MohitRawat017 <rawatmohit17906@gmail.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> * cherry-pick(pr-9601): feat(responses): add encrypted reasoning replay opt-in (#9876) * feat(codex): add encrypted reasoning replay opt-in * feat(responses): generalize encrypted reasoning replay * docs: clarify encrypted reasoning provider scope * fix(ui): group reasoning replay with connection controls * fix(logs): omit encrypted reasoning payloads * fix(chat): reduce file size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(chat): reduce combined file size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: jackjinke <jack.kejin@gmail.com> * cherry-pick(pr-9572): fix(providers): reject the dashboard password as a connection API key (#9877) * fix(providers): refuse to store the dashboard password as a connection API key A browser autofilled the management password into a connection's API-key field. The resulting credential authenticates against nothing, so every request routed through that connection came back 401, and because the field looks like any other password input the same autofill fired again while the connection was being repaired by hand. The refusal belongs on the write path rather than in the form. Twenty routes create or update connections and all of them funnel through createProviderConnection and updateProviderConnection, so one check there covers every entry point including a future one. The two other places that write api_key are left alone on purpose: one re-encrypts rows that already exist and the other is the one-time db.json import, and neither takes a value an operator just typed. Update checks the incoming value, never the merged one. A connection that already holds the password has to stay editable or the operator cannot repair the exact state this prevents, and re-checking the merged value would spend a bcrypt round on every unrelated field edit. Only a real match blocks the write. An unreadable settings row or a throwing bcrypt call logs and allows, because a guard against one specific mistake must not turn into a way to lock out every connection write. Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(providers): compare the untrimmed credential, and cover the guard's branches The guard trimmed the incoming value before comparing it, which catches a paste carrying whitespace the password does not have. It missed the mirror case: neither the login route nor the set-password route trims, so a dashboard password may itself begin or end with a space, and an autofill reproducing it exactly was trimmed into a value that no longer matched the stored hash. The write then went through, which is the state this guard exists to prevent. Both forms are compared now, the second only when the first fails on a string that differs, so an ordinary key still costs a single bcrypt round. Two branches carried no coverage and both are load-bearing. The catch that logs and allows is the only path that lets a write through; a stored hash bcrypt cannot parse reaches it without needing a mock, since the shape check accepts an impossible cost factor that the comparison then rejects. The early return is what keeps a token renewal -- a write carrying tokens but no apiKey -- from paying for a settings read and a bcrypt round every time it fires, and the same unparseable hash makes that path observable, so an absent warning is proof the return happened. The narrower scope is deliberate and now says so in the code: the OAuth tokens arrive from a provider's token endpoint rather than from a form, so extending the comparison to them would charge every renewal for a field no autofill can reach. Signed-off-by: Minxi Hou <houminxi@gmail.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: Minxi Hou <houminxi@gmail.com> * cherry-pick(pr-9569): fix(settings): use provider prefixes in model overrides (#9878) * fix(settings): use provider prefixes in model overrides * refactor(settings): extract pricing tab helpers Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> * fix: address self-review findings (#9900) Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> * cherry-pick(pr-9675): fix(providers): per-provider opt-out for anonymous no-auth fallback (opencode-go/zen 401s) (#9873) * fix(providers): add per-provider opt-out for anonymous no-auth fallback API-key providers with anonymousFallback: true (opencode-go, opencode-zen, pollinations, kilocode) receive a synthetic "noauth" connection whenever all real connections are terminal (credits_exhausted/banned/expired) or unavailable. The opencode upstream now rejects anonymous requests with 401 Missing API key, so the fallback adds a guaranteed-failing round trip and health/reconnect noise before the combo moves on. Add a noAuthFallbackDisabledProviders settings array (zod-validated, persisted via /api/settings, following the blockedProviders pattern). When a provider is listed, maybeSyntheticNoAuthFallback returns null for anonymousFallback-only providers, so exhausted providers are skipped immediately as allExpired/allRateLimited while real keyed connections keep working and recover automatically once quota state clears. True no-auth providers are unaffected; blockedProviders remains their disable mechanism. Default (absent/empty list) preserves current behavior. Provider detail pages for anonymousFallback providers gain an "Anonymous fallback" toggle (default ON) backed by the new setting. Refs #9674 * fix(auth): reduce file size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Hermes Agent <hermes@hermes-chloe.hyades.io> * cherry-pick(pr-9634): fix(test): reconcile base-drifted test expectations on release/v3.8.50 (#9874) * fix(combo): restore routing module load * fix(db): resolve ccr migration version collision Renumber the CCR block-store migration from 134 to 139, reconcile databases that already applied the legacy slot, and add regression coverage for both upgrade paths. Co-Authored-By: GPT-5 <noreply@openai.com> * fix(changelog): format the aggregator balance fragment as a bullet The fragment landed with YAML frontmatter rather than the bullet the aggregator reads, so check:changelog-integrity exits 1 on every branch and takes the merge-integrity job down with it regardless of what the branch changed. Only the format changes. The entry text is the author's, unedited, and now carries the link to the pull request that shipped it. * fix(test): update expected auth/vision/provider schema for base-drifted expectations * fix(test): narrow this branch to the drifted test expectations Three other PRs already cover what this one was carrying. #9618 renumbers the colliding ccr_blocks migration, #9632 repairs the malformed aggregator changelog fragment, and #9676 restores the combo module load by implementing the selection helper the import was reaching for, rather than deleting the caller the way this branch did. Keeping any of it here would put two files back on the same migration slot and overwrite a better fix with a worse one. What survives is the part none of them touch. Once the combo barrel loads again, three assertions in the context-window filter suite start failing: they demand that catalog-too-small targets be dropped, while the file's own header and its four neighbouring tests say those targets stay available as runtime fallback. The unresolved import was masking them. A new case pins the output-token limit as a genuine hard requirement so the relaxation cannot drift further. The provider count assertion kept one literal at the old value after the rest of the file moved to 198, so the partition check failed on a sum that was correct. * chore(quality): re-time migrationRunner for the 139 guard on the new tip --------- Co-authored-by: alexey.nazarov@softmg.ru <alexey.nazarov@softmg.ru> Co-authored-by: GPT-5 <noreply@openai.com> Co-authored-by: Minxi Hou <houminxi@gmail.com> * cherry-pick(pr-9556): fix(translator): preserve Kimi K3 Responses reasoning (#9879) * fix(translator): preserve Kimi K3 Responses reasoning * fix(translator): make K3 reasoning preservation model-driven * fix(translator): replay cached Kimi reasoning before fallback * fix(translator): keep authentic K3 reasoning through cleanup * refactor(reasoning): use replay policy for K3 --------- Co-authored-by: jackjinke <jack.kejin@gmail.com> * maint: follow-up cherry-pick fix-in-place #9510 (fallback resolution) (#9880) * feat(api): add GET /api/resilience/connections for per-account state The three temporary-failure mechanisms each have their own scope -- the provider circuit breaker covers a whole provider, connection cooldown covers one account, model lockout covers a provider/connection/model triple -- and until now nothing showed them side by side. Diagnosing "why is this key being skipped" meant reading three separate surfaces and correlating by hand, which is exactly what the docs' own debugging guidance asks an operator to do. The route returns all three keyed by connection, plus the breaker's transition history so a flapping provider is visible as a sequence rather than a single current state. getStatus() already assembled everything except that history; it now returns a copy of it and carries an explicit CircuitBreakerStatus type instead of an inferred one. Reading raw connection rows for this meant widening getRawProviderConnections' column projection, so the existing allowlist is exported and the route selects through it. A test asserts every column the route names is in that allowlist, which turns a future typo into a failure here rather than a silent empty field. Each of the three data sources is wrapped independently: one of them throwing degrades that section and sets meta.degraded rather than failing the whole response, since a partial view still answers most of the questions the page exists for. Loopback-gated. It spawns nothing, unlike every other entry on that list, but it exposes per-account operational state and the comment says so to keep it from being read as precedent for gating read-only routes generally. Tests are real isolated-DB integration tests rather than mocks -- ESM mocking is unavailable here (no mock.module, non-configurable exports) and the codebase already has the isolated-DB pattern, which exercises more than a mock would anyway. Signed-off-by: Minxi Hou <houminxi@gmail.com> * feat(dashboard): add the per-account resilience connections page Renders what the API added: every connection with its cooldown, its provider breaker, and its model lockouts in one table, with a detail view per connection and the breaker's transitions drawn as a timeline. The timeline is the part that is hard to get from the existing surfaces -- a breaker sitting at CLOSED right now looks healthy, and only the sequence shows it has opened four times in the last hour. Polls rather than streams. The state it displays changes on the order of seconds to minutes and the page is loopback-gated, so an SSE channel would buy nothing over an interval. ModelCooldownsCard had its own formatRemaining. The new table needs the same countdown format and two copies would drift, so it moves to shared/utils/formatRemaining.ts and both import it -- behaviour unchanged, the extracted version differs from the deleted one only in local variable names. DataTable's column and row interfaces are exported for the same reason: the new table types against them rather than restating their shape. Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(i18n): translate new resilience-connections screen strings PR #9510 added the "Connection Resilience" dashboard screen but the sync-added i18n keys (sidebar.resilienceConnections/Subtitle and the full resilienceConnections namespace) were left as __MISSING__: in every non-English locale, dropping i18nUiCoverage.pct below the 99 ratchet baseline. Translate all ~78 new leaf strings into all 41 non-English locales. Pre-existing unrelated __MISSING__ debt (hermesRole*, apiProtocol*, grokAutoTopUp*, featureFlagExposeFunctionalGatewayMirrorsDescription) is left untouched — out of scope for this fix. Co-authored-by: HouMinXi <HouMinXi@users.noreply.github.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: HouMinXi <HouMinXi@users.noreply.github.com> * maint: follow-up cherry-pick fix-in-place #9549 (conflict-resolved fallback) (#9881) * fix(adobe-firefly): open browser sign-in and resolve provider slug in /login POST /api/providers/[id]/login passed the connection DB id to inAppLoginService.startLogin, but that service looks up the provider by slug in TOKEN_EXTRACTION_CONFIGS. The lookup always missed and returned "No extraction config" without launching a browser — so the VibeProxy "Sign in" button for Adobe Firefly (and every other web-cookie provider) never opened a browser. Adobe Firefly additionally had no extraction config because its IMS JWT is never in cookies/localStorage — it only rides on the Authorization: Bearer header of firefly-3p.ff.adobe.io XHRs. - Resolve the provider slug from the connection row and pass the slug (not the DB id) to inAppLoginService.startLogin. - Add open-sse/services/adobeFireflyBrowserLogin.ts: a Playwright service that launches a visible browser at firefly.adobe.com and intercepts firefly-3p requests to capture the IMS JWT + sherlockToken cookie. Wire it into the /login route for the adobe-firefly slug. - Fix latent bug: updateProviderConnection reads camelCase keys (apiKey, providerSpecificData), so the previous snake_case call never persisted extracted credentials. * fix(adobe-firefly): open browser sign-in and resolve provider slug in /login POST /api/providers/[id]/login passed the connection DB id to inAppLoginService.startLogin, but TOKEN_EXTRACTION_CONFIGS is keyed by provider slug — so browser login never launched for web-cookie providers. Adobe Firefly also cannot use cookie extraction: the IMS JWT only appears on Authorization headers to firefly-3p.ff.adobe.io. Add a dedicated Playwright interceptor and persist credentials with camelCase keys that updateProviderConnection actually reads. * fix(adobe-firefly): use system Chrome/Edge CDP for browser sign-in Playwright is not available inside the pkg-packaged VibeProxyServices.exe, so import('playwright') always failed with 'Playwright not installed' and never opened a window. Launch Chrome/Edge with --remote-debugging-port and capture the firefly-3p Authorization Bearer via pure CDP WebSocket instead. * fix(adobe-firefly): live x-arp-session-id / Arkose wire (stop 408 under load) Browser generate-async requires x-arp-session-id as base64({sid,ark,ftr}) with a real Arkose blob (sherlockToken). JWT alone frequently returns colligo HTTP 408 system under load while credits still work. - Match live ftr magic __UDF43-m4_31ck + Arkose pk in synthetic ARP fallback - Ranked extract of sherlockToken / x-arp from Cookie, HAR, fetch() paste, and space-joined JWT+ARP (PasswordBox newline collapse) - Reuse one ARP for storage upload + generate-async - Clearer 408 errors when browser ARP is missing vs stale - Unit suite 42/42 * fix(adobe-firefly): durable session ARP rebuild and aux_sid false-positive Rebuild x-arp-session-id from forterToken/arkose/ff_session_guid instead of ranking long Cookie pairs (e.g. aux_sid=…) as opaque ARP, which caused colligo HTTP 408. Cache IMS JWT + cookie sessions, rotate ARP on 408 retries, and keep Playwright warm-up opt-in only (headless Forter is rejected). Also expand synthetic ARP shape with bfp/fpjs to match live successful captures. * fix(adobe-firefly): durable session, off-screen Chrome recovery, browser sign-in Rebuild x-arp-session-id from Cookie pieces (sid/ark/forter) so aux_sid is never sent as ARP. Sticky ARP + submit spacing reduce mid-batch colligo 408 thrash. Add optional managed Chrome warm (off-screen headed by default; Forter rejects headless) and POST /api/providers/{id}/login browser sign-in that returns JWT+Cookie after a fresh SSO. Visible sign-in resets off-screen window placement and clears prior Adobe session when adding another account. * fix(adobe-firefly): renew sessions through durable CDP * fix(adobe-firefly): isolate browser sessions per account * fix(adobe-firefly): make account login fresh and deterministic * chore(adobe-firefly): remove obsolete browser fallback * docs(adobe-firefly): document renewal controls * fix(adobe-firefly): harden CDP warm, risk session, and browser sign-in Stop colligo 408 thrash from stale Forter and frozen Google login during Sign in with browser: - CDP warm: clear Firefly origin storage + risk cookies (keep SSO); require forter age under 10 minutes on loop and timeout paths; dual CDP queues; await Runtime.runIfWaitingForDebugger; profile-lock launch retries - Session: connectionId fingerprint; write-back JWT+Cookie; warm-fail cooldown; fail closed risk_session_stale when forter is known-stale - Client: submit gate around generate-async; max 2 attempts when forter known-stale; poll 401 one refresh; pass sessionBrowserKey through handlers - Login route: pure system Chrome/Edge CDP only; camelCase credential persist - Unit: browser-login + firefly suites green (60) --------- Co-authored-by: artickc <artur1992123@mail.ru> * fix(db): resolve ccr migration version collision (#9884) Renumber the CCR block-store migration from 134 to 139, reconcile databases that already applied the legacy slot, and add regression coverage for both upgrade paths. Co-authored-by: fenix007 <fenix007@users.noreply.github.com> * maint: follow-up cherry-pick fix-in-place #9629 (conflict-resolved fallback) (#9885) * fix(compression): add Lite tool truncation toggle * fix(antigravity): add missing antigravityProjectPersistence.ts module The quota-strategy engine (quotaStrategies.ts) imports from antigravityProjectPersistence.ts, but only antigravityProjectPersist.ts existed in the tree. Add the missing module with the expected preferAntigravityConnectionsWithStoredProject() helper and re-export the existing persistDiscoveredAntigravityProjectId(). Co-authored-by: diegosouzapw <diegosouza.pw@outlook.com> * fix(file-size): rebaseline strategySelector.ts for Lite truncation toggle The PR adds one line to threading options?.config?.lite into applyLiteCompression. Update the frozen size from 1060 to 1061. Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Refs #9629 --------- Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: xz-dev <xz-dev@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> * maint: follow-up cherry-pick fix-in-place #9704 (conflict-resolved fallback) (#9889) * fix(sse): persist per-tool-call JSON escape state across SSE delta chunks escapeJsonStringValues() reset its inString/pendingEscape state on every call instead of carrying it forward per tool-call index, so a raw newline byte (or an already-escaped \n) split across two delta chunks got corrupted in transit — the model's own output was correctly escaped, OmniRoute broke it. Root-caused via a dispatched investigation into real OpenClaw traffic that looked like model-generation quality but wasn't. Fix: escapeJsonStringValues now takes and mutates a persistent per-call state object (JsonStringEscapeState), keyed per tool-call index in the translator's init state and cleared when a tool call is superseded. * chore(quality): rebaseline openai-responses.ts for the escape-state fix Own growth from the extracted per-tool-call JSON escape-state fix (previous commit): open-sse/translator/response/openai-responses.ts 1204->1249 (+45). --------- Co-authored-by: Markus Hartung <mail@hartmark.se> * maint: follow-up cherry-pick fix-in-place #9711 (conflict-resolved fallback) (#9891) * fix(sse): grace period before finalizing a client disconnect as 499 (#9653) A client that closes its connection right after reading a fully-completed SSE stream can race OmniRoute's own completion bookkeeping: the bytes already reached the client, but the transform stream's own completion callback (onStreamComplete, which flips streamCompletionRecorded) hasn't finished bubbling up when the disconnect handler fires, so the request gets persisted as a false 499 with zero token usage even though it delivered its full response. Confirmed live on real traffic before this fix: a request whose server log showed "disconnect: request_signal_aborted" at 18236ms was persisted with status 200 and full token usage (82814/1292) once the grace period let the real completion win the race, matching what the client actually received. createClientDisconnectGraceHandler (new leaf in streamFailureFinalization.ts) polls isStreamCompletionRecorded() for up to STREAM_DISCONNECT_GRACE_PERIOD_MS (default 10s, env-configurable, 0 disables) before finalizing as a failure. If a real completion lands within the window, handleStreamFailure's own guard is a no-op and the genuine 200 stands. Covered by tests/unit/stream-disconnect-grace-period-9653.test.ts (fake-timer driven: already-recorded completion short-circuits, disabled-grace-period finalizes immediately, a completion landing mid-window skips finalize entirely, and no completion ever landing finalizes once the deadline passes). (cherry picked from commit 5d0fe28c4246518f7c7b588795a6da1573b5df16) * chore(quality): rebaseline chatCore.ts for the disconnect grace-period fix Own growth from the disconnect grace-period fix: 5030->5039 (+9, the createClientDisconnectGraceHandler wiring at the existing onClientDisconnectFinalize call site). --------- Co-authored-by: Markus Hartung <mail@hartmark.se> * chore: ignore playwright cli artifact dir * maint: final follow-up cherry-pick #9619 (#9901) * fix(quality): clears two release/v3.8.50 base-red gates Unblocks Merge integrity and Docs Gates for every PR against release/v3.8.50, not just this branch: - changelog.d/features/9415-newapi-sub2api-aggregator-balance.md had a non-standard YAML frontmatter header that no other fragment in the tree uses. check-changelog-integrity.mjs reads a fragment's first non-blank line to validate it starts with a markdown bullet; the frontmatter's leading `---` made that check fail regardless of the actual bullet content further down. Removed the frontmatter and reformatted the body to match the documented changelog.d/README.md bullet convention. - docs/ops/VM_DEPLOYMENT_GUIDE.md documented OMNIROUTE_MAX_POOL_SIZE and OMNIROUTE_DB_POOL_SIZE as tunable env vars, but neither is read anywhere in the codebase (confirmed via full-repo grep) — this repo uses SQLite, which has no connection-pool concept these vars could plausibly control. check:fabricated-docs --strict correctly flags fabricated env-var claims; removed the bullet rather than implementing a feature to match invented documentation. * fix(i18n): completes Vietnamese parity, fixes empty migration query Two more release/v3.8.50 base-red items, both surfaced while chasing CI failures on unrelated PRs: - vi.json was missing 8 keys that #9539 (NewAPI/Sub2API aggregator balance) added to en.json without a matching i18n:sync-ui run — pt-BR.json already had all 8, only Vietnamese drifted. Added translations for the 6 provider-settings strings, the feature-flag description, and the quota tooltip; verified against tests/unit/i18n-vi-completeness.test.ts (parity, placeholder preservation, ICU parse — all 5 assertions pass). - src/lib/db/migrations/120_interception_rules.sql was pure comments documenting a no-schema-change key_value namespace, with no executable SQL statement — the migration runner logged "FAILED: 120_interception_rules — Query contained no valid SQL statement" on every fresh DB init. 118_provider_param_filters.sql (same pattern, two migrations earlier) already ends with a bare `SELECT 1;` no-op for exactly this reason; 120 was just missing it. Verified directly against better-sqlite3 that the file now executes without error. * fix(types): clears 6 pre-existing release/v3.8.50 typecheck errors typecheck:core is its own blocking CI job (quality.yml), separate from Docs Gates/Merge integrity. Confirmed pre-existing and unrelated to any current work by branching this worktree directly from upstream/release/v3.8.50 with no other merges applied. - accountSemaphore.ts: isBypassed() already excludes null/<=0 maxConcurrency before ensureGate() is called, but a boolean- returning helper isn't a type predicate TS can narrow through. Added a targeted `as number` at the one call site, with a comment explaining why it's safe. - combo/comboStructure.ts: two module-scope `const HARD_COMPAT_REASONS` declarations with different values — a genuine "can't redeclare" compile error, not a narrowing gap. The first (4-item set including "output_tokens") had zero usages between its own declaration and the second; the second (3-item set, matching the CompatFilterOptions doc comment exactly) is what hasHardCapabilityFailure/ describeCapabilityFilterExhaustion/the third call site all actually use. Removed the dead first declaration. - combo/comboStructure.ts + combo/fusionPanel.ts: both accessed `.prompt`/`.model` on a `ComboModelStep | ComboProviderWildcardStep` union after only excluding `combo-ref`, but `ComboProviderWildcardStep` has neither field — a real latent bug (fusionPanel would have pushed `undefined` into a fusion panel for a wildcard step). Narrowed to `step.kind === "model"` in comboStructure, and switched to the already-existing `getComboModelString()` helper in fusionPanel (which correctly resolves to null for unsupported step kinds, mirroring how combo-ref is already skipped there). Verified directly via a standalone script exercising both branches (wildcard vs. model step). - combo/quotaStrategies.ts: imported `preferAntigravityConnectionsWithStoredProject` from a module that never existed (`../antigravityProjectPersistence.ts`, distinct from the real `antigravityProjectPersist.ts`) — the function itself was referenced nowhere else in the codebase. Wrote the missing implementation: prefers Antigravity connections with a discovered `projectId` for reset-aware routing, failing open to the full list when none have one yet (per the file's own "Exclude... from reset-aware pool" changelog note, softened to a preference — strict exclusion would empty the pool entirely for a fleet of freshly-added accounts). Verified directly via a standalone script. - compression/engines/ccr/index.ts: `enforceGlobalBudget(owner, bytes)` was called with only `bytes` at one of its two call sites, missing the `owner` argument the other call site (and the function's own doc comment on preferring the calling principal's LRU eviction) already uses correctly. Added the missing `entry.principalId` argument. - firecrawlQuotaFetcher.ts: `fetchFirecrawlQuota` was annotated to return `Promise<QuotaInfo | null>` but every return path constructs a `FirecrawlQuota` (QuotaInfo extended with remainingCredits/planCredits/ extraCreditsInferred/overPlan) — the type the file already defines and the type `parseFirecrawlCreditUsage` already correctly returns. Widened the annotation to match; `FirecrawlQuota extends QuotaInfo` so this stays compatible with the `QuotaFetcher` contract. npm run typecheck:core and npm run check:dashboard-typecheck both pass cleanly. A subset of DB-backed tests in this area also fail, but 100% attributably to an already-tracked, unrelated migration version collision (134 -> [ccr_blocks, proxy_logs_egress_ip], see _tasks/features-v3.8.4/9route/POST-MERGE-AUDIT.md) — confirmed by every failure's stack trace bottoming out at that exact error, not at anything touched here. * fix(sse): update stale ALL_ACCOUNTS_INACTIVE test assertions to ALL_TARGETS_SKIPPED Two combo-routing-engine.test.ts cases assert the pre-dispatch-skip scenario (isModelAvailable always false, zero dispatch attempts) returns ALL_ACCOUNTS_INACTIVE. Production code already distinguishes this case via the recordedAttempts === 0 branch and returns the more precise ALL_TARGETS_SKIPPED -- the tests were never updated when that branch shipped upstream, so they fail on a clean release/v3.8.50 checkout independent of this PR's changes. * fix(sse): update second stale ALL_ACCOUNTS_INACTIVE assertion (T24) Same pre-existing upstream test-drift as |
||
|
|
7ca73697b0 |
fix(i18n): localize hardcoded web UI copy (#9245)
* fix(i18n): localize hardcoded web UI copy * test(i18n): cover hardcoded UI regressions * chore(changelog): add PR 9245 fragment |
||
|
|
def5334768 |
feat(api-manager): add provider-level model permissions (#9313)
* feat(api-manager): add provider-level model permissions Persist canonical provider wildcards alongside exact model grants and preserve explicit restricted-empty deny-all semantics across API, SQLite, JSON import, sync, runtime policy, and the dashboard. Invalidate filtered model catalogs on permission changes and guard against stale in-flight catalog builders repopulating invalidated cache entries. * fix(api-manager): show provider and model counts separately in summary Provider wildcard selections (provider/*) are no longer counted as individual models in the Selected Models Summary. The header now shows "N providers · M models" when both are present, or just the non-empty category when only one type is selected. * fix(api-manager): separate provider and model permission displays * fix(api-manager): separate provider wildcard permissions in UI |
||
|
|
3898305df0 |
fix(rate-limit): separate queue wait from execution timeout (#9164)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
47c819df66 |
fix(combo): network errors must not trip provider circuit breaker (#9342)
* fix(combo): keep queue/network timeouts out of the provider breaker A single-model network error (ECONNREFUSED / proxy_unreachable) means we never reached the provider — the provider may be healthy while only the network path is broken. OmniRoute's own rate-limit queue timeouts are backpressure we applied, not an upstream failure. Neither should trip the whole-provider breaker. - chatPredicates: the single-model path excludes proxy_unreachable and RATE_LIMIT_QUEUE_* from the provider-breaker trip. - accountFallback.recordProviderFailure: isQueueTimeout short-circuits before the breaker ever counts (combo.ts already flags it from errorText). - chat.ts: the queue/network guard on the allRateLimited _onFailure trip. Deliberately leaves the combo same-provider dead-proxy leg (#8376) intact: there a proxy_unreachable on the next same-provider target must still be able to open the breaker, or a dead proxy burns every attempt until the 503 max-retry limit. Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(resilience): dedup same-provider network errors per event Same-provider combo targets can all fail the same single network event (a VPN blip) within one request. Without a dedup each target counts once toward the provider breaker, so one transient blip opens the whole-provider breaker while the provider is healthy — the antigravity outage this branch originally chased. recordProviderFailure now keeps a short per-provider window (10s) for proxy_unreachable failures: the first network error in a window counts, the rest of that window are the same event and return. A genuinely dead proxy keeps failing across requests (past the window) and still accumulates to its threshold, so the #8376 dead-proxy protection is not weakened. Covered by tests/unit/breaker-network-error-guard.test.ts: same-window errors dedup to one, cross-window errors still open the breaker. Signed-off-by: Minxi Hou <houminxi@gmail.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> |
||
|
|
fb83f43fca | fix(rate-limit): patch Bottleneck doExpire capacity leak (#9328) | ||
|
|
a367bf62f5 |
fix(providers): scope model-level targetFormat to declaring provider catalog (#9994)
Model-level targetFormat is provider-scoped endpoint semantics: a catalog entry declares how the DECLARING provider serves the model. getModelTargetFormat() fell back to getGlobalModel() when the provider's own catalog lacked the model id, importing another provider's tag into every provider serving that id. catalog. command-code serves gpt-5.6-luna over its chat-shaped /alpha/generate endpoint but inherited that tag, so chatCore translated the request to Responses format (messages -> input). CommandCodeExecutor.buildCommandCodeBody reads chat-format input.messages -> undefined -> [] -> upstream 502 "Invalid prompt: messages must not be empty" (call log 1786341194167-774a5b). Fix: resolve the provider alias (mirroring getProviderModels), only apply the provider's OWN catalog entry's targetFormat, and skip the global fallback when the provider has a catalog. Catalog-less providers keep the global fallback unchanged; ghe-copilot's Responses routing (#8835) is preserved. Regression test: tests/unit/provider-models-target-format-scoping.test.ts (red before the fix, green after). |
||
|
|
4795825513 |
fix(sse): make Claude effort/no-think catalog variants dispatchable on every provider (#9006)
* fix(executors): route Claude-via-Vertex through native rawPredict with real streaming
Claude models on Vertex AI were being sent through the generic OpenAI-
compatible partner endpoint, which 404s/errors for Claude on at least
some projects. Route them through Vertex's native Anthropic Messages
API (publishers/anthropic/.../rawPredict) instead, stripping the
body-level model field rawPredict rejects and injecting the required
anthropic_version field.
rawPredict only ever returns a complete JSON body, never real SSE
framing, so streaming requests now get a genuine Anthropic-format SSE
stream synthesized from that JSON (message_start/content_block_*/
message_delta/message_stop), which the existing claude-to-openai
response translator already knows how to parse.
Also fixes two response-format resolution bugs that silently dropped
a custom model's DB-stored targetFormat override whenever the model
id also existed in the static provider registry (as claude-sonnet-4-6
and claude-opus-4-7 do under vertex): resolveModelOrError had its own
ad-hoc resolution that never consulted the override, and even once
fixed, executeChatWithBreaker discarded the correctly-resolved format
before handleChatCore's own resolution ran a second time.
* docs: add changelog fragment for #8909
* refactor(sse): extract shared Claude effort-model predicate
* fix(sse): strip Claude effort-suffix ids for any provider serving a real Claude model
* fix(sse): keep no-think and CC-discovery catalog variant roots unprefixed
* fix(dashboard): re-qualify no-think playground model ids correctly
* fix(sse): scope Vertex 404s to a per-model lockout via passthroughModels
* docs: add changelog fragment for the Claude catalog/dispatch fix
* fix(sse): align regex naming and changelog formatting
* fix(sse): clarify effort-variant strip comment and add cross-module drift guard
* fix(sse): disambiguate Vertex connection-wide vs per-model 403s
* docs: document Vertex 403 disambiguation in changelog fragment
* fix(sse): correlate reason and resource within the same ErrorInfo detail
* fix(sse): extract Vertex error classifier and rebaseline frozen file sizes
* test: register vertex-passthrough-model-lockout in stryker tap.testFiles
* fix(sse): reconciles rebase-onto-tip drift for 9006
Two categories of inherited base-branch breakage surfaced when
rebasing onto release/v3.8.50's latest tip, both confirmed unrelated
to this PR's own diff:
- check:file-size: base.ts and chat.ts drifted further past their
frozen caps via already-merged commits (
|
||
|
|
ca6e944bb1 |
fix(antigravity): propagate switchAuth signal from 429 engine to retry guard (#9351)
When Google returns a 429 with no parseable retry hint, decide429 correctly classifies it as short_cooldown_switch_auth (switch accounts). But the executor discarded that decision, keeping only retryMs=60000. The retry guard then slept 60s against the same URL/account up to 3 times because 60000 <= LONG_RETRY_THRESHOLD_MS (inclusive boundary). Plumb a switchAuth boolean through tryResolveRetryFromErrorBody so the retry guard can decline the sleep branch and fall through to URL/account fallback immediately. Signed-off-by: Minxi Hou <houminxi@gmail.com> |
||
|
|
36959c37d3 |
[v3.8.50] fix(models): keep model catalogs responsive (#9199)
* fix(models): preserve catalog on affinity bookkeeping Related to #8697. Focused follow-up to #8728; this does not replace or supersede that contribution. * docs(changelog): record model catalog affinity fix * fix(models): keep cold catalog builds responsive * docs(changelog): record catalog responsiveness fix * fix(models): snapshot auto candidate capabilities * fix(models): invalidate capability catalog snapshots * test(models): register catalog invalidation coverage * fix(models): bulk-load catalog capability snapshots Resolve synced capabilities and persisted overrides from one build-local view instead of repeating per-target SQLite reads. Keep ordinary runtime lookups on demand and preserve catalog generation invalidation. Refs: #9199 * fix(models): snapshot catalog pricing once per build Production profiling showed per-model models.dev pricing reads and JSON parsing dominated cold catalog builds. Reuse one build-local pricing snapshot during enrichment and yield before publication so queued health checks can run, while preserving fresh reads for ordinary callers. * docs(changelog): record catalog pricing snapshot |
||
|
|
2b2d947faf |
fix(cache): add latency marker + per-key bypass for semantic cache (#8984)
* fix(cache): add latency marker + per-key bypass for semantic cache
Semantic cache silently corrupts latency measurements: a 10s upstream
call served from cache looks like 19ms. Three fixes:
A. Latency marker: cache HIT responses now carry
X-OmniRoute-Cache-Latency: synthetic so measurement tools can
distinguish real vs cached latency.
B. Per-key bypass: new apiKeys.cacheDefaultMode ('legacy' | 'bypass')
lets latency-sensitive clients opt out of cache reads entirely.
- DB column + migration (134)
- rowParser parseCacheDefaultMode
- API create default + PATCH update
- checkSemanticCache returns null on bypass
C. Type safety: ApiKeyRow/ApiKeyView/params updated, superRefine
guard includes cacheDefaultMode.
Cache write path intentionally unchanged: apiKeyId is already in the
cache signature (semanticCache.ts:140), so per-key isolation prevents
cross-key pollution.
Changed test files:
- tests/unit/chatcore-semantic-cache.test.ts (3 new tests)
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* docs: document semantic cache latency impact + bypass configuration
---------
Signed-off-by: Minxi Hou <houminxi@gmail.com>
|
||
|
|
16b67f5f68 | fix(dashboard): make quota providers expandable (#9025) | ||
|
|
a99c795a67 |
Add native ChatGPT Web provider for Codex clients (#8949)
* Bypass proxy compaction for native Codex context
* Add native ChatGPT Web provider pipeline
* Add managed browser and tunnel deployment
* Add ChatGPT Web setup and doctor UI
* Document and test ChatGPT Web integration
* fix(security): register chatgpt-web-codex-doctor in LOCAL_ONLY_API_PATTERNS
The diagnostic route under /api/providers/{id}/chatgpt-web-codex-doctor
was not registered in the spawn-capable route guard. Adding it for
parity with the existing /login pattern.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): route chatgpt-web-codex admin routes through a service boundary
The provider CRUD/doctor routes imported chatgpt-web-codex helpers
(finalizeValidatedChatGptWebCodexSecrets, encode/decodeChatGptWebCodexSecrets,
getChatGptWebCodexDoctorStatus) directly from open-sse/executors/**, which
no-restricted-imports (EXECUTOR_IMPORT_RESTRICTION) forbids for src/app/**
files — executor implementations must stay behind an open-sse handler or
service boundary.
Add open-sse/services/chatgptWebCodexAdmin.ts as a thin re-export boundary
(mirroring the existing tokenRefresh.ts re-export pattern) and import from
there instead. No behavior change.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
1d33025c70 |
fix(translator): restore TitleCase tool names on the Claude to Gemini path (#9993)
Gemini lowercases tool names in functionCall responses, so the request translator must publish a lowercase alias (read -> Read) for gemini-to-claude to restore the casing Claude Code registered. claude-to-gemini.ts filtered identity entries (Read -> Read) out of _toolNameMap, so no alias reached the response translator and normalizeToolName() - whose REVERSE_MAP is keyed by TitleCase - left the lowercase name untouched, surfacing as 'No such tool available: read'. Reuse buildChangedToolNameMap(), which #9568 already introduced for the openai-to-gemini path. Closes #9713 Co-authored-by: Marcos Jr <engenheiromarcosjr@gmail.com> |
||
|
|
d774ccecac | fix(routing): account for active OAuth sessions (#8940) | ||
|
|
84e83e2f19 |
feat(providers): add DeepSeek V4 thinking effort aliases (#9485)
* feat(providers): add DeepSeek V4 thinking effort aliases * docs(changelog): add DeepSeek effort alias entry * fix(catalog): scope effort-tier fallback to declared models and harden resolver Addresses reviewer findings on #9485: - CRITICAL #1: catalog no longer synthesizes unresolvable effort aliases for static reasoning models without declared tiers (cheaperinference, cline, etc.) - CRITICAL #2: tiered static models survive synced-coverage suppression so normal installs with synced DeepSeek base models still expose aliases - WARNING #3: registry suffix resolution short-circuits when the raw id matches a direct custom or synced model, preserving custom apiFormat/targetFormat - WARNING #4: empty synced effort array no longer erases the registry fallback - WARNING #5: isFlash check is robust to suffixed/prefixed model ids - Added regression tests for blast radius, custom-model shadowing, none-path, and suffixed isFlash * fix(combos): expose static registry effort tiers in Combo Builder (#9485) Static provider registry models (e.g. DeepSeek V4 Flash/Pro) declare supportedThinkingEfforts, but buildModelOptions() only ran appendSyncedEffortVariants() over DB-synced rows. Synced metadata for a DeepSeek connection can omit supportedThinkingEfforts, so the catalog/ Playground surfaced the declared aliases while the Combo Builder picker showed only the bare base ids. Feed builtInModels with declared effort tiers through the same appendSyncedEffortVariants() utility used for synced rows, inheriting the base entry's contextLength/outputTokenLimit/supportedEndpoints/ supportsThinking and preserving its source. DeepSeek is not skipped by shouldExposeSyncedEffortVariants(), so Flash (none/low/high/max) and Pro (none/high/max) aliases now appear in the Combo Builder for any connection whose synced rows omit effort metadata. Regression test seeds a DeepSeek connection with effort-less synced rows and asserts the exact alias sets, source preservation, and metadata inheritance. |
||
|
|
45b997765c |
Document default behavior for ToS-flagged free-tier providers (addresses #10004) (#10013)
Co-authored-by: yulinlina <yulinlina@users.noreply.github.com> |
||
|
|
a9d6fd3d9a |
feat(api): add per-key prompt compression bypass (#10001)
* feat(api): add per-key compression bypass * docs(changelog): note per-key compression bypass * chore(db): renumber API key compression migration * fix(compression): preserve hard kill during adaptive planning * chore(db): refresh migration gap allowlist |
||
|
|
1113e72a74 |
chore(repo): ignore Electron build output unpacked into repo root (#9770)
* chore(repo): ignore Electron build output unpacked into repo root
electron-builder (squirrel-windows target) unpacks the packaged app -- the
entire Chromium runtime, ~24k files -- directly into the repository root:
OmniRoute.exe, chrome_*.pak, *.dll, locales/, resources/, icudtl.dat,
snapshot blobs and the Chromium license files.
None of it was covered by .gitignore, so `git add -A` would commit the whole
runtime. Every rule is root-anchored (leading `/`) because a bare `locales/`
or `resources/` would also swallow tracked sources -- notably the CLI
translations in bin/cli/locales/*.json.
Verified with `git check-ignore`: all artifact paths ignored, and
bin/cli/locales/{en,de}.json remain tracked.
* chore(electron): sync package-lock for windows installer deps
Adds the lockfile entries for the Windows installer/signing toolchain that
the electron build now pulls in: electron-builder-squirrel-windows,
electron-winstaller and @electron/windows-sign (plus their transitive
fs-extra/jsonfile/universalify/mkdirp pins), and bumps app-builder-lib and
builder-util-runtime.
Lockfile-only change; no source or runtime behaviour is affected.
|
||
|
|
aa4e72097a |
fix(bun): make server child and outbound fetch Bun-safe (#9761)
* chore(changelog): v3.8.49 reconciliation — 200 missing bullets + 22 restored credits Phase 0a of /generate-release. Measured commit<->CHANGELOG coverage over the real cycle range (2c62333b0..HEAD, 933 non-merge commits) instead of the last tag: 180 merged PRs had no bullet at all (they landed without a changelog.d fragment) and a further 19 were invisible because the merge-train landed them under a generic 'Train 1D: merge via --admin' subject that carries no PR reference. - +200 bullets, all with PR back-reference and author attribution (1179 -> 1379) - 🙌 Contributors 156 -> 178; credits @terrafirmbot-source for #7904, which shipped through the conflict-resolved #8685 without any attribution - closed-PR credit audit over the 32 human PRs closed unmerged this cycle: 12 had already landed under the author's own follow-up PR and were verified credited - rollup bullet for the direct release-branch maintenance (merge-train landings, ratchet re-pins, base-red sweeps) that carries no PR of its own - [3.8.49] header dated 2026-07-28 (was TBD) in the root file and the 42 i18n mirrors Coverage after: 0 commits uncovered. * chore(quality): v3.8.49 pre-flight — clear 4 base-reds, absorb cycle drift Pre-flight sweep (Phase 0). Test suites ran on the dedicated 32-core box so the self-inflicted load of `node --test` could not fabricate timing flakes. Base-reds fixed (all real, all from merged cycle PRs that did not update their characterization tests): - providers-constants-split / quota-plan-registry / provider-translate-path GOLDEN: #8861 added the Xiaomi MiMo Token Plan provider, so APIKEY_PROVIDERS is 195 (was 194), knownProviders() is 12 (was 11) and the translate-path snapshot gains one purely additive entry. Counts aligned to the shipped catalog, never relaxed. - agent-skills-content: skills/config-codex-cli/ was added by #8709 with a custom block, so the custom-block set is 13, not 12. - chatcore-compression-integration: #8595/#8560 deliberately decoupled REACTIVE context compaction from the `enabled` master switch, so a body above 70% of the window is pruned even with compression off. The test was sized above that threshold, which made it assert against intended behavior; it now stays below it and keeps testing the invariant it was written for (resolveBasePlan short-circuits to "off" before reading comboOverrides). Static gates: - 3 shellcheck directives were malformed (`# shellcheck disable=SC2086 — text`; the em-dash makes shellcheck reject the whole directive as SC1125) in ci.yml and nightly-release-green.yml — the comment now sits on its own line. - gitleaks: 2 new generic-api-key false positives allowlisted with justification — a localStorage key for the sponsor banner (#8723) and the PUBLIC Adobe Firefly web x-api-key, whose only literals are in JSDoc (the runtime reads it through resolvePublicCred, per Hard Rule #11). secretFindings back to 0. - zizmor 176 -> 189 and bundleSize 6762 -> 7666 rebaselined with the measurement and the reason; both are ordinary cycle drift absorbed at release. Environment-dependent failures classified out, not silenced: the two tproxy tests assert the native addon is unavailable/unprivileged and therefore fail when the suite runs as root on the build box (they pass as a normal user), and the consoleInterceptor rate-limit test is a 4s-timing flake under load (6/6 isolated). * test(codex): align the Responses HTTP e2e to the #8507 input-item contract Fifth and last base-red of the v3.8.49 pre-flight. #8507 (#8083) deliberately sets `status: "completed"` on Responses input items so strict upstream validators accept them; codex-chat-reasoning-http-e2e still asserted the pre-#8507 shape, so it failed against intended behavior. Expectation updated with the reason inline — the assertion is not relaxed, it now pins the current contract. The test was never reached in the first pre-flight sweep (the run was interrupted during the integration phase, and this file sorts after the one that failed). * docs(release): v3.8.49 feature-documentation sync Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite) was already covered. Six real gaps were left undocumented by the PRs that shipped them, each verified in source before being written up: - CredentialMaskerGuardrail (#7683) is registered in guardrails/registry.ts but the GUARDRAILS table listed only 3 of the 4 guardrails - the cacheAffinity scoring factor and the cache-optimized combo strategy (#8008): the docs still said 12 factors / 18 strategies, the code has 13 / 19 - the optional dashboard OIDC login gate (#6973) — /api/auth/oidc/{login,callback} had no mention in AUTHZ_GUIDE - GET /api/usage/cache-health (#8827) and GET /api/usage/model-latency-stats (#6873) were missing from the API reference README "What's New" gains one bullet (routing transparency) and merges two others rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the generator (Firecrawl reclassified to Search, Xiaomi MiMo added by #8861). check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI references. Known pre-existing drift left alone and reported: stale nominal counts in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this environment does not have — the .mmd source is updated and the .md says so). * chore(release): v3.8.49 — clear the release-PR CI in one pass Every finding from the first full ci.yml run on the release PR, fixed or justified together so a single re-push clears the board. Lint / check:route-validation:t06 — three routes read request.json() with no visible Zod validation. The two proxy-subscriptions routes validated with a hand-rolled parsePayload(); they now use real Zod schemas (src/lib/proxySubscription/schema.ts) reproducing the same acceptance rules, error strings and status codes. chat/completions is the proxy's hottest path and parses the body ONCE on purpose (#4380 OOM crash-loop), so it now safeParses the ALREADY-PARSED object against a deliberately permissive structural schema — proven not to change behavior: absent model and model:null still pass through, role "developer" still reaches 200, a ~300 KB payload is accepted, and the body is still read exactly once. 25 new tests. i18n UI value drift — 13 English strings rewritten during the cycle left stale translations in up to 41 locales (317 pairs). Eleven are genuine rewrites and now carry the pipeline's __MISSING__:<english> marker so the runtime serves corrected English until translation catches up; vi forbids that marker by test, so it got a real translation. PR Test Policy — 33 files flagged. Each was verified against the SOURCE, not the diff: 26 assert reductions are legitimate (mostly the #7866 Qwen OAuth provider removal and the #8013 Antigravity refactor deleting the surface under test) and are allowlisted with the PR and the evidence; 5 deleted files have verified replacements. One was NOT legitimate: #7528's GraphQL->WebSocket migration dropped four muse-spark continuation scenarios whose logic is still live — connection isolation, cache eviction after a failed turn (the commit itself says "was missing"), parallel-chat cache collision, and the empty-content guard. All four are restored against the new transport and each was verified to fail when the corresponding production mechanism is broken. Quality Ratchet / openapiCoverage — 36.6% against a baseline of 38: the cycle added routes faster than the spec. Eight real endpoints are now documented from their route.ts (usage cache-health and model-latency-stats, the two OIDC endpoints, and the five proxy-subscriptions paths), bringing it to 38.1%. Quality Gates (Extended) / zizmor — the runner measures 190 where the devbox measures 189 on the same commit, a delta already recorded in this baseline's history. Baselined to the runner's number. Also: the driverFactory better-sqlite3 guard moved from a mid-body t.skip() to a declared { skip: <condition> } test option. Same behavior for the optional native dependency, but the skip now shows up in the report and is distinguishable from a test.skip() that silences a test outright. Verified under both runners: 15/15 on Node, 14/14 on Bun. SonarCloud Code Analysis stays red and is not a blocker: sonar.qualitygate.wait=false since #7038 makes the job informative, the built-in gate cannot be swapped on the FREE plan, and main has no branch protection. * chore(quality): close the last two release-PR reds test-masking — I had missed one of the 34 flagged files: my first pass grepped only paths under tests/, so open-sse/services/__tests__/tierResolver.test.ts was invisible. Same #7866 cause as the other eight qwen-driven reductions: the "classifies Qwen as free" case and qwen's entry in the batch list went with the removed provider, and the batch indices dropped from 10 to 9 (61→59). Allowlisted with that evidence. dast-smoke — all four Schemathesis findings are on the two OIDC endpoints documented in the previous commit, and none is a defect. /api/auth/oidc/* is a BROWSER redirect flow: it answers 302 to the IdP and 302 back to /login?oidc_error=... on every failure, which Schemathesis reads as "accepted a schema-violating request", and it answers 400 when OIDC is not configured, which it reads as "rejected a schema-compliant request". Keeping the endpoints in the spec is right — operators need them, and they are what brought openapi coverage back over the baseline — so the flow is excluded from the fuzz instead, with the reason inline in the workflow. The rest of /api/auth and /api/keys stays in scope. * test(db): reword the driverFactory skip comment so the gate stops counting it The anti-test-masking gate greps text, not code: my explanation of WHY the better-sqlite3 guard moved out of the test body spelled the runner API out literally, and those two mentions inside a comment were counted as two new skip markers — the exact signal the previous commit set out to clear. Same explanation, phrased without the call syntax. Verified with the gate's own exported helpers against the merge-base: 0 modified-file violations, 0 deletion violations. Test still 15/15. * fix(dashboard): unbreak the vitest:ui gate — 2 real production bugs + the i18n test seam The Vitest job is a BLOCKING gate that had not run to completion once in this whole release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push, so its red was indistinguishable from green. Round 4 finally ran it and the suite was broken cycle-wide. Root cause of the suite: #7935 instrumented ~180 shared/dashboard components with next-intl's useTranslations/useLocale without updating the tests that mount them, so every one of them threw "context from NextIntlClientProvider was not found". Fixed at the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator built from the REAL en.json via next-intl's own createTranslator, memoized per namespace — the naive version returns a fresh function each call and any component whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a failure. A local mock still wins over the default. 22 files fixed by the seam alone, 15 realigned to the real strings; no assert removed or weakened. Two production bugs the suite was hiding, both pre-existing and both with a failing regression test already in the tree: - RequestLoggerDetail crashed on a structured error object. #7920 gave the component formatErrorForDisplay for exactly this case, then #8213's combo-503 / cooldown checks went to the raw field and called .toLowerCase() on it. Both paths now use the helper. - The logs detail modal reopened on first close again. #6830 fixed that by reading the deep-link id ONCE; the #8354 page rewrite regressed it by reading the live searchParams every render, so the prop flips mid-session and re-fires the child's deep-link effect exactly as the modal closes. Frozen at mount again. Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under --require-tighten: the metric genuinely improved as the async translation workflow paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The collector subtracts placeholders, so this release's 317 __MISSING__ markers are already netted out of the 99. Two UI files still fail locally under 20-worker concurrency (combos-page-smoke, evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout. * test(e2e): repair the four shards the first green Build finally exercised test-e2e has `needs: [build]`, and the release PR's Build died on every round until now — so the 9-shard matrix produced ZERO signal for this whole cycle while ~200 PRs merged. The first successful Build surfaced four independent breakages, each traced to the commit that caused it: - providers-management (#7361): the single-connection delete moved from window.confirm() to a ConfirmModal, so page.once("dialog") never fired and the DELETE was never sent (deleteCalls stayed 0). Click the modal instead. - providers-bailian-coding-plan (#7882): the free-text Base URL field was deliberately replaced by a region step whose choice resolves the endpoint (global-sg -> coding-intl.dashscope, china-beijing -> coding.dashscope). Both cases rewritten against the region step; the invalid-URL case is unreachable from this modal now, so it covers the CN choice instead. - group-b-activity-feed: the stack-trace guard ran against page.content(), which embeds the serialized i18n payload — zenmux's "endpoint at /api/v1/chat/completions" is prose, not a leak. Assert on rendered innerText and require the :line:col every real stack frame carries. - navigation (#8292): APP_ROUTE_PATTERN accepted only /login and /dashboard, but the new prefetch spec is the sole caller passing /home, so waitForURL never resolved and the retry loop burned the full 180s timeout. E2E is green on main (9/9 on 07-22 and 07-23), so all four are cycle regressions, not pre-existing debt. Tests only — no production code touched. * fix(dashboard): stop the /home quick-start cards from prefetching too #8292 fixed half the RSC prefetch storm: it added prefetch={false} to the sidebar's navigation and logo links, but /home — the landing route, and the one its own e2e guard visits — renders five more internal Links in the quick-start cards. First paint still fired 12 speculative RSC requests for /dashboard/{analytics,logs,providers,api-manager} and /docs. That PR shipped the test that would have caught this, but the test never got to its assertion: gotoDashboardRoute("/home") hung because APP_ROUTE_PATTERN accepted only /login and /dashboard, so the retry loop burned the whole 180s timeout with no assertion error. With that helper repaired in the previous commit, navigation.spec.ts finally ran and reported the 12 requests. Validated both ways, per Hard Rule #18: - tests/unit/sidebar-prefetch-policy-8281.test.ts extended to /home — red on the parent commit (5 internal Links, 5 without prefetch={false}), green here. - the e2e assertion expect(speculativeRequests).toEqual([]) is the end-to-end guard; it is what surfaced the defect in the first place. * refactor(dashboard): shrink HomePageClient back under the size gate The prefetch fix in the parent commit tripped check:file-size — the frozen budget for this file is 1377 lines and a naive fix measured 1391, because `href` + `prefetch={false}` + `className` no longer fits Prettier's 100-column budget, so three one-line <Link> elements each expanded to five. Followed the gate's own first suggestion (extract/DRY) before touching the baseline: the quick-start links repeated the same className literal four times, and the docs link carried a 180-char one inline. Hoisting both into INLINE_LINK / DOCS_LINK collapses five wrapped <Link> blocks back to a single line each and removes the duplication — 1391 -> 1381. The remaining +4 over the frozen budget is the five prefetch attributes themselves, which cannot be expressed in fewer lines. Rebaselined to 1381 with the rationale recorded in file-size-baseline.json under _rebaseline_2026_07_29_8281_home_quickstart_prefetch. tests/unit/sidebar-prefetch-policy-8281.test.ts still passes (2/2): it matches whole <Link ...> blocks, so it is indifferent to the wrapping and only checks that every internal link opts out of prefetch. * fix(bun): use native fetch for direct outbound requests * test(bun): cover native direct fetch path * fix(bun): preload polyfill for next build workers * fix(bun): expose AsyncLocalStorage globally * fix(bun): filter non-page Fumadocs metadata * fix(bun): defer docs-only route dependencies * chore(skills): sync generated OmniRoute agent skill docs --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
350eb8f36f |
fix(perf): memoize synced pricing reads (#9746)
Co-authored-by: chloeassistant <279834366+chloeassistant@users.noreply.github.com> |
||
|
|
acae259967 |
fix(executors): repair DuckDuckGo AI Chat challenge solver (418 ERR_CHALLENGE) (#9733)
Every duckduckgo-web chat request failed with HTTP 418 ERR_CHALLENGE while
duck.ai worked normally in a browser from the same IP. Ground truth was
established by driving a real headful Chromium at duck.ai from that IP (it
returned 200), so the environment was never the problem — the anti-abuse
challenge solver was. Six independent defects were found; the first alone
disabled the solver completely.
1. Module syntax inside the vm sandbox source.
CHALLENGE_STUBS is executed with vm.runInContext, which compiles in SCRIPT
mode. A refactor mass-added `export` to the five `function` declarations
inside that template literal (they read as ordinary top-level TS functions),
so every solve threw SyntaxError. The executor swallows solve failures and
posts the raw unsolved challenge, which upstream answers with 418.
2. Double-escaped regex in a String.raw template.
`\\s` in __parseCssDisplay reached the sandbox as a literal backslash, so the
display regex never matched and a getComputedStyle probe silently read empty.
3. buildHtmlLookup undercounted descendants by one.
`count` backs el.querySelectorAll('*').length; that returns DESCENDANTS and
countHtmlElements already skips the #document-fragment root, so the `- 1` was
wrong. Chromium reports 3 for '<li><div></li><li></div'; we reported 2, and a
variant multiplies innerHTML.length by that count.
4. Browser-fidelity probes.
Newer challenge variants assert JS/DOM invariants a flat stub cannot satisfy:
real prototype chains (HTMLDivElement -> HTMLElement -> Element), NodeList
identity, a live body.children HTMLCollection, native-code toString, and
sloppy-mode `this === window`. Nine of thirteen failed. Notably Math must NOT
be sealed — Chromium reports Object.isSealed(Math) === false, and sealing it
made our vector differ by one.
5. The solved payload dropped meta.origin / meta.stack / meta.duration.
The duck.ai bundle always sends all three; captured browser requests confirm
it. Without them upstream returns 418 even when every client_hash is correct.
6. reasoningEffort is now mandatory on duckchat/v1/chat.
An otherwise byte-identical payload returns 200 with the field and 400
ERR_BAD_REQUEST without it (A/B verified live, repeated).
Also removes the throwaway "seed" chat POST that ran before every real request.
It existed to coax a usable challenge out of the upstream while the solver was
broken; it only doubled chat calls against an IP-rate-limited endpoint, showing
up as spurious 429 ERR_RATE_LIMIT.
Verification: the solver now reproduces real Chromium's probe vectors exactly
for all 8 captured challenge variants, and the executor returns 200 end-to-end
live (non-streaming, streaming, claude-haiku-4-5, and a math prompt returning
"42").
Tests: tests/unit/duckduckgo-challenge-solver-regression.test.ts (32 tests) and
tests/unit/duckduckgo-reasoning-effort-required.test.ts (5 tests), backed by
tests/fixtures/duckduckgo/challenge-variants.json — real captured challenge
programs plus the probe vectors a real browser produced for them, so the suite
asserts against recorded browser behaviour rather than our own output. Each fix
was confirmed to fail its test when individually reverted.
|
||
|
|
18e89e6fb0 |
fix(compression): persist RTK renderer configuration (#9730)
* fix(compression): persist RTK renderer configuration * docs(changelog): add fragment for #9730 Adds the changelog.d/fixes/9730-persist-rtk-renderers.md fragment required by check:changelog-integrity for the RTK enableRenderers persistence fix in PR #9730. --------- Co-authored-by: Isaac <isaaclyons98@gmail.com> |
||
|
|
f5ce51a9ff |
feat(providers): add Conol (conol.ai) web session provider (#8974)
* feat(providers): add Conol web support
* fix(conol): preserve sessions and image turns
* fix(conol): pin session model and effort via /model endpoint
Conol ignores agentModel/agentEffort on POST /api/sessions, so every
session silently ran on the downgraded account default (the create
response reports modelDowngraded: true / effectiveModel).
Sessions are now created empty and configured out-of-band against
POST /api/sessions/{id}/model before the first turn is submitted, in the
order the web client uses: modelPreset, then agentModel, then agentEffort.
The ordering is load-bearing because the model call resets agentEffort to
null server-side.
Effort now defaults to xhigh when the caller does not pin one via the
-<effort> model suffix, and is clamped onto the ladder each model actually
advertises, so xhigh degrades to high on claude-sonnet-5 and is skipped
entirely for models without an effort ladder such as openrouter/fusion.
Model and effort are also dropped from the session binding key so switching
models re-pins the existing session instead of stranding it and losing the
conversation history. Re-pinning only happens on an actual change, so
steady-state follow-ups cost no extra round trips.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
55c2b35eb7 |
fix(codebuddy-cn): replace agent system prompts to bypass Tencent content filter (#9723)
* fix(codebuddy-cn): replace agent system prompts to bypass Tencent content filter Tencent's content filter flags CLI agent system prompts (e.g. 'You are Claude Code, Anthropic's official CLI...') as prompt injection / sensitive content and rejects the entire request with error: 抱歉,系统检测到您当前输入的信息存在敏感内容,我无法响应您的请求 This patch adds detection and replacement logic to the CodeBuddyCnExecutor: - Regex-based identity marker detection (Claude Code, Cursor, Windsurf, Cline, Aider, Copilot, Cody, etc.) + length catch-all (>2000 chars) - Handles both top-level 'system' field (Anthropic format) and messages array with role:'system' (OpenAI format) - Preserves original content shape (string vs typed content blocks) - Strips oversized tool descriptions (>64KB) that can also trigger the filter - Replaces with neutral prompt, leaving legitimate user prompts untouched Based on approach from rafilajhh/9router commit 7f7d7ce. * test(codebuddy-cn): add regression coverage for system prompt replacement Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
de33b2cee3 |
feat(resilience): expose providerQuotaOverrides via /api/resilience (#9714)
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com> |
||
|
|
7cee5020bc |
fix(docker): make the webpack build-arg escape hatch actually work (#9695)
* build(docker): make the bundler build-arg actually take effect A bare ENV shadows a same-named ARG for the rest of the stage, so --build-arg OMNIROUTE_USE_TURBOPACK=0 was silently ignored and the webpack escape hatch the surrounding comment advertises only ever worked through -e at runtime, never at build time. That mattered because Turbopack compiles in native Rust memory living outside the V8 heap, so OMNIROUTE_BUILD_MEMORY_MB cannot bound it. A build host with a memory ceiling gets SIGKILLed by the cgroup OOM killer with no error text at all, which reads like a hung build rather than an out-of-memory one. * docs(docker): correct the builder stage facts and document its cost The stage table described a builder that no longer exists: it named node:24.15.0-trixie-slim where every stage now derives from node:26-trixie-slim, and said the stage runs `npm run build -- --webpack` where it runs plain `npm run build`, which is Turbopack by default. That second one is worse than stale. A reader who needs the webpack fallback would conclude the Docker build already uses it and never look for the switch. Adds a Build-time resources section covering the two build args, why the V8 heap arg cannot bound Turbopack, and measured ceilings for both bundlers. The runtime paragraphs that followed get their own heading so they no longer read as part of the build-time story. * docs(docker): correct the runtime heap defaults Same drift as the builder stage, in the paragraphs just below it. The image exports OMNIROUTE_MEMORY_MB=1024 and derives NODE_OPTIONS from it, but the guide reported 512 in three places, including the environment variable table. The "if unset, the launcher uses 512" line was misleading in both readings: the image always sets the variable so that branch cannot fire under Docker, and outside Docker the launcher calibrates from host RAM rather than using a flat 512. * docs(changelog): add fragment for #9695 |
||
|
|
76c2f56ca9 |
fix(web-tools): anchor tool contract at prompt tail + user-turn reminder for large prompts (#9693)
* fix(web-tools): anchor tool contract at prompt tail + user-turn reminder The <tool> contract from prepareToolMessages was prepended as the first system message. Web executors fold all system messages into one block, so with agentic clients whose system prompts exceed ~28K chars the contract sat at the head of a huge block and web models ignored it, refusing tool calls with "tool X is not in my tool set" (chatgpt-web, 0/3 at 30K chars). Two changes, both required in testing: - Dual placement: the full contract now rides as a trailing system message (folds to the tail of the system block) and a one-line reminder naming the tools is appended to the latest user message. - Rewording: the contract now frames injected tools as client tools invoked via a plain-text protocol, distinct from the model's native tool registry (web.run, python.exec, ...), and instructs the model to never claim they are unavailable. Without this the model resolved tool names against its native registry and refused even when it had seen the contract. Measured on cgpt-web gpt-5.5-thinking/gpt-5.6-thinking/o3: prepend 0/3 tool calls at 30K chars; dual placement 16/17 across 30K-250K system prompts, 30-tool sets, multi-turn tool history, streaming, and 3-way concurrency, with no spurious calls on no-tool prompts. Known limit: ~40K-char single user messages still flake (2/3) due to the upstream model's own injection heuristics. All prepareToolMessages consumers parse system messages position-independently and select the current user turn by role scan, so the trailing system message is shape-safe for every web executor. * test(web-tools): cover contract placement edge cases --------- Co-authored-by: Ryan Brosas <ryanjoserbrosas@gmail.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
d6543d71ae | fix(usage): reject impossible provider token counts (#8927) | ||
|
|
c7be8a4870 |
fix(providers): drop dead Cloudflare Workers AI free catalog IDs (#8717) (#8804)
Four of the original six free-catalog model IDs return 400/403/410 from Workers AI. Remove them from freeModelCatalog + cloudflare-ai registry, keep the live replacements from #8763, and move the 30M monthlyTokens budget onto @cf/meta/llama-3.3-70b-instruct-fp8-fast. Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com> |
||
|
|
549739a9b7 | fix(kimi): apply K3 effort policy to aliases (#10005) | ||
|
|
e3675916c9 | docs: fix duplicated word in MCP server audit logging section (#10000) | ||
|
|
1423259fec | fix(opencode): fallback unsupported DeepSeek json schema output (#9992) | ||
|
|
2b5253da79 | fix(types): narrow Claude stream deltas (#9990) | ||
|
|
33d58ed420 | fix(types): validate Fal video result URLs (#9989) | ||
|
|
861011c02b | fix(types): preserve GHE Copilot executor configuration (#9988) | ||
|
|
425396d11d | fix(types): narrow chatCore local contracts (#9987) | ||
|
|
390efaafaf | fix(types): narrow chat dispatch contracts (#9986) | ||
|
|
16c566d146 | fix(copilot-web): restore browser authentication (#9984) | ||
|
|
3e2b166869 |
feat(combo): add quota-only priority fallback (#9983)
Add a per-target priority option that advances only after trusted quota exhaustion while preserving retry, nested Combo, quality, and Global Fallback semantics. |
||
|
|
82115193c2 |
fix(media): support Gemini Omni Flash video (#9982)
* feat(media): add provider-neutral video and music generation * fix(db): clean audit tables by created timestamp * fix(media): support Fal-hosted Grok video * fix(media): route Fal video references to Grok * fix(media): support Gemini Omni Flash video * fix(media): use Gemini Omni Flash Fal endpoint --------- Co-authored-by: rinseaid <rinseaid@rinseaid.net> |
||
|
|
16bf95fe33 |
fix(executors): strip redundant oneOf matching sibling enum (#9828)
* fix(executors): strip redundant oneOf matching sibling enum
The Codex private Responses endpoint intermittently returns a 502 upstream_empty_response for tool parameters that combine oneOf:[{const,...}] with a sibling enum containing the same value set.
When the const and enum sets match exactly, oneOf adds no constraint beyond enum. Add stripRedundantOneOfConstEnum to normalizeCodexTools to remove only this semantically redundant form.
The schema-aware recursive walker requires non-empty, unique string const branches containing annotations only, string enum values, and an exact set match. It preserves bare oneOf[const], narrowing or non-matching sets, type-discriminated oneOf, empty oneOf, non-string values, and anyOf/allOf.
Run the normalization after stripUnsupportedRegexPatterns and before assigning tool.parameters. Add focused regression coverage for matching, non-matching, nested, immutable, and Chat-to-Responses cases.
* docs(changelog): update PR number in changelog fragment
|
||
|
|
029a43359c |
fix(command-code): include tool call arguments (#9821)
* fix(command-code): normalize malformed tool call arguments and fix test assertion handling
* fix(command-code): resolve toolName from assistant calls and update version header to 1.15.1
* refactor(command-code): consolidate pre-pass message tool metadata extraction and add unknown fallback test
* fix(command-code): fallback unnamed tool calls to unknown to satisfy upstream name validation
* fix(db): rename 139_job_registry -> 143 to avoid collision with 139_ccr_blocks
release/v3.8.50 owns version 139 (ccr_blocks, #9061). The #9631 job
registry cherry-pick (
|
||
|
|
f8c8bae75c | fix(ci): repair release lint test regressions (#9813) | ||
|
|
43da333970 |
docs(proposals): Telegram Mini App integration feasibility analysis (#9810)
Assess adding a Telegram Mini App chat surface to OmniRoute. Verifies
against current main (
|
||
|
|
258fde411a |
fix(response): strip internal reasoning placeholder from all reasoning fields (#9790)
copyOpenAICompatibleReasoningFields only stripped the sentinel (NON_ANTHROPIC_THINKING_PLACEHOLDER = "(prior reasoning summary unavailable)") from reasoning_content and reasoning. Non-standard reasoning fields (reasoning_text, thinking, thought) and reasoning_details items passed through raw, leaking the internal replay sentinel to clients on providers that use those fields (e.g. Venice), where the model echo surfaces as a bogus thought block and can degrade into empty turns. Strip the sentinel from every forwarded reasoning field, including per-item text/content inside reasoning_details; drop items/fields that strip to nothing while preserving non-text details such as reasoning.encrypted. Fixes #9765 Refs #8081, #9606 |
||
|
|
ce9b1e4641 | fix(api): enforce model permissions on gateway mirrors (#9788) |