* fix(command-code): preserve literal max effort for command-code provider
* test(command-code): type the new sanitizeReasoningEffortForProvider assertions
The 3 new command-code reasoning-effort test cases cast the function's
unknown return value with `as any`, which pushes the file's frozen
no-explicit-any suppression count (48) to 51 and trips the "No new
ESLint warnings" gate. Use a minimal EffortCarrierResult shape instead
of any, matching the fields the assertions actually read.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* test(v1-models): type the API key lookup in the #9320 auth-leak regression test
The release-tip test file added by #9320 used `(k: any)` in an Array.find
callback, which is not covered by config/quality/eslint-suppressions.json
(the file was added after the suppressions snapshot was frozen). That
leaves the "No new ESLint warnings" gate red for any branch that merges
this exact release/v3.8.50 tip, unrelated to this PR's own diff. Fixing
it here with a minimal derived type (Awaited<ReturnType<typeof
getApiKeys>>[number]) unblocks the gate without touching the frozen
suppressions baseline.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* refactor(cursor): extracts token extraction into shared lib
Moves tryIdeAuth/tryAgentAuth and supporting helpers out of the
auto-import route into src/lib/cursor/tokenExtractor.ts, and adds
an agent-cli-state.json fallback candidate path to tryAgentAuth
(alongside the existing auth.json candidate) so the extraction
logic can be reused by the upcoming renewal orchestrator.
* feat(cursor): adds cursor-agent-backed token renewal orchestrator
Builds the renewal orchestrator in src/lib/cursor/renewal.ts: a
bounded, unattended-safe --list-models nudge, a side-effect-free
status availability check, an in-flight spawn lock keyed by
command, and renewCursorConnection() which nudges cursor-agent
then independently re-scrapes the IDE and cursor-agent credential
sources to detect whichever refreshed. Extends cursorAgent.ts's
binary resolution and spawn helper with fixed-paths-only mode and
a SIGKILL follow-up for background use. Adds a generic keyed-mutex
utility (src/shared/utils/keyedMutex.ts) for serializing a
connection's renew-then-persist cycle, and forwards a busy-timeout
through driverFactory's node:sqlite fallback path.
* feat(cursor): proactively renews Cursor sessions in the sweep
Adds src/lib/tokenHealthCheckCursor.ts, sweep-side glue that calls
the renewal orchestrator and persists the result, wired into
tokenHealthCheck.ts's checkConnection() via a new Cursor-specific
branch placed ahead of the generic no-refresh-token fallthrough.
Carves out a non-terminal exception for a Cursor connection that
already landed at testStatus "expired" via the request-time 401
path, excluding permanently-dead account_deactivated connections.
Extends buildRefreshFailureUpdate() with an overrides param so
Cursor's failure path can use a distinct, non-terminal errorCode
instead of the generic refresh_failed/expired taxonomy.
* feat(cursor): adds local-only manual refresh route
Adds POST /api/providers/[id]/refresh-cursor, a dedicated
loopback-only route that calls the renewal orchestrator on demand
for a single Cursor connection, bounded by a 30s per-connection
cooldown. Classifies the new route in LOCAL_ONLY_API_PATTERNS and
closes the manage-scope-bypass gap for dynamic-segment spawn-capable
routes under /api/providers/ via a new SPAWN_CAPABLE_PATTERNS /
SPAWN_CAPABLE_PATTERN_ANCESTORS mechanism, which also retroactively
covers the pre-existing /login route. The existing shared
/api/providers/[id]/refresh route is untouched and stays
remote-reachable for every other provider.
* feat(cursor): surfaces a dismissible cursor-agent nudge
Adds GET /api/providers/cursor/agent-availability, a credential-free
LOCAL_ONLY route returning only { cursorAgentAvailable: boolean },
backed by a 5-minute cached wrapper around the renewal orchestrator's
existing availability check. Surfaces a dismissible dashboard banner
on the Cursor provider page suggesting cursor-agent installation
when it isn't detected, following the existing dismissible-banner
convention. Also fixes a pre-existing bracket character in a
routeGuard.ts comment that was silently truncating
check-openapi-security-tiers.mjs's view of LOCAL_ONLY_API_PREFIXES.
* fix(cursor): wires manual refresh button to the new route
Branches handleRefreshToken to call the dedicated Cursor refresh
route instead of the generic /refresh route, which silently 502s
for Cursor connections today since they carry no refresh token.
Every other provider's refresh behavior is unaffected. Adds the
cursorSessionUnchanged i18n key and syncs it (plus a pre-existing,
unrelated 28-key backlog) across all 42 locale files.
* fix(cursor): addresses Phase 4/4.5 review findings
Restores the legacy stdout/stderr auth-pattern fallback in
checkCursorAgentAvailability() that the plan's Task 2 Step 4
required but the implementation had dropped. Threads an optional
deps parameter through checkCursorConnectionIfNeeded() so its
error branch is reachable in tests, and switches both it and the
manual-refresh route to exhaustive switch statements over the
renewal result. Adds a short-lived host-keyed dedup cache around
tryIdeAuth() so multiple due Cursor connections sharing a host
don't each open the same state.vscdb file in one sweep tick.
Adds opportunistic eviction to the manual-refresh cooldown map,
an outer try/catch to the availability route for defense-in-depth
consistency with the plan's other routes, and corrects a stale
JSDoc claim about the /login route's auth check. Documents the
now-empirically-confirmed agent-cli-state.json schema mismatch
found while validating against a real cursor-agent install.
* docs(cursor): adds changelog fragments for the renewal plan
Adds one fragment per user-facing outcome per changelog.d/README.md's
convention for a PR that both fixes and adds. PR number placeholder
to be filled in once the PR is opened.
* fix(i18n): translates the new Cursor keys into Vietnamese
The i18n:sync-ui run in an earlier commit left __MISSING__
sentinels for the 4 new Cursor keys in every locale, but
Vietnamese has a dedicated completeness test requiring zero
internal missing markers. Provides real translations for
cursorSessionUnchanged, cursorAgentNudgeTitle,
cursorAgentNudgeBody, and cursorAgentNudgeDismiss.
* fix(cursor): addresses quality-gate Layer 1.5 findings
Restores a comment that misrepresented execFile's actual argv shape
after an earlier bracket-removal fix, this time avoiding literal
closing-bracket characters entirely so the openapi checker's naive
array parser can't be broken by either version. Bounds the sweep-
and manual-route-triggered tryIdeAuth() busy-timeout to 250ms
(down from the interactive auto-import path's 2000ms), since both
share the main event loop with all other in-flight requests and
should fail fast on a WAL-lock collision rather than block the
whole instance for up to ~4s. Has the manual refresh route bypass
the sweep's IDE-auth dedup cache so a click always sees a fresh
read, consistent with this plan's existing "manual actions never
see stale cached data" convention. Documents the previously-missing
agent-availability route in ROUTE_GUARD_TIERS.md's spawn-capable
table.
* fix(cursor): adds SIGKILL follow-up to the status-check spawn
Matches the nudge spawn's existing SIGTERM+SIGKILL pattern so an
unresponsive cursor-agent status check can't leak a lingering
process if it ignores SIGTERM.
* docs(cursor): fills in the PR number for changelog fragments
Renames the 3 changelog.d fragments to their PR-numbered filenames and replaces the (#PR) placeholder with #9173, now that the PR exists.
* fix(cursor): corrects changelog fragments to reference PR #9173
The prior commit only staged the git mv rename — a git add invocation with a stale (pre-rename) pathspec aborted before the actual (#PR) -> (#9173) content edit was staged, so the rename landed without the fix it was meant to carry. This captures the actual content change.
* docs(cursor): regenerates the agent-skills catalog for the new route
check:agent-skills-sync (CI's Merge integrity gate) requires SKILL.md files to stay in sync with the live route catalog. Adding /api/providers/cursor/agent-availability in an earlier commit needed a regen this branch never ran.
* chore(quality): rebaselines file-size caps grown by agentrouter merges
Two already-merged agentrouter commits (564c204ef, ec150a006) on release/v3.8.50 grew open-sse/executors/base.ts, open-sse/handlers/chatCore.ts, and tests/unit/chatcore-translation-paths.test.ts past their frozen caps before this PR branched — unrelated to the Cursor renewal changes here. No PR branch is left to fix the growth in-place, so the caps are bumped to the current real sizes, following the existing release-green rebaseline precedent in this file.
* fix(sse): imports getModel helpers from db/models, not localDb
A recently-merged agentrouter commit added a @/lib/localDb import in chatCore.ts, violating the no-restricted-imports rule (Hard Rule #2 — never barrel-import from localDb.ts). Points the import at the owning module, src/lib/db/models.ts, where both functions are actually defined, and prunes the now-stale suppression entry.
* fix(sse): scopes CC-relay anthropic-beta to its own requestDefaults
Two already-merged agentrouter commits widened usesClaudeCodeProtocol()'s native-Claude system-transform block (billing header + selectBetaFlags-derived anthropic-beta) to also run for generic CC-compatible relay connections, not just real claude traffic and agentrouter's own wire-image mimicry. selectBetaFlags() has no visibility into a relay's own providerSpecificData.requestDefaults, so its header replacement silently wiped out an earlier context-1m append and force-included redact-thinking regardless of the relay's own opt-in. Restores both for plain CC-compatible relays only; real claude/agentrouter traffic is unaffected.
Also bumps four stale hardcoded Codex/Claude Code CLI version-string test assertions (0.144.1->0.146.0, 2.1.219->2.1.220) that drifted when the same two commits bumped the version constants without updating their tests, and rebaselines base.ts's frozen file-size cap for this fix's own +35 lines.
* fix(sse): preserves bare CC-relay native treatment and context-1m
The previous commit's fix was too broad in one direction: excluding ALL CC-compatible relays from the native-Claude header block broke two pre-existing tests (cc-compatible-provider.test.ts, v3.6.6) that rely on that treatment for a 'vanilla' relay with no providerSpecificData.requestDefaults configured.
Refines the gate to this whole native-Claude header-replacement block: replace headers for real claude traffic, agentrouter's wire-image mimicry, OR a CC-relay with no requestDefaults at all — only a relay with EXPLICIT requestDefaults (context1m/redactThinking/summarizeThinking) gets to keep buildHeaders()'s own correctly-computed header set. A redact-thinking-beta strip (unconditional, a no-op when native treatment didn't apply) covers the one remaining gap: selectBetaFlags() force-includes it for a bare relay's opaque client, which a bare relay never explicitly opted into.
Verified against all three previously-conflicting pre-existing tests simultaneously: executor-default-base.test.ts's '1M beta' test, both cc-compatible-provider.test.ts SSE-forcing tests, and provider-request-failure-pipeline.test.ts's 'keeps request beta headers' test (the last of which was already broken by the raw agentrouter merge, confirmed via direct comparison against that exact commit).
* fix(sse): fills in remaining stale CLI version literals
The same two agentrouter commits bumped Codex/Claude Code CLI version constants (0.144.1->0.146.0, 2.1.219->2.1.220) without updating every hardcoded test assertion. This round covers the ones the previous version-string commit missed: the anthropic-cache-fingerprint billing-version constant, a cc-bridge-transforms body assertion, the UI-mirror parity test's own snapshot plus its RoutingTab.tsx source of truth, an integration test's User-Agent assertion (inconsistent with its own dynamic Version assertion two lines up), and the translate-path golden snapshot. Also updates a stale doc comment referencing the old literal by value instead of by constant name.
* fix(cursor): imports from db/ modules, not the localDb barrel
Both files violated Hard Rule #2 (never barrel-import from localDb.ts) — a genuine lint error that had gone uncaught locally. refresh-cursor/route.ts imported getCachedProviderConnectionById from @/lib/localDb instead of its owning module, @/lib/db/readCache. tokenHealthCheckCursor.ts copied the same pattern from its sibling tokenHealthCheckCopilot.ts (an existing, already-suppressed violation) for updateProviderConnection; imports it from @/lib/db/providers instead, with no circular-import fallout (verified via the existing token-health-check-cursor and refresh-cursor-route test suites).
* fix(db): removes stale raw-SQL allowlist entry for cursor route
The cursor auto-import route no longer contains raw SQL — that query
now lives in src/lib/cursor/tokenExtractor.ts, outside the
route/handler scope check-db-rules scans. The allowlist entry was
stale, tripping the stale-enforcement gate.
* fix(test): registers cursor test files in stryker tap.testFiles
Three unit test files covering mutation-tested modules
(route-guard-cursor-agent-availability, route-guard-cursor-refresh,
cursor-renewal) were missing from stryker.conf.json's tap.testFiles,
tripping the mutation-test-coverage gate's drift detection.
* chore(ci): retriggers checks (stuck GH Actions runner on shard 2/4)
* fix(sse): restores CC-relay context1m/redact-thinking test coverage
Rebasing onto release/v3.8.50's new tip (35405be60, an unrelated
agentrouter protocol-inference commit) silently flipped two assertions
this branch's own earlier fix (687fbda62) depends on, in the same test
files that commit touched for other reasons:
- executor-default-base.test.ts: calls[0] (a bare CC-relay with no
requestDefaults) expected redact-thinking-beta absent; flipped to
present. calls[1] (context1m+redactThinking requestDefaults) expected
the context-1m beta preserved; flipped to absent.
- provider-request-failure-pipeline.test.ts: expected Accept:
text/event-stream and the context-1m beta present for a relay with
explicit requestDefaults; flipped to application/json and absent.
35405be60 did not touch open-sse/executors/base.ts at all, so these
were test-only edits made without visibility into the still-unmerged
CC-relay header-preservation fix on this branch — they quietly matched
the assertions back to the pre-fix (buggy) behavior instead. Restores
the original, validated expectations; all three interdependent test
files (executor-default-base, cc-compatible-provider,
provider-request-failure-pipeline) verified passing together again.
* ci: re-trigger checks after GitHub Actions incident (2026-08-07, resolved)
* ci: re-trigger checks (previous push event was dropped)
* fix(quality): restore dropped vi.json cursor-renewal keys + rebaseline test growth
vi.json was missing 4 keys (cursorSessionUnchanged, cursorAgentNudgeTitle/Body/Dismiss) that this PR's own pre-merge branch had translated -- the original merge's 'git checkout --theirs' resolution for the 7 conflicted locale files discarded them since upstream's vi.json has no cursor-token-renewal feature. Restored from pre-merge tip a38003e30. Also rebaselines combo-routing-engine.test.ts (3457->3464) for the comment growth from the ALL_ACCOUNTS_INACTIVE fix, caught by CI's PR-mode check:file-size.
* chore(tests): drop explanatory comments on ALL_TARGETS_SKIPPED assertions
Kept the assertion value fix (ALL_ACCOUNTS_INACTIVE -> ALL_TARGETS_SKIPPED); the comments were unnecessary. Reverts the file-size baseline bump these comments caused (combo-routing-engine.test.ts back to its original 3457).
* Fix custom tool output pairing during compression (#8932)
* Bypass proxy compaction for native Codex context
* fix(sse): extract Codex tool-call output repair to leaf module for file-size gate
repairMissingCodexToolCallOutputs (added by #8932 for custom_tool_call
pairing) pushed codex.ts past the frozen file-size baseline. Extract it
to open-sse/executors/codex/toolCallRepair.ts, leaving only the wiring
call in codex.ts. Rebaseline the test file's genuine +41 line growth
from #8932's new custom_tool_call_output coverage.
Co-authored-by: JxnLexn <JxnLexn@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: JxnLexn <JxnLexn@users.noreply.github.com>
* fix(antigravity): per-model quota + 30min credits_exhausted reprobe
- accountFallback.ts: hasPerModelQuota() now treats antigravity/agy as
per-model quota. A single-model 429 no longer cascades to all models
in the provider.
- connectionRecovery.ts: credits_exhausted removed from terminal set;
isCreditsExhaustedReprobeCandidate() with 30min default. Loads
active+inactive rows so inactive credits_exhausted accounts can recover.
- tests/unit/quota-connection-recovery.test.ts: 6 cases covering pure
helpers + tick wiring.
* fix(antigravity): persist projectId and prefer healthy accounts
Save Cloud Code projectId after runtime discovery, skip accounts missing
projectId when alternatives exist, and mark missing_project_id on 422.
* fix(antigravity): skip quota-exhausted models during account selection
Avoid repeatedly dispatching to Antigravity models that already report
exhausted quota, reducing wasted upstream calls and combo fallback latency.
---------
Co-authored-by: hermes <hermes@nous.local>
GithubExecutor.buildUrl() only consulted the static PROVIDER_MODELS registry
via getModelTargetFormat("gh", model), so a custom Copilot model (e.g.
gpt-5.6-terra/gpt-5.6-luna) with its dashboard "Target Format" set to
OpenAI Responses API always still routed to /chat/completions and got
rejected upstream with "model ... is not accessible via the
/chat/completions endpoint" — the setting had no effect on real routing.
chatCore already resolves the correct per-request targetFormat (including
the custom-model override) via resolveChatCoreTargetFormat(), but that value
was never threaded past chatCore into the executor's own URL-building
decision. Mirrors the zai/glm-coding-apikey fix (#7364) for the identical
class of bug: chatCore/executionCredentials.ts now surfaces the resolved
override onto providerSpecificData.targetFormat when it resolves to
openai-responses for the github provider, and GithubExecutor.buildUrl()
prefers that value over the static registry lookup when present.
Verified: 6 new regression tests plus all 95 pre-existing github/executor
tests green.
Co-authored-by: Wital <wital@example.com>
* feat(cursor): prefer live synced catalog for listing and Test All
When an active synced Cursor catalog exists, list only live models plus
injected auto routers (and customs). Keep the static registry as offline
fallback.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): send live-catalog model ids verbatim on AgentRun
Skip #7289 effort/reasoning splits when the exact id is in the active synced
Cursor catalog so AgentRun does not rewrite flattened live ids into missing
bases that return AI Model Not Found. Also wires auto-cost/balance/intelligence
to default + optimization for the injected routers.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yansigit <yansigit@users.noreply.github.com>
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>
* 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): 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>
* 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
---------
Co-authored-by: Vasily Larin <larin.vas@outlook.com>
* fix(ci): drop unused RadarReferrals type export — dead-code ratchet back to 227 baseline
The radar referral-links feature (#9697) exported the inferred type
RadarReferrals from feedSchema.ts but nothing imports it (the singular
RadarReferral is the consumed type). knip counts it as a new dead export,
pushing the dead-code ratchet to 228 > 227 and failing Fast Quality Gates
on every PR born after the merge. RadarReferralsSchema itself stays — it
is used by RadarFeedSchema.
Refs #9737
* fix(ci): clear the 08-08 base-red layer — prod crash in chat.ts, Responses API payload regression, born-red stdio test, gate drifts
Six independent base-reds from the 08-07 evening merge batch, each verified
against the pure release/v3.8.50 tip:
- src/sse/handlers/chat.ts: #9467's squash carried a refactor hunk that
renamed the all-rate-limited breaker guard to an UNDEFINED variable
(isAllRateLimited) — a production ReferenceError on the all-accounts-429
path (chat.ts is outside typecheck:core scope, so only tests caught it).
Restore credentials?.allRateLimited. Guard: chat-rate-limit-body-lock (2/2),
also un-breaks batch_api and chat-combo-live-test.
- open-sse/utils/stream.ts: #9315 switched providerPayload summaries to the
accumulated responseBody, but in passthrough paths that body is synthesized
in chat-completion shape — Responses API lost its `response` object in the
dashboard payload. Keep the events-derived summary for OPENAI_RESPONSES
only. Guard: stream-utils + stream-collector-9315 suites (51/51).
- tests/unit/mcp-stdio-json-purity.test.ts: born red — the full CLI chain
takes ~10s (2x tsx import + DB init) and the test slept a fixed 4s. Poll
for the first stdout line with a 60s deadline instead.
- tests/unit/plugins-route-error-sanitization.test.ts: register #9445's new
marketplace/install route in PLUGIN_ROUTES (route already sanitizes) (33/33).
- tests/unit/provider-models-route-codex.test.ts: realign pinned GPT-5.6
input limit to #9432's deliberate 272000→922000 bump (7/7).
- lint: fix 11 no-explicit-any errors in repro-9630 + specialty-9293 tests,
prune 1 orphaned suppression, allowlist the opencode-ai devDependency
(#8869, publisher-verified), and reword a doc line the fabricated-docs
gate misread as an env var.
Gates re-verified locally: lint:json --max-warnings 0 exit 0, dead-code 227,
typecheck:core clean, check:deps OK, check:fabricated-docs OK.
Refs #9737
* fix(ci): clear the third 08-08 base-red layer — invalid ru rule pack, stale event pin, orphaned UI repro test, pack/mutation/file-size drifts
Follow-up to the previous layer: the serial fast-gates chain unmasked one
more stratum after file-size/dead-code went green, all verified against the
merged release/v3.8.50 tip:
- compression rules ru/ultra.json (#9581): two rules shipped
minIntensity "notes", which is not a valid CavemanIntensity
(lite|full|ultra) — loading ANY language pack list threw and killed the
rtk-loader suite. Mapped both to "ultra" (they are the most aggressive
punctuation/case rules, matching the en pack tiers). 2/2.
- plugins-welcome-banner-e2e: #9668 added the onStreamComplete builtin
event (real emission path via runOnStreamCompleteHooks) and missed this
pinned-list sibling. 35/35.
- tests/unit/free-pool-frontend-repro (#9046): landed as .tsx with
node:test semantics — no runner collects tests/unit/*.tsx, so it NEVER
ran (test-discovery NEW-orphan). It contains zero JSX; renamed to .test.ts
so the unit runner's existing glob collects it. 5/5 (first real run).
- pack-policy: allow + require bin/mcpStdioConsoleGuard.mjs (#9281) — it is
preloaded via node --import by bin/mcp-server.mjs, so a published artifact
without it crashes 'omniroute --mcp' at startup.
- stryker.conf.json: add 5 covering unit tests from the batch (#8779/#9204/
#9330/#9630/openrouter-passthrough) to tap.testFiles (--strict drift).
- file-size-baseline: consolidate the base-drift rebaseline for the 12
files grown by the 08-06..08-08 batches (#9616's entries never reached the
base; measured on this branch's tree — this PR's own source edits add zero
lines to any frozen file).
Local battery: file-size/deps/test-discovery/mutation/pack-policy/dead-code/
duplication/docs-all/secrets/vuln/workflows ratchets all exit 0; full lint
gate --max-warnings 0 exit 0.
Refs #9737
* fix(types): clear the 3 uncovered open-sse-typecheck regressions + realign combo skip-code siblings
Fourth base-red layer unmasked by the serial gates. The other 4 typecheck
regressions (codex.ts, kiro.ts, tierResolver.test.ts, translator/index.ts)
already have dedicated open [TS7] PRs (#9748/#9753/#9742/#9747) — not
duplicated here. This commit covers only what no open PR owns:
- devin-agentic/serializer.ts TS2367: drop the dead 'role === "system"'
branch — the guard above already narrows role to user|assistant (system
throws unsupported_role). Devin suites 104/104.
- raycast.ts TS2416: the buildHeaders 'override' never matched the base
signature (2nd param is the signed payload string, not the stream
boolean) — renamed to a private buildRaycastRequestHeaders helper so a
polymorphic buildHeaders(credentials, true) call can never bind here.
- modelMetadataRegistry.ts TS2352: PricingByProvider → nested-record cast
now goes through unknown (shape is runtime-guarded by findInsensitive).
- combo-routing-engine.test.ts: realign 2 pre-dispatch-skip expectations to
#9630's deliberate ALL_TARGETS_SKIPPED contract (87/87).
Refs #9737
* fix(ci): clear the fifth 08-08 base-red layer — reasoning-placeholder contract sweep, GPT-5.6 limits sweep, vi key parity
The 08-08 merges (#9610 reasoning replay, #9432 GPT-5.6 limits, #9630 combo
skip codes, #9336 provider key links) each changed a contract and left
sibling tests pinning the old one. Full grep sweep per contract, not just
the shard that happened to go red:
- reasoning placeholder (#9573/#9610): the fix DELIBERATELY removed
NON_ANTHROPIC_THINKING_PLACEHOLDER injection on cache miss — the model
echoed the placeholder as its own reasoning (empty stop) and re-poisoned
cache + client history; DeepSeek's 400 is specific to an EMPTY STRING, not
an absent field. Realigned reasoning-cache (2 cases, renamed to describe
omission) + tool-request-sanitization (1 case + dead import). 60/60.
- GPT-5.6 Codex limits (#9432, 272000 -> 1050000 ctx / 922000 input):
realigned vscode-token-routes-gpt56 (2) + vscode-token-routes (3). 43/43
together with t23-t24.
- combo skip codes (#9630): t23-t24-fallback-resilience T24 now expects
ALL_TARGETS_SKIPPED like the combo-routing-engine siblings.
- vi.json key parity: #9336 added providers.getApiKey/getApiKeyDescription
to en.json without syncing vi (the only locale with a parity gate).
Translated both; providers block reordered to match en key order. 5/5.
- pack-artifact-policy.test.ts: sibling of this PR's own required-paths
change (bin/mcpStdioConsoleGuard.mjs). 10/10.
- combo-routing-engine.test.ts: dropped the 6 comment lines added in the
previous commit so the frozen test file-size stays at its baseline (the
rationale lives in that commit message, not the test body).
Gates: file-size, test-discovery, mutation-test-coverage, pack-policy,
open-sse-typecheck, dead-code all exit 0.
Refs #9737
* fix(translator): keep the reasoning_content placeholder for Xiaomi MiMo — #9610 traded one live 400 for another
The xiaomi-mimo replay test (9router#1321) went red on the base after #9610
removed the NON_ANTHROPIC_THINKING_PLACEHOLDER injection globally. That test
is NOT stale — it guards a documented upstream 400 ('Param Incorrect: The
reasoning_content in the thinking mode must be passed back to the API'), so
realigning it would have masked a reintroduced production bug.
Two real bugs conflict here:
- #9573: forwarding the placeholder makes the model continue its chain of
thought FROM that text (echo -> empty stop) and re-poisons cache/history.
- 9router#1321/#1337: omitting reasoning_content on a plain replay turn makes
Xiaomi MiMo reject the request outright.
#9610's evidence for omitting is provider-specific — it verified that
deepseek-v4-flash accepts an ABSENT field. It does not extend to MiMo. So the
omission stays for every provider #9610 covered, and the placeholder survives
the cache miss only for xiaomi-mimo (new requiresReasoningContentPresence
predicate next to isReasoningOnlyReplayTarget). The echo that comes back is
still stripped on the way in by isInternalReasoningPlaceholder(), so #9573's
cache/history poisoning stays fixed for MiMo too.
Both contracts now hold simultaneously: xiaomi-mimo replay + reasoning-cache +
tool-request-sanitization 61/61; placeholder-strip/responses/translator/combo
regression sweep 168/168. Gates: file-size, open-sse-typecheck, dead-code,
mutation-test-coverage exit 0; typecheck:core clean.
A live check on the VPS (Hard Rule #18 path 2) is the only way to confirm the
DeepSeek half of #9610's empirical claim; flagging it in the PR rather than
widening this fix on speculation.
Refs #9737
* test(translator): pin the reasoning-placeholder provider scope so neither half of the conflict can silently re-break
#9610 removed the placeholder globally on the strength of ONE provider's
observed behavior (deepseek-v4-flash accepting an absent reasoning_content),
which re-opened the MiMo 400 (9router#1321). The previous commit scoped the
placeholder to xiaomi-mimo; this pins BOTH directions in one test so the next
global edit fails loudly instead of trading the bugs again:
- xiaomi-mimo plain replay turn, cache miss -> reasoning_content present
(narrowing the scope away from MiMo re-opens 9router#1321)
- deepseek plain replay turn, cache miss -> reasoning_content absent
(widening it back to DeepSeek re-opens the #9573 echo bug)
Guard verified by mutation: forcing requiresReasoningContentPresence() to
return true makes the DeepSeek half fail (1 pass / 1 fail), and the file was
restored from the pre-probe copy before committing.
Also checked kimi-coding/kimi-coding-apikey, the other strict-contract entries
in REASONING_REPLAY_PROVIDERS: their originating PR (#7673) fixes capture and
replay of REAL reasoning and documents no 400 on an absent field, so they stay
out of the placeholder scope — evidence-scoped, not speculatively widened.
Reasoning suites together: 87/87. Gates: file-size, test-discovery,
mutation-test-coverage, dead-code exit 0; eslint clean.
Refs #9737
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(providers): gate premium opencode-zen/opencode-go models behind an API key (#8681)
Root cause: the free/noauth opencode provider (and opencode-zen/opencode-go)
expose the full upstream model list including PREMIUM models (gpt-5, claude-*,
gemini-*, kimi-k2.6, etc.). With a keyless connection, the executor sends no
Authorization header and upstream returns 401 'Missing API key' for any
premium model — which is the exact string the client shows.
Fix: add a request-time gate in OpencodeExecutor.execute() that detects
keyless connections + premium models and returns a clear 402 error with
message 'This model requires an opencode API key — add one in Settings →
Providers.' instead of proxying the raw upstream 401.
Free models (known free catalog + suffix) continue to work keyless
(deepseek-v4-flash-free, big-pickle, etc.). Users with a valid opencode API
key keep premium access. opencode-go has no free tier — all models require
a key.
* fix(providers): use a free opencode model in the #7993 proxy-routing test
The #8681 keyless-premium gate short-circuits 'grok-code' (a premium
model) with 402 before any fetch happens, so the proxy-egress assertion
never saw a request. Swap to 'deepseek-v4-flash-free' (already applied
to the sibling opencode-proxy-rotation-4954.test.ts in this same PR)
so the test again exercises the proxy-routing path it targets.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* feat(dashboard): add RADAR_ENABLED flag (default off)
* feat(db): radar feed cache + settings with encrypted supporter key
* feat(radar): signed feed sync with pinned key and version floor
- feedSchema.ts: Zod v4 schema mirroring the server feed format
(discriminated union on budget.kind, enum constraints, etc.)
- pinnedKeys.ts: Ed25519 SPKI-DER pinned key + env override for forks
- verify.ts: signature verification over exact wire bytes, never throws
- sync.ts: full download/verify/validate/cache pipeline with injectable
deps, feature-flag gate, opt-in gate, version floor (numeric compare),
and sanitized error reasons (no stack traces)
- 40 tests covering: contract hash, key handling, sig verification,
schema validation, version compare, all sync paths (disabled, opt_out,
invalid_signature, invalid_schema, stale, updated, error), auth header
injection, and cache-untouched assertions for every failure mode
* feat(radar): read-time overlay merge rules over the free catalog
Pure function applyFeed() merges the cached Radar feed over the static
baseline catalog at read time, honoring 4 rules:
1. Feed never overwrites a local override field.
2. enabled:false disables the entry with disabledBy:"radar" provenance.
3. User-added entry NOT in the feed survives untouched.
4. User deletion tombstone prevents feed resurrection.
getRadarCatalog() accessor in index.ts: flag off / no cache / corrupt
payload all fall back to baseline. Valid cache applies the overlay and
returns feed metadata (version, tier, fetchedAt).
TDD: 19 tests (4 rules + dedup + origin + accessor flag/cache/corrupt/
valid/bad-feed + baselineToMergedEntries converter).
* feat(dashboard): radar catalog and guided setup screens
- API routes: GET /api/radar/catalog, POST /api/radar/sync, POST /api/radar/settings
- All gated on RADAR_ENABLED flag (404 when off)
- Error responses via buildErrorBody(), never raw stack/message
- Settings never echoes clear supporter key (masked omr_****<last4>)
- Sync delegates to syncRadar() server-side, never proxies feed URL
- Dashboard pages:
- /dashboard/radar: 4 states (flag off, opt-in pending, empty, populated)
- /dashboard/radar/setup?provider=X: guided setup with steps, key URL, test connection
- Uses existing Card component and next-intl patterns
- Sidebar: radar entry in costs group with icon
- i18n: pt-BR and en keys for radarPage and radarSetupPage namespaces
- Tests:
- radar-api-routes.test.ts: 11 tests (flag-off 404, flag-on shape, error sanitization)
- radar-page-state.test.ts: 5 tests (pure state logic)
- All 90 radar tests pass (including prior 74)
* docs(radar): module doc and flag-off inertia test
Add docs/frameworks/RADAR.md covering the flag gate, the separate data-sync
opt-in and privacy promise, the Ed25519 signature/pinned-key security model,
tiers, the read-time overlay merge rules, and the self-hosting env vars —
plus index entries in CLAUDE.md/AGENTS.md/docs/README.md/REPOSITORY_MAP.md.
Document RADAR_FEED_URL and RADAR_FEED_PUBKEY in .env.example and
docs/reference/ENVIRONMENT.md to satisfy check:env-doc-sync, which was
failing on this branch since the sync.ts commit added the reads.
Add tests/unit/radar-inertia.test.ts as the single canonical place asserting
the "RADAR_ENABLED off => zero behavioral delta" claim end to end: the three
/api/radar/* routes 404, the flag resolves to the definition default with no
override, getRadarCatalog() returns exactly the baseline without touching the
cache, and computeFreeModelTotals() keeps its pinned values with the Radar
module imported alongside it.
* fix(db): renumber radar migration to 135 after collision with 134
The base branch introduced 134_proxy_logs_egress_ip while this branch carried
134_radar_cache_settings; the migration runner rejects duplicate numeric prefixes.
This migration has never been applied to a real database (the PR is unmerged), so
no retroactive isSchemaAlreadyApplied guard is needed.
* i18n(radar): translate radar catalog and setup strings to all locales
The UI-coverage ratchet measures (present - placeholder) / total_en, so the
__MISSING__ sentinels that i18n:sync-ui writes do not count as covered — only
real translations restore the metric. Scoped to this PR's namespaces
(radarPage, radarSetupPage, sidebar.radar*) instead of a bulk sync, which would
have pulled ~978 unrelated pending keys into this diff.
Placeholders and code identifiers verified preserved across all 1682 strings.
* fix(radar): trust the served-tier header instead of the signed body field
The signed feed body always carries tier:"live" by design (one signed
artifact per version — rewriting the field server-side per request
would break the exact-bytes Ed25519 signature). The server now returns
the tier ACTUALLY served via the x-omniroute-feed-tier response
header, so free users on a delayed community snapshot no longer see
"Ao vivo (tempo real)" in the UI.
sync.ts now reads and validates that header (falling back to the
body's tier only when the header is absent or holds an unrecognized
value) and stores the served tier in the cache; index.ts already
surfaces cache.tier to the UI unchanged.
* test(combo): shorten an assert message that exceeded the line limit
The assertion added by #9507 was 104 chars, so prettier reformatted it into
five lines on the next commit that touched the file, pushing it past its
frozen size (3449) and failing check:file-size. The message is shortened
(the issue reference stays in the comment directly above); the assertion
itself is unchanged, and the file is back to 3448 lines and prettier-clean.
* i18n(radar): use the canonical zh-TW glossary terms
The machine translation produced retired renderings the glossary gate blocks:
供應商 for provider (canonical 提供者) and 文檔 for documentation (canonical 文件).
Fixed across the 11 affected radar strings; tests/unit/i18n-glossary-consistency-check.test.ts
is back to 17/17.
* fix(radar): point the default feed URL at the domain that exists
radar.omniroute.dev was a placeholder for a domain that was never registered,
so an out-of-the-box sync would fail DNS resolution for every user. The live
feed is served from radar.omniroute.online (the subdomain the design always
specified), now behind Cloudflare TLS. Forks still override it via
RADAR_FEED_URL.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>