Notion AI has no public inference API (see closed request #3272), so this
adds it as a new entry in the established web-cookie provider category
(chatgpt-web, claude-web, grok-web, ...): cookie-based auth via the
token_v2 session cookie posted to Notion's undocumented internal
POST /api/v3/runInferenceTranscript endpoint, translating its NDJSON
transcript-patch stream into OpenAI-compatible chat completions.
- NotionWebExecutor (open-sse/executors/notion-web.ts): resolves the
token_v2 cookie (+ optional space_id/notion_browser_id), builds a
Notion transcript from the chat messages, parses the NDJSON response
(cumulative-snapshot semantics, mirroring gemini-web.ts's handling of
#7163), and returns a chat.completion or pseudo-streamed SSE response.
All error paths route through makeExecutorErrorResult (sanitized).
- RegistryEntry under open-sse/config/providers/registry/notion-web/,
registered in providers/index.ts REGISTRY and executors/index.ts
(alias "nw").
- WEB_COOKIE_PROVIDERS entry (src/shared/constants/providers/web-cookie.ts)
with subscriptionRisk + webCookie risk notice, clearly labeled
"(Unofficial/Experimental)".
- Cookie-probe validator (validateNotionWebProvider) against Notion's
getSpaces endpoint, and a webSessionCredentials.ts UI entry for the
"Add session cookie" flow.
- Regenerated docs/reference/PROVIDER_REFERENCE.md and the
provider/translate-path golden snapshot (purely additive diffs); synced
the "251 providers" count across README/AGENTS/CLAUDE.md
(check:docs-counts STRICT gate).
Tests: tests/unit/executor-notion-web.test.ts (22 cases — registry
consistency, mocked-upstream request/response translation, NDJSON
snapshot parsing, cookie resolution, sanitized error paths) plus the
existing executor-web-cookie-sweep, provider-alias-uniqueness,
check-provider-consistency, web-session-credentials, and
provider-translate-path-golden suites all pass with notion-web included.
Adds felo-web, a free no-signup no-API-key chat/search-agent aggregator
(felo.ai), following the same architectural pattern as the existing
duckduckgo-web/blackbox-web "-web" scrape family:
- POST /api-proxy/main/search/threads opens a search thread and returns a
stream_key.
- GET /api/message/v1/stream/{stream_key} streams Felo's bespoke
data:{...}-line SSE, translated into OpenAI-compatible chunks.
- 5 models (felo-chat/search/scholar/social/document) map to Felo's
chat/google/scholar/social/document search categories.
Registered in providers.ts (noauth.ts, no-auth like duckduckgo-web),
providerRegistry.ts, and executors/index.ts. Free-tier catalog entries
added with tos: "avoid" (reverse-engineered endpoint, no published API —
same ToS posture as the other -web scrape providers).
No live network access was available in this environment to smoke-test
against the real felo.ai endpoint, so validation is TDD via mocked fetch
(tests/unit/felo-web-executor.test.ts): thread-creation payload shape,
SSE parsing (answer-snapshot diffing + final_contexts drop), streaming
and non-streaming response translation, and error/timeout paths that
route through sanitizeErrorMessage() per the error-sanitization rule.
Registers Rev AI as a 13th async-job STT provider, mirroring the
AssemblyAI/Kie.ai upload -> submit -> poll pattern already used by the
audio transcription handler:
- audioRegistry.ts: new "rev-ai" entry (bearer auth, async: true,
format: "rev-ai") with machine/low_cost/fusion transcriber models.
- audioTranscription.ts: handleRevAiTranscription() submits the job
with the media file inline in the multipart body (field "media"),
polls GET /jobs/{id} until "transcribed"/"failed", then fetches the
plain-text transcript. buildMultipartBody() gained an optional
fileFieldName param (default "file") so Rev AI's "media" field name
doesn't require a bespoke multipart builder. Errors route through
the existing upstreamErrorResponse()/errorResponse() helpers.
- providers/audio.ts: catalog entry (id/alias/name/icon/color/website)
for the dashboard connection UI.
- validation/audioMiscProviders.ts + validation.ts: validateRevAiProvider
wired into the provider "Test Connection" dispatcher.
Streaming (WebSocket) STT is scoped as a follow-up per the analyzed
plan — no existing precedent to extend, needs its own design pass.
Closes#6655.
The Discord invite (discord.gg/EkzRkpzKYt) was returning "Invalid Invite";
replace it with the new permanent invite across README + docs + zh-CN/zh-TW
i18n mirrors, and refresh the WhatsApp Brasil group link.
Reported-by: WhatsApp community (support mesh)
Rebuilt onto release/v3.8.49 feature-only: the branch's original file-size
decomposition of CostOverviewTab collided with the release's own component
extraction (#7272 TopListCard). Kept just the 180D/365D range delta —
CostRange/COST_RANGE_VALUES, the RANGE_OPTIONS selector, the getRangeStartIso
handlers in the analytics and requests-by-provider-date usage routes, and the
range180d/range365d i18n labels across all locales.
Inspired-by: 9router#2361
Codex's rolling "session" quota window only starts counting down once a
request lands inside it, so an idle connection's window keeps sliding
forward and the first real request after a long idle period pays for the
whole warm-up latency. This adds a strictly opt-in, per-connection
scheduler (default OFF) that watches an enabled connection's reported
resetAt and, once it slides forward, fires one tiny non-billed-model
request through the real Codex executor to keep the window warm.
- New in-process scheduler (src/lib/services/quotaAutoPing.ts), fully
dependency-injected (settings, DB, credential refresh, usage fetch,
executor, circuit breaker) and clock-injectable for deterministic tests.
Reimplemented in TS from the shipped 9router
src/shared/services/quotaAutoPing.js (Codex half only).
- Migration 123: last_ping_at / last_pinged_reset_key on
provider_connections, so the scheduler never re-pings the same reset
window twice.
- Settings: codexAutoPing.connections map, default {} (nobody opted in),
validated by the shared Zod schema.
- Respects the existing resilience layers: skips a connection whose
provider circuit breaker is open or whose rateLimitedUntil cooldown is
active, and applies its own 15-minute failure cooldown after a failed
ping.
- UI: new per-connection toggle (Settings -> AI -> Codex Quota Auto-Ping)
with an explicit "consumes real quota" tooltip, wired to the settings
PATCH route; i18n keys added to all 43 locales (EN authored, others
filled from the EN fallback pending real translation).
- 15 new deterministic unit tests covering enable/disable, first-reset
observation (cache-only, no ping), reset-slide ping, stable-reset
no-op, min-ping-interval, same-resetKey dedupe, session/weekly quota
exhaustion, non-OAuth skip, circuit-breaker-open skip, cooldown skip,
failure-cooldown skip, failed-ping bookkeeping, the real executor call
shape, and credential-refresh failure handling.
Antigravity's 2-bucket auto-ping is out of scope for this PR (no upstream
reference exists for its reset shape) and is tracked as a follow-up.
Ref #6977 (backend + settings + minimal UI toggle; Antigravity follow-up
tracked separately — not closing the issue from this PR)
* fix(providers): cap grok-cli tools at 200 for cli-chat-proxy
xAI's cli-chat-proxy enforces a hard limit of 200 tools per request and
returns a 400 above that ceiling. A client fanning a large MCP toolset
through Grok Build/Composer (e.g. Claude Code with many registered
tools) can exceed it. transformRequest() now caps the tools array
defensively before forwarding, and the grok-cli registry entries are
annotated supportsReasoning:false to document the existing (already
unconditional) reasoning_effort/reasoning strip for these two models.
Co-authored-by: Joseph Yaksich <294273268+gitcommit90@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2534
* chore(changelog): fragment for #6986
---------
Co-authored-by: Joseph Yaksich <294273268+gitcommit90@users.noreply.github.com>
Newly-added provider connections default testStatus to null until an operator explicitly runs a connection test. The combo builder's active-providers filter only kept testStatus === active/success, so a freshly-added custom provider was excluded from activeProviders — ModelSelectModal's loadCustomProviderModels() effect never fired for it, and its models never populated the combo model picker.
Extracted the eligibility check into isEligibleActiveConnection (src/lib/combos/builderDraft.ts), treating a never-tested connection the same as a known-good one (consistent with deriveConnectionStatus in builderOptions.ts, which only flags error on an explicit error/fail testStatus).
Reported-by: fajarbossit (https://github.com/decolua/9router/issues/2057)
gemini-cli (and any @google/genai-based client) sends its credential
exclusively via x-goog-api-key and it is not client-configurable to use
Authorization/x-api-key instead. Add it as an unconditional fallback,
after Authorization: Bearer and x-api-key, before the path-scoped URL
token, in both the real enforcement gate
(src/server/authz/policies/clientApi.ts::extractBearer()) and the
general extractor (src/sse/services/auth.ts::extractApiKey()).
The header-read/trim logic is extracted into a new leaf module
(src/sse/services/googApiKeyAuth.ts) shared by both call sites, so the
frozen auth.ts file only takes the minimal chokepoint wiring
(config/quality/file-size-baseline.json rebaselined 2458->2461 with
justification, matching this repo's established extraction pattern).
Closes#7034
* fix(openai): strip reasoning_effort when GPT-5.x tools present (port from 9router#2540)
Raw api.openai.com Chat Completions rejects GPT-5.x reasoning models that carry both function tools and an active reasoning_effort with HTTP 400 ("Function tools with reasoning_effort are not supported ... Please use /v1/responses instead"). The existing forceResponsesUpstream guard only reroutes openai-compatible-* connections carrying MCP/tool_search tool shapes; the plain openai provider had no equivalent guard, so gpt-5.x models used with a coding client (function tools + any explicit reasoning effort) still hit the upstream 400. Add stripGpt5ReasoningWhenTools() (gpt5SamplingGuard.ts), wired into chatCore.ts alongside the existing sampling guard, to drop reasoning_effort/reasoning when function tools are present and reasoning is active, letting the request succeed on /v1/chat/completions.
Reported-by: Tech Solution (@techsolutionmta) (https://github.com/decolua/9router/issues/2540)
* fix(openai): scope reasoning-strip guard to /chat/completions only
stripGpt5ReasoningWhenTools gated on provider+model-name alone, so once
#7242 routes the public GPT-5.6 family to /v1/responses (targetFormat
"openai-responses", which natively supports tools + reasoning), the two
PRs would compose into the worst of both worlds: routed to the endpoint
that supports reasoning, but reasoning stripped anyway. Pass the
request's already-resolved targetFormat into the guard and skip the
strip whenever it is not going out over /chat/completions, so the
guard tracks the actual upstream surface instead of a model-name list
that would need updating for every future GPT-5.x family.
Reported-by: Tech Solution (@techsolutionmta) (https://github.com/decolua/9router/issues/2540)
* fix(providers): honor configured proxy on Grok Build egress
The grok-cli executor reaches Grok Build over raw `https.request()` (forced
IPv4, to dodge Cloudflare blocking on the direct path) rather than the
process-wide patched `fetch()` that every other executor uses. `https.request()`
never consults the proxy AsyncLocalStorage context, so the proxy the caller
already pinned upstream in chatHelpers.ts (`runWithProxyContext`) was silently
ignored on BOTH grok-cli paths: chat inference (`nativePost`) and OAuth token
refresh (`nativeHttpsPost`, POST https://auth.x.ai/oauth2/token).
User-visible effect: an operator who assigns a proxy to a Grok Build connection
(or provider/global scope) still egresses on the host's real IP — an IP leak
that defeats account-isolation/anonymity setups, and breaks Grok Build entirely
for operators who must egress through a proxy.
Fix is delta-only: `resolveGrokRequestDispatch()` reads the already-resolved
proxy via the shared `resolveProxyForRequest()` and returns either an
HttpsProxyAgent bound to it, or — when no proxy is configured — the existing
forced-IPv4 direct options, unchanged. Only HTTP/HTTPS CONNECT proxies are
supported on this path; an explicitly configured proxy of another kind (SOCKS5)
fails closed rather than silently leaking direct, matching the fail-closed
convention for OAuth/account proxies (#3051). The proxy URL is never logged, so
proxy credentials cannot leak into logs.
Regression test: tests/unit/grok-cli-proxy-selection.test.ts (RED before the fix
— `resolveGrokRequestDispatch` did not exist and both request builders hardcoded
`family: 4` with no agent; GREEN after).
Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2343
* chore(changelog): fragment for #7244
---------
Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
* feat(sse): add native xAI Grok Imagine video generation provider
OmniRoute's /v1/videos surface already supported 10 provider formats
(vertex-veo, google-flow, comfyui, sdwebui-video, kie-video, runwayml,
haiper-video, veoaifree-web, leonardo-video, dashscope-video), but xAI
had no native entry — Grok Imagine was only reachable indirectly through
the kie proxy market (kie's "grok-imagine/text-to-video" models), which
requires a separate kie.ai account and bills through kie.
This registers xai as a first-class video provider that talks to
api.x.ai/v1/videos directly, reusing the stored xai Bearer apiKey that
the existing image-generation "xai" entry in imageRegistry.ts already
uses — no new credential flow. The new xai-video handler format mirrors
the DashScope create+poll shape, adapted to xAI's request_id / status
("pending" | "processing" | "done" | "failed") job model.
User-visible effect: `xai/grok-imagine-video` works on
POST /v1/videos/generations against a user's own xAI key.
Co-authored-by: ann <daohuyentfqn2l@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2593
* chore(changelog): fragment for #7238
* fix(sse): extract xAI Grok Imagine video handler to fix file-size ratchet
videoGeneration.ts grew to 1407 lines (frozen cap 1265) after adding the
Grok Imagine handler. Extract handleXaiVideoGeneration into a co-located
module (open-sse/handlers/videoGeneration/xaiGrokImagineHandler.ts),
following the googleFlowHandler.ts precedent — same pattern already used
for the Google Flow video handler. File now sits at 1261 lines, under cap.
No behavior change; existing tests (video-xai-grok-imagine.test.ts) cover
the handler through the public handleVideoGeneration() entry point and
pass unmodified.
* refactor(sse): decompose xAI Grok Imagine handler to fix complexity ratchets
The file-size red was masking two ratchet regressions (the gate aborts on
the first failure): complexity 2058 > 2056 and cognitive 891 > 890. Both
came from the PR's own handleXaiVideoGeneration — a single 107-line
function with complexity 37 / cognitive 24, tripping `complexity`,
`max-lines-per-function` (2 complexity-ratchet violations) and
`sonarjs/cognitive-complexity` (1 cognitive violation).
Decompose it into four cohesive units instead of rebaselining:
- resolveXaiVideoOptions() — timeouts/credential/endpoints/prompt
- buildXaiVideoPayload() — OmniRoute body -> xAI create payload
- createXaiVideoJob() — create-job POST -> request_id | error
- pollXaiVideoJob() — poll loop -> terminal outcome
- buildXaiVideoResponse() — outcome -> OpenAI-like response
Both ratchets now sit exactly at baseline (complexity 2056, cognitive 890)
and file-size stays under cap. pollXaiVideoJob reads Date.now() only in the
loop condition, so the caller keeps its timeout budget semantics. No
behavior change; the 7 existing tests pass unmodified.
---------
Co-authored-by: ann <daohuyentfqn2l@gmail.com>
* feat(providers): add Mixedbread AI as embeddings provider (#6660)
Registers Mixedbread AI (https://api.mixedbread.com) in the
EMBEDDING_PROVIDERS registry alongside the other bearer-auth embedding
providers (Voyage AI, Jina AI, Nomic, ...): OpenAI-compatible
/v1/embeddings endpoint, exposing mxbai-embed-large-v1 and
mxbai-embed-2d-large-v1 (both 1024d, Matryoshka). Adds a matching
provider metadata entry (icon/color/authHint/free-tier note) modeled
on the nomic block, regenerates docs/reference/PROVIDER_REFERENCE.md,
and syncs the 250->251 provider-count mentions in README/AGENTS/CLAUDE
required by the strict docs-counts gate.
No executor/translator changes needed — the embeddings handler is a
generic pass-through with no provider-specific branching.
* test(providers): align APIKEY_PROVIDERS count 167→168 for the new 6660 provider (#6660)
Adding the mixedbread embeddings provider to specialty-media.ts grows APIKEY_PROVIDERS
by one; providers-constants-split.test.ts hardcodes the family-partition
total. Legitimate count alignment (the code genuinely added a provider),
not a weakened assertion — all 4 partition/dedup checks still enforced.
* fix(sse): stop dropping tool_search and stop leaking OpenAI-only params in Responses->Chat translation (#7532, #7533)
#7532: `openai-responses.ts` unconditionally dropped `tool_search` when
downgrading a Responses-shaped request to Chat Completions, hiding the tool
from the model and breaking Codex's deferred/lazy tool-discovery protocol for
any provider that gets downgraded (e.g. built-in providers like opencode-go).
tool_search carries `execution: "client"` — the client resolves the call
locally regardless of wire shape — so it is now mapped to a normal Chat
function tool, mirroring the existing local_shell -> shell pattern in the
same file, instead of being silently discarded.
#7533: the same translator unconditionally copied two GPT-5/OpenAI-only
fields (`verbosity`, `prompt_cache_key`) into the translated Chat body
regardless of destination provider. A strict-protocol non-OpenAI upstream
(NVIDIA confirmed by the reporter) 400s on unrecognized top-level parameters.
Both fields are now gated on `credentials.provider === "openai"`, stripped
otherwise; the existing OpenAI-destined behavior (needed for #517's
prompt-caching fix) is preserved byte-identical via a dedicated sanity test.
Regression tests: tests/unit/tool-search-filtered-responses-to-chat-7532.test.ts,
tests/unit/verbosity-prompt-cache-key-provider-gate-7533.test.ts. Two existing
tests that encoded the old buggy contract (unconditional tool_search drop /
unconditional field leak with no credentials) were aligned to the corrected
contract: tests/unit/translator-openai-responses-req.test.ts,
tests/unit/openai-responses-verbosity.test.ts.
Gates run green: file-size, complexity, cognitive-complexity, typecheck:core,
lint (scoped to changed files), and the full touched-area unit test suite
(329 tests, 0 failures).
* fix(sse): keep prompt_cache_key/verbosity for the codex destination (#7533)
The #7533 provider gate allowlisted only "openai", but /v1/responses routes
EVERY request through this downgrade (handleResponsesCore ->
convertResponsesApiFormat) regardless of provider, and codex is an
OpenAI-operated upstream (chatgpt.com/backend-api/codex). Gating it out
stripped prompt_cache_key for Codex and silently re-broke the prompt-cache
affinity #517 exists to protect — with no test covering it.
Allowlist is now {openai, codex} and carries two #517 regression guards.
Non-OpenAI upstreams (NVIDIA) still get both fields stripped, per #7533.
Non-stream Codex (ChatGPT account) chat 502'd with "Response body is already
used". On the wreq-js TLS-fingerprint transport the Response is backed by a
native body handle, and merely accessing response.body disturbs it so a later
.text() throws. The Codex non-stream upstream response has an empty content-type,
so peekCodexSseTransientError early-returns — but its guard evaluated
!response.body (touching .body) before the content-type check, consuming the
body; chatCore's readNonStreamingResponseBody then re-read it and 502'd.
Streaming was unaffected. Reorder the guard to check content-type first.
Validated live on the VPS (192.168.0.15): codex/gpt-5.5 and codex/gpt-5.6-terra
non-stream now return 200; streaming still works. Regression test drives the real
peek with a destructive-.body mock.
CodeQL js/stack-trace-exposure flags ANY error-derived value returned in
the mock route bridge's 500 path, not just error.stack — swapping .stack
for error.message (in #7354, alert #736) left sibling alert #737 open on
the same line. Replace the body with a static string; the test only
asserts status===200, so the 500 body is never inspected. Clears the last
open CodeQL alert repo-wide, unblocking the Quality Ratchet on every PR.
* docs(troubleshooting): document Avast/AVG README.md false positive (#5946)
Avast/AVG quarantine the packaged README.md with MD:HttpRequest-inf[Susp] --
a heuristic false positive on the ~15 http://localhost:20128 examples the file
carries (README ships via package.json -> files, landing at
node_modules/omniroute/README.md).
Adds a Troubleshooting section explaining the detection is benign, how to stop
the notifications (AV exclusion), how to report the false positive upstream,
and why we do not mangle the localhost examples to dodge one vendor heuristic.
Documentation only -- no functional change.
Reported-by: DemonNCoding
* docs(changelog): add fragment for #7295
* fix(sse): silence noisy proxy-failure log on caller-initiated abort
The pinned-proxy dispatch path in proxyFetch.ts logged every failure —
including a plain caller abort or the caller's own AbortSignal timeout
firing — as "[ProxyFetch] Proxy request failed ... fail-closed". A
client cancelling its own request is not a proxy transport failure and
shouldn't be misreported as one in ops logs/alerting; it still
propagates to the caller unchanged (fail-closed behavior is untouched).
Co-authored-by: TuyulSpam <287281626+TuyulSpam@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2589
* chore(changelog): fragment for #7266
---------
Co-authored-by: TuyulSpam <287281626+TuyulSpam@users.noreply.github.com>
* feat(db): include xp_audit_log in automatic retention/prune (#6801)
* fix(i18n): mirror retentionXpAuditLog into pt-BR.json (#6801)
pt.json and pt-BR.json are distinct files; the key landed only in pt.json,
so the i18n pt-BR integrity test (no drift, #6695) went red.
Fusion's panel-model extraction in combo.ts only recognized plain string
or {model: string} entries in combo.models; a {kind:"combo-ref", comboName}
step (a first-class, Zod-validated combo-step shape the dashboard already
lets you add to a fusion panel) had neither field, so it was silently
filtered out — no error, no warning, and an opaque 400 if it was the only
panel member.
A combo-ref panel member is now dispatched as one black-box panel voice
(a recursive handleComboChat call into the referenced combo, reusing the
same executeComboRefUnit + cycle/depth guards every other combo-ref-
consuming strategy already uses), not a fan-out of the referenced combo's
own targets.
New module open-sse/services/combo/fusionPanel.ts keeps the frozen
combo.ts god-file's growth minimal (extraction/dispatch-wrapper logic
lives there; the fusion branch itself only wires it in).
Add a per-connection cache capability override (supportsPromptCaching,
cacheControlPassthrough) stored in provider_specific_data.cache, consulted
first by providerSupportsCaching() / providerHonorsOpenAIFormatCacheControl()
before falling back to the hardcoded CACHING_PROVIDERS name sets. Unblocks
prompt_cache_key injection, the compression cache-aware guard, and
cache_control passthrough for openai-compatible-chat-<uuid>-style custom
connections that can never match the hardcoded provider-name sets. Default
(no override) is byte-identical to current behavior.
* fix(sse): project non-streaming JSON back to the Gemini/Antigravity envelope
The streaming and non-streaming response paths disagreed on how a response is
projected back into a non-OpenAI client's wire format.
Streaming goes through the translator registry, where the
FORMATS.OPENAI -> FORMATS.ANTIGRAVITY translator
(open-sse/translator/response/openai-to-antigravity.ts) projects each OpenAI
chunk into the `{ response: { candidates: [...] } }` envelope, mapping
tool_calls to `functionCall` parts and reasoning to `thought` parts.
The non-streaming path uses translateNonStreamingResponse() instead. Its
"Phase 3: translate back to client source format" step only special-cased
FORMATS.CLAUDE — every other non-OpenAI client format fell through and returned
the raw OpenAI chat.completion intermediate. A Gemini/Antigravity client issuing
a non-streaming request therefore received `choices[]`/`tool_calls` instead of
`candidates[]`/`functionCall`: the client's parser sees no candidates and the
function calls are effectively dropped, so tool-calling silently breaks on the
JSON path while working over SSE.
Adds convertOpenAINonStreamingToGeminiFamily() and wires it into Phase 3 for
FORMATS.GEMINI / FORMATS.ANTIGRAVITY, mirroring the shape the streaming
translator already emits so both paths agree. Tool-call `arguments` are parsed
through a non-throwing helper: a provider emitting truncated JSON degrades that
call's args to `{}` rather than raising an uncaught SyntaxError in the shared
response hot path (matching the streaming translator's behaviour).
Scoped deliberately narrow: only the Gemini-family projection gap proven by the
failing test is closed. The Ollama/Responses projections and the SSE terminal
tracker from the upstream change are not ported — OmniRoute has no OLLAMA format
in FORMATS, and its Responses/[DONE] handling already lives in
nonStreamingSse.ts + the registry.
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2348
* chore(changelog): fragment for #7255
---------
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
* fix(build): isolate Windows HOME/AppData during next build
next build's static-generation glob scan and framework cache helpers walk
%USERPROFILE%/AppData, which on GitHub-hosted Windows runners (and some
OneDrive-backed dev profiles) contains reparse points/junctions that raise
EPERM during Next's file-system scans. .github/workflows/electron-release.yml
already patches USERPROFILE for that one CI job ("Sanitize Windows home
directory" step), but a local `npm run build` on Windows — or any other
Windows CI path that calls scripts/build/build-next-isolated.mjs directly —
hits the same EPERM unprotected, and the existing CI patch does not touch
APPDATA/LOCALAPPDATA at all.
Folds the isolation into resolveNextBuildEnv() (the existing seam every
caller of build-next-isolated.mjs already goes through), rather than adding
a second build entrypoint the way upstream's scripts/build-app.js does:
on win32, HOME/USERPROFILE/APPDATA/LOCALAPPDATA are pointed at a fresh
per-process temp profile dir, created just-in-time via the new
ensureWindowsBuildProfileDirs() before spawning `next build`. Skipped when a
caller has already sandboxed the build via NEXT_DIST_DIR (the existing
signal this file reads for isolated-build callers, e.g. CLI packaging), so
nested build invocations are never double-isolated. Non-Windows behavior is
unchanged.
Co-authored-by: KunN21 <kunn21.nv@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2402
* chore(changelog): fragment for #7249
---------
Co-authored-by: KunN21 <kunn21.nv@gmail.com>
* fix(sse): reconstruct Claude-format content in synthetic bypass responses
handleBypassRequest() returns a canned response for CLI warmup/title-
extraction patterns without calling the provider. For Claude-format
clients (e.g. Claude Code CLI), the non-streaming path merged translated
SSE chunks by taking message_start.message as-is — but the
openai-to-claude translator always initializes that message with
content: [] and streams the actual text via separate
content_block_start/delta events. Every synthetic Claude-format
bypass response therefore silently returned empty content.
mergeChunksToResponse() now rebuilds the content array from
content_block_start/delta events (mirroring the streaming path) and
carries over stop_reason/stop_sequence from message_delta. Extracted
the response-builder helpers (createOpenAIResponse,
create{Non}StreamingResponse, mergeChunksToResponse) out of
bypassHandler.ts into a new open-sse/utils/bypassResponse.ts module so
this logic has a single owner instead of being duplicated inline.
Co-authored-by: KunN-21 <kunn21.nv@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2404
* chore(changelog): fragment for #7248
* refactor(sse): extract Claude chunk-merge helpers to fix complexity ratchet
mergeChunksToResponse() regressed both quality ratchets by +1
(complexity 2057>2056, cognitive 891>890). Split the Claude-format
reconstruction into buildClaudeContentBlocks(), applyClaudeMessageDelta()
and mergeClaudeChunks() — same behavior, verified by the existing
bypass-response-claude-merge.test.ts (4/4 passing unchanged).
---------
Co-authored-by: KunN-21 <kunn21.nv@gmail.com>
* fix(nvidia): expand NIM chat model catalog with newly-observed models
NVIDIA NIM's live catalog has added several chat-completions-capable models
since the registry was last swept (#6108): Llama 3.x/4 family, Mistral
variants, several Nemotron/Nemoguard safety and reasoning models, Qwen3-Next,
and a few smaller vendor models (Sarvam, Stockmark, Upstage). Adds them to
open-sse/config/providers/registry/nvidia/index.ts with supportsReasoning /
supportsVision flags where applicable.
minimaxai/minimax-m3 is intentionally NOT re-added — it stays excluded per
the #3329 guard (still 404s for most callers). Two non-chat entries from the
upstream sweep (nvidia/gliner-pii — an NER/PII tagger, and
google/diffusiongemma-26b-a4b-it — a diffusion model) are dropped: this
registry only models the /v1/chat/completions surface, and OmniRoute already
covers NVIDIA's embedding/ASR/TTS models separately in embeddingRegistry.ts
and audioRegistry.ts. Upstream's per-model `thinkingFormat` capability
override (a legacy open-sse/providers/capabilities.js concept) has no
OmniRoute equivalent — reasoning-param translation here is scoped per
PROVIDER (translator/paramSupport.ts, executors/default.ts), not per model,
so only the catalog needed porting.
Co-authored-by: baibiao <baibiaoxxl123@outlook.com>
Inspired-by: https://github.com/decolua/9router/pull/2373
* chore(changelog): fragment for #7247
---------
Co-authored-by: baibiao <baibiaoxxl123@outlook.com>
* feat(provider): add Chenzk API OpenAI-compatible gateway
Registers Chenzk (chenzk.top) as a new API-key gateway provider — an
OpenAI-compatible aggregator exposing GPT/Claude/DeepSeek/GLM model groups
behind one endpoint. Adapted to OmniRoute's directory-per-provider registry
(open-sse/config/providers/registry/) and metadata catalog
(src/shared/constants/providers/apikey/gateways.ts), following the same
passthrough-models pattern already used for kenari/x5lab/sumopod (live
/v1/models catalog resolves the model list instead of a hardcoded array).
Co-authored-by: Ahmad Putra Cahyo <CahyokPutraDev99@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2437
* chore(changelog): fragment for #7246
* test(provider): regen golden snapshot + bump family-count for Chenzk gateway
The Chenzk provider added in a616b88c9 registered a new APIKEY_PROVIDERS
entry (gateways.ts) but did not update the two characterization tests
that assert exact provider counts: the translate-path golden snapshot
(missing the chenzk entry) and the 167-entry family-merge count in
providers-constants-split.test.ts (now 168, verified as a strict
partition sum across the 6 family files, no loss/dup).
Co-authored-by: Ahmad Putra Cahyo <CahyokPutraDev99@users.noreply.github.com>
---------
Co-authored-by: Ahmad Putra Cahyo <CahyokPutraDev99@users.noreply.github.com>
* fix(sse): route the public OpenAI GPT-5.6 family through the Responses API
OpenAI's Chat Completions endpoint rejects GPT-5.6 requests that combine
function tools with an active reasoning_effort:
400 "Function tools with reasoning_effort are not supported for
<model> in /v1/chat/completions. Please use /v1/responses instead."
The openai (API-key) registry entries for gpt-5.6 / -sol / -terra / -luna
were missing the per-model `targetFormat` tag, so every request was posted
to /v1/chat/completions. Any agentic client sending tools + reasoning to
openai/gpt-5.6-sol hit the 400 and burned a combo fallback attempt.
OmniRoute already has the generic mechanism this needs — the same
per-model `targetFormat: "openai-responses"` override that routes
gpt-5.5-pro / gpt-5.4-pro (#5842). It drives BOTH the outbound URL
(DefaultExecutor.buildUrl → api.openai.com/v1/responses) and the body
translation (chatCore's resolveChatCoreTargetFormat → openai-responses).
Tagging GPT_5_6_API_CAPABILITIES is therefore the whole fix; no new
transport table or routing branch is required.
Scoped to the public OpenAI API catalog: the codex provider has its own
Responses transport and is untouched.
Co-authored-by: Sutarto Jordan Chrisfivo <Jordannst@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2547
* chore(changelog): fragment for #7242
---------
Co-authored-by: Sutarto Jordan Chrisfivo <Jordannst@users.noreply.github.com>
* feat(cli): add Grok Build CLI tool setup (~/.grok/config.toml)
Registers xAI's Grok Build TUI coding agent as a configurable CLI tool in
/dashboard/cli-code, so OmniRoute can write itself in as a custom model
provider in ~/.grok/config.toml.
Mechanism: Grok Build reads a TOML config that can hold several user-defined
[model.*] sections plus a [models].default pointer. Unlike the sibling Forge
handler (which owns its whole config file and can full-replace it), this one
surgically upserts ONLY the [model.omniroute] section and rewrites
[models].default, leaving every other section byte-intact. Apply records the
previous default in an `# omniroute-prev-default` marker comment so Reset can
restore the user's original default instead of guessing.
Built on OmniRoute's existing CLI-tools infrastructure rather than replaying
the upstream shape: getCliRuntimeStatus() for detection (no ad-hoc
`which grok` exec), Zod validation via cliModelConfigSchema, the write guard,
createBackup(), the cliToolState DB module, and sanitizeErrorMessage() for
every error path (Hard Rule #12).
Security: GET reaches getCliRuntimeStatus(), which spawns a child process to
locate and healthcheck the `grok` binary. That is the same transitive-spawn
surface that classified /api/skills/collect/, so the route is registered in
LOCAL_ONLY_API_PREFIXES and loopback-enforced before any auth check
(Hard Rules #15 + #17). Writing a local CLI's config file is inherently a
local-machine operation, so this costs no real capability.
Co-authored-by: rixzkiye <rizkiyemubarok05@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2571
* chore(changelog): fragment for #7241
* fix(cli): shrink cliTools.ts/cliRuntime.ts under the file-size ratchet + fix stale catalog counts
The grok-build registry/runtime entries pushed cliTools.ts (916->932) and
cliRuntime.ts (1128->1137) past their frozen file-size caps. Extract the
grok-build entries into cliToolsGrokBuild.ts (registry, typed) and
cliRuntimeGrokBuild.ts (runtime metadata, deliberately untyped/no
cliCatalog import so it doesn't drag that schema file into the
typecheck:core curated allowlist's transitive graph). The amp runtime
entry rides along in the same runtime file for the extra headroom needed
to clear cliRuntime.ts's cap with zero slack.
Also update the two catalog-cardinality canaries (cli-tools-schema.test.ts,
cli-catalog-counts.test.ts) and EXPECTED_CODE_COUNT to include grok-build:
20->21 visible code entries, 24->25 total code entries, 32->33 grand total.
Fixes CI reds on #7241 surviving a release/v3.8.49 merge: Fast Quality
Gates (check:file-size) and Unit Tests fast-path (1/4, 2/4).
* test(stryker): register grok-build route-guard test in tap.testFiles
check:mutation-test-coverage --strict flagged
tests/unit/route-guard-grok-build-settings-local-only.test.ts as a covering
unit test for src/server/authz/routeGuard.ts missing from
stryker.conf.json's tap.testFiles allowlist (only became reachable once the
Fast Quality Gates job got past the file-size fix earlier in this branch).
---------
Co-authored-by: rixzkiye <rizkiyemubarok05@gmail.com>
* feat(dashboard): add Type filter and easiest-first sort to Free Provider Rankings (#6915)
Adds sort/filter controls keyed on provider auth type (NOAUTH/OAUTH/APIKEY)
to /dashboard/free-provider-rankings so zero-setup providers can be
surfaced without eyeballing the Type column:
- Type filter chips (All / No Signup / OAuth Login / API Key), client-side
over the already-fetched rankings (no API change).
- "Easiest first" sort toggle groups NOAUTH < OAUTH < APIKEY while
preserving the existing score-descending order within each group
(stable sort).
- Type column legend/tooltip explaining what each auth type means.
- Pure filter/sort logic extracted to a new freeProviderRankingsAuthType.ts
module (no DB imports) rather than freeProviderRankings.ts, so importing
it from the "use client" page does not pull server-only DB wiring
(fs/path/better-sqlite3) into the client bundle; freeProviderRankings.ts
re-exports both for API/test parity.
- i18n keys added to en.json + __MISSING__ placeholders synced to all 42
locale mirrors (consistent with the existing untranslated-key convention).
* test(6915): move page test to tests/unit/ui so a runner actually collects it
The .test.tsx sat at tests/unit/ top-level, which no runner collects
(vitest ui filters on tests/unit/ui) — check:test-discovery flagged it as
a new orphan: the test never ran. Moved under tests/unit/ui/ and switched
the dynamic import to the @/ alias (the convention of its neighbours).
6/6 now pass under test:vitest:ui.
* fix(types): explicit unknown hop on the getProviderConnections cast (#6915)
The dashboard-typecheck gate (added by #7203, after this branch was cut)
scopes tsc to the src/app/(dashboard) import graph. This PR's page.tsx now
imports freeProviderRankingsAuthType, which type-imports freeProviderRankings
— dragging that module into the graph for the first time and surfacing its
pre-existing TS2352 (JsonRecord[] -> ConnectionState[] is a structural subset).
Fixed the cast rather than widening the frozen baseline.
* fix(api): bulk-add API keys no longer overwrite existing connections
createProviderConnection upserts apikey connections BY NAME (same provider +
auth_type "apikey" + same name updates the row in place, replacing its
apiKey/priority/testStatus instead of inserting). The bulk-add route
auto-names unnamed lines "Key 1", "Key 2", ... restarting from 1 on every
request, blind to names already saved for the provider — so re-running a
bulk paste against a provider that already had "Key 1" silently replaced it
instead of adding a new connection alongside it. The same collision could
also happen within one batch for two identical custom name|apiKey lines.
Add resolveBulkNameCollisions (src/shared/utils/bulkApiKeyParser.ts): gap-fills
the smallest free "<name> <n>" suffix against both existing connection names
and names already assigned earlier in the same batch, so a name is never
reused. Wire it into POST /api/providers/bulk before the create loop, fetching
existing apikey connection names via the existing getProviderConnections db
module (no raw SQL added to the route).
Co-authored-by: asynx6 <sahrulbeni656@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2587
* chore(changelog): fragment for #7234
---------
Co-authored-by: asynx6 <sahrulbeni656@gmail.com>
Codex Responses API strict mode forces every "optional" tool property into
`required`, so a model that intends to OMIT an optional enum property (no
declared `default`, e.g. Agent.isolation: enum["worktree","remote"]) must
still emit a concrete value. Neither of the two ops shipped in #6992
(drop-if-default, generalized drop-if-empty) can catch this: drop-if-default
needs a declared default (none exists); drop-if-empty needs an empty
string/array (the emitted value is non-empty).
Adds a paired request/response transform scoped strictly to
targetFormat === OPENAI_RESPONSES:
- Request side: injectOptionalEnumOmissionSentinel/-ForTools widen no-default
optional enum properties to accept `null` (OpenAI's own documented
nullable-union idiom for this exact strict-mode limitation).
- Response side: isDroppableNullEntry drops the key when the model emits
`null` for a non-required, schema-declared property, reusing the existing
#6992 toolSchemas plumbing (no new tracking structure needed).
* feat(providers): editable ComfyUI base-URL field + per-connection override for image/video/music generation (#6928)
Adds a shared resolveComfyUiBaseUrl() helper (open-sse/utils/comfyuiClient.ts) that
prefers a per-connection providerSpecificData.baseUrl override over the registry
default, wired through the comfyui dispatch branch in the image, video, and music
generation handlers plus a best-effort authType:"none" credential lookup in all
three /v1/{images,videos,music}/generations routes (never hard-fails when no
connection exists, so zero-config localhost users are unaffected).
Surfaces an editable base-URL field on the ComfyUI connection form by adding it to
CONFIGURABLE_BASE_URL_PROVIDERS / DEFAULT_PROVIDER_BASE_URLS /
getProviderBaseUrlPlaceholder in providerPageHelpers.ts, so Docker-network setups
(e.g. http://comfyui:8188) can be configured the same way self-hosted chat
providers are.
Closes#6928
* refactor(6928): extract local-override credential lookup in media routes
The inline per-connection override block nested if>if inside the music and
videos POST handlers, taking each to cognitive complexity 16 (>15) — two NEW
violations that broke check:complexity-ratchets (892 > baseline 890).
Extracted resolveLocalOverrideCredentials() in both routes; behaviour is
unchanged. cognitive-complexity back to 890 = baseline.
* feat(dashboard): replace free-text model inputs with hidePaid-aware Selects (#6540)
Swap RoutingTab.webSearchRouteModel, ComboDefaultsTab.handoffModel, and
BackgroundDegradationTab's from/to fields from free-text inputs to a new
shared ModelSelectField fed by the already hidePaidModels-aware
GET /api/models, with an off-catalog "(custom)" fallback so an existing
saved value is never silently dropped. ModelRoutingSection's glob pattern
field gets a fail-open "matches only paid models" warning instead, since
it's a wildcard matcher rather than a single model id.
Adds save-time paid-target rejection (PAID_MODEL_TARGET_BLOCKED, 400) on
PATCH /api/settings, PATCH /api/settings/combo-defaults, and PUT
/api/settings/background-degradation when hidePaidModels is on, failing
open for aliases/combo names/unrecognized providers.
globToRegex is extracted from lib/db/modelComboMappings.ts into a new
dependency-free shared/utils/globPattern.ts so both the DB module and the
new client-side pattern heuristic reuse the same regex-building logic.
* refactor(6540): extract paid-target guard in background-degradation PUT
The inline hidePaid check nested if>if>for>if inside the PUT handler,
pushing its cognitive complexity to 16 (>15) — a NEW violation that broke
the check:complexity-ratchets gate (891 > baseline 890). Extracted the
check into a module-local hasBlockedPaidTarget() helper; behaviour and
response body are unchanged. cognitive-complexity back to 890 = baseline.
* fix(i18n): mirror paidModelPatternWarning into pt-BR.json (#6540)
The new key landed only in en.json; the i18n pt-BR integrity test
(no drift, #6695) requires pt-BR.json to carry every en.json key.
* feat(mitm): add Antigravity reasoning-effort overrides
The Antigravity MITM alias mapping only ever swapped the destination model;
there was no way to override the reasoning effort Antigravity's own
thinkingConfig requested. Alias entries are now `{ model?, reasoningEffort? }`
(a legacy plain-string mapping still normalizes to `{ model }`, so no DB
migration is required). The standalone proxy (server.cjs) forwards the chosen
tier as a top-level `reasoningEffortOverride` on the intercepted request; the
antigravity->openai translator honors it ahead of its thinkingConfig-derived
guess, and an explicit "none" suppresses reasoning_effort entirely even when
Antigravity's own request asked for thinking. Reuses the existing canonical
5-tier reasoning vocabulary (`@/shared/reasoning/effortStandardization.ts`,
with max/extra aliasing to xhigh) instead of introducing a new one. The API
route validates the reasoning-effort value at the boundary and the Antigravity
tool card UI now exposes a per-model reasoning-effort selector alongside the
existing model-mapping input.
Co-authored-by: Truong Fiu <gnourtf@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2584
* chore(changelog): fragment for #7228
---------
Co-authored-by: Truong Fiu <gnourtf@gmail.com>
* feat(sse): route GitHub Copilot Claude models through native /v1/messages
GitHub Copilot's /chat/completions and /responses endpoints never surface
prompt-cache token counts (cached_tokens) for Claude models, and round-tripping
Claude tool_use/tool_result/thinking content blocks through the OpenAI shape
is lossy. Copilot also exposes an Anthropic-native /v1/messages shim that
reports cached_tokens correctly and accepts native content blocks as-is.
Tag each github registry claude-* model with targetFormat: "claude" so
chatCore.ts translates the request to Anthropic-native shape before the
executor ever sees it (the same mechanism opencode/zen's Qwen entries and
opencode/go already use), and teach the github executor's buildUrl() /
buildHeaders() to dispatch those models at the new messagesUrl
(api.githubcopilot.com/v1/messages) with the required anthropic-version
header. transformRequest() now skips its /chat/completions-only quirks
(content-part flattening, trailing-assistant-prefill drop, the
response_format-as-system-prompt workaround) for the native path — the first
would destroy native tool_use/tool_result blocks, the prefill drop is
unnecessary because the real Anthropic API supports assistant prefill, and
the response_format workaround is superseded by the generic openai-to-claude
translator's own JSON-mode handling.
Co-authored-by: luoyide <ydhome.code@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2608
* chore(changelog): fragment for #7223
* fix(sse): green PR #7223 CI — complexity ratchet + stale test expectations
- Extract applyChatCompletionsOnlyQuirks() and resolveInitiatorHeader()
out of GithubExecutor.transformRequest()/buildHeaders() so the two
methods drop back under the complexity/cognitive-complexity ratchets
(2058/891 -> 2056/890, matching the frozen baseline). No behavior
change — same guards, just relocated.
- Update 4 pre-existing unit tests that hard-coded now-native claude-*
Copilot ids (claude-sonnet-4.5/4.6) to exercise the /chat/completions
legacy path via an unregistered id (claude-sonnet-4), matching the
sibling test already using that pattern. These ids now intentionally
route to the native /v1/messages shim added by this PR, which
correctly skips the /chat/completions-only workarounds these tests
were built to verify — the native path's own coverage lives in
github-copilot-claude-native-messages.test.ts.
- Split the routing invariant test (copilot-gemini-claude-route-no-responses.test.ts)
into a Claude case (expects /v1/messages) and a Gemini case (still
expects /chat/completions), reflecting the intentional routing change.
---------
Co-authored-by: luoyide <ydhome.code@gmail.com>
Extracts the routing-combo compression-mode dropdown (Default/Off/Lite/
Standard/Aggressive/Ultra) from the combo card into a shared
ComboCompressionModeSelect component, reused on both the combo card
(compact) and the Compression Combos page's "Assign to routing" list
under Context & Cache. Both surfaces persist through the existing
PUT /api/combos/{id} route -- no backend or schema change.
* feat(dashboard): add reorder-by-availability button to provider connections
Adds a "Reorder" action to the provider detail Connections toolbar that
sorts a provider's connections so available ones float to the top and
unavailable ones sink to the bottom, then persists the new order via the
existing per-connection priority PUT endpoint (same pattern already used
by handleSwapPriority).
Availability is computed with OmniRoute's own resilience model rather than
upstream's `modelLock_*` convention: a connection counts as available when
its effective status (testStatus, adjusted for the lazy connection-cooldown
window via rateLimitedUntil) is active/success — mirroring the exact logic
ConnectionRow already uses for its status badge, so the button and the row
badges never disagree. The sort is a stable Array.prototype.sort, so
connections keep their relative order within each availability group.
New pure helpers (sortConnectionsByAvailability, isConnectionAvailable,
getConnectionEffectiveStatus) live in connectionRowHelpers.ts and are
covered by a dedicated unit test, including the cooldown-lazy-recovery
edge case. i18n keys added to all 43 locales.
Co-authored-by: Fazril Syaveral Hillaby <fazriloke18@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2558
* chore(changelog): fragment for #7211
* fix(dashboard): extract reorder-by-availability into its own hook (file-size ratchet)
The reorder-by-availability feature pushed useProviderConnections.ts to 974
lines, past its frozen file-size cap (954). Extract the handler + its state
into a dedicated useReorderByAvailability hook, following the same pattern
already used for useModelVisibilityHandlers/useModelImportHandlers — no
behavior change, same tests still cover the sort logic in
connectionRowHelpers.ts.
* fix(dashboard): type the reorder hook's notifier explicitly (dashboard-typecheck TS2339)
ReturnType<typeof useNotificationStore> resolves to unknown under the
dashboard-scoped tsconfig gate (#7203), so notify.error tripped TS2339.
The hook only needs error(), so declare that minimal surface directly.
---------
Co-authored-by: Fazril Syaveral Hillaby <fazriloke18@gmail.com>
* feat(dashboard): show Codex plan label in provider and quota views
ConnectionRow on the provider-detail page never surfaced the Codex
subscription plan captured at OAuth import time
(providerSpecificData.chatgptPlanType, src/lib/oauth/services/codexImport.ts)
anywhere in the row UI. Added a small pure helper, getCodexPlanLabel, and a
Badge in ConnectionRow gated on isCodex.
Separately, the quota view's plan-badge machinery (resolvePlanValue /
tierByConnection / QuotaCardHeader) already existed for all providers, but
its persisted-metadata fallback list omitted chatgptPlanType. When the live
Codex usage endpoint has no plan_type/planType field, the usage service
reports the literal string "unknown" (open-sse/services/usage/codex.ts),
which resolvePlanValue's normalizePlanCandidate() filters out — so the quota
badge fell through to "Unknown" instead of the plan captured at login.
Added chatgptPlanType to the persisted candidate list.
Co-authored-by: Carmelo Campos <carmelogunsroses@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2570
* chore(changelog): fragment for #7210
* fix(dashboard): extract getCodexPlanLabel to unfreeze providerPageHelpers.ts
The Fast Quality Gates file-size ratchet froze
providerPageHelpers.ts at 1053 lines; adding getCodexPlanLabel
inline pushed it to 1067. Move the self-contained helper into its
own codexPlanLabel.ts module instead of growing the frozen file,
and repoint ConnectionRow.tsx + the regression test at the new
location. No behavior change.
---------
Co-authored-by: Carmelo Campos <carmelogunsroses@gmail.com>
* feat(kiro): register GPT-5.6 Sol/Terra/Luna model family
Kiro announced its first OpenAI-family models on 2026-07-14
(kiro.dev/changelog/models): GPT-5.6 Sol (flagship), Terra (balanced
mid-tier) and Luna (fastest/cheapest), all sharing a 272k context
window. Registers the three base model ids in the kiro provider
registry with contextLength/maxOutputTokens so getResolvedModelCapabilities()
resolves the real 272k window instead of falling back to the generic
default. OmniRoute derives the thinking/agentic synthetic variants and
per-account rate multipliers dynamically at discovery time
(open-sse/services/kiroModels.ts), so only the three base entries need
static registration here.
Co-authored-by: Edison42 <gn00742754@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2596
* chore(changelog): fragment for #7209
* fix(kiro): add GPT-5.6 Sol/Terra/Luna pricing rows
The registry additions in this PR exposed three new Kiro model ids
without matching pricing rows, tripping the catalog invariant that
every Kiro registry model must resolve a non-zero pricing row
(tests/unit/catalog-updates-v3x.test.ts) — the models would have
billed at $0.00.
Reuses the shared GPT_5_6_{SOL,TERRA,LUNA}_PRICING tiers already used
by the codex and openai aliases.
---------
Co-authored-by: Edison42 <gn00742754@gmail.com>
* fix(cli): fast-path --version to skip full CLI bootstrap
`omniroute --version` ran the entire CLI bootstrap before printing the
version: the tsx/esm + polyfill imports, env-file loading, and
Commander's ~70-command registration (importing DB, providers, OAuth,
and other heavy modules). That took ~1.5s just to print a version
string.
Add isVersionFastPath() (bin/cli/utils/versionFastPath.mjs) and check it
at the very top of bin/omniroute.mjs, before any of that work runs. It
only trips for an unambiguous bare `--version`/`-V` invocation (no other
args), so it never changes behavior for real commands or for `--help`
(whose output is generated dynamically from every registered
subcommand, so it still needs full registration and is deliberately not
fast-pathed).
`--version` now returns in ~0.3s instead of ~1.5s locally.
Co-authored-by: Sutarto Jordan Chrisfivo <Jordannst@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2414
* chore(changelog): fragment for #7208
* fix(build): enforce bin/cli/utils/versionFastPath.mjs in the pack-artifact gate
bin/omniroute.mjs now imports ./cli/utils/versionFastPath.mjs on its boot path
(the --version fast-path). bin/cli/ is only an allowlist PREFIX, so the file
vanishing from the npm tarball would never fail the unexpected-paths check --
only PACK_ARTIFACT_REQUIRED_PATHS makes its absence loud (#7065 class).
Adds the required path and updates the hardcoded expectation in
pack-artifact-policy.test.ts, matching the existing data-dir.mjs /
storageKeyProvision.mjs entries. Fixes the red in
tests/unit/pack-artifact-entrypoint-closures.test.ts, which derives the
requirement from the entrypoint's own imports.
---------
Co-authored-by: Sutarto Jordan Chrisfivo <Jordannst@users.noreply.github.com>
* fix(translator): register openai response projection for gemini clients
The response-translator registry had an OpenAI -> Antigravity response
projection registered, but no OpenAI -> Gemini one. When a client request is
detected as Gemini format (body-shape match on `contents: [...]`, per
detectFormat()) and combo routing lands the request on an OpenAI-native
provider, translateResponse() fell through its hub-and-spoke path with no
`openai -> gemini` translator registered, so the raw OpenAI
`chat.completion.chunk` shape reached the client unchanged instead of the
shared Gemini `response.candidates[]` envelope.
Registers FORMATS.OPENAI -> FORMATS.GEMINI reusing the existing
openaiToAntigravityResponse projection — Gemini and Antigravity already
share the same wrapped `{ response: { candidates: [...] } }` envelope
elsewhere in the pipeline (see the unwrapGeminiChunk callers in
open-sse/utils/stream.ts, which treat FORMATS.GEMINI and FORMATS.ANTIGRAVITY
identically), so no new conversion logic is introduced.
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2399
* chore(changelog): fragment for #7207
---------
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
* fix(translator): preserve Gemini thought parts as reasoning_content on the OpenAI request bridge
Gemini thinking-mode output marks internal reasoning with `part.thought === true`
inside a content's `parts` array. geminiToOpenAIRequest() ran every part (thought
or not) through the same text-part branch, so a thought part was merged straight
into the message's visible `content` — leaking private reasoning into whatever the
OpenAI pivot forwarded downstream, and hiding it from Reasoning Replay Cache (which
only ever inspects `reasoning_content`).
Add convertGeminiContentWithReasoning(): split out `thought: true` parts before
delegating to the existing convertGeminiContent(), then re-attach the joined
thought text as `reasoning_content` on the resulting message (skipping tool/
functionResponse messages, whose schema has no such field). Non-strict-provider
stripping and reasoning-replay injection in translator/index.ts are untouched —
this only fixes what reasoning_content gets populated with on this one inbound
hop.
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2401
* chore(changelog): fragment for #7206
---------
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
tryAwsSsoCache() only resolved clientId/clientSecret via data.clientIdHash -> <hash>.json. Newer kiro-auth-token.json files instead carry a top-level clientId directly, so that lookup silently failed and left clientId/clientSecret null, sending the dashboard's Import Token POST down the non-IDC path. That path (KiroService.validateImportToken -> readCachedClientCredentials) picked a client registration by region + latest-expiry across ALL cached SSO client registrations, ignoring the token's actual clientId — on a machine with multiple stale registrations this returned a mismatched clientId/clientSecret pair, producing 'Bad credentials' on refresh.
Fix: resolve clientId/clientSecret by scanning the cache for a registration file whose own clientId matches the token's clientId (falling back to clientIdHash first, then a direct-match scan), and thread an optional clientId hint into readCachedClientCredentials()/validateImportToken() so an exact match always wins over the region/latest-expiry heuristic.
Reported-by: Asher (@XCrag) (https://github.com/decolua/9router/issues/1253)
* fix(providers): add MiniMax image-generation provider (port from 9router#2482)
MiniMax already had entries in the music/audio/video registries, but no entry at all in imageRegistry.ts and no dedicated provider handler under open-sse/handlers/imageGeneration/providers/. A MiniMax image-model request therefore fell through the format dispatch in imageGeneration.ts to a 404/unmatched-format response instead of reaching MiniMax's synchronous image_generation endpoint.
Registers a minimax image provider (format: minimax-image, models image-01/image-01-live) and a new handleMinimaxImageGeneration handler that POSTs to https://api.minimax.io/v1/image_generation and normalizes data.image_urls into the OpenAI-compatible images payload.
Reported-by: felipeleite (https://github.com/decolua/9router/issues/2482)
* refactor(providers): split KIE image catalog out of imageRegistry to respect file-size cap
imageRegistry.ts hit 805 lines after adding the MiniMax image provider (cap is
800). Extract the KIE image-model catalog (largest single provider entry,
~35 models) into its own semantic-family module,
providers/registry/kie/imageModels.ts, following the same pattern already
used for LMARENA_DIRECT_IMAGE_MODELS. imageRegistry.ts now imports
KIE_IMAGE_MODELS instead of inlining the list.
Also update minimax-media-servicekinds.test.ts: getRegistryMediaKinds derives
membership by design from every registry in MEDIA_KIND_REGISTRIES, including
IMAGE_PROVIDERS. Now that minimax is a key in IMAGE_PROVIDERS, it correctly
gains the "image" kind alongside tts/video/music — the same behavior already
asserted for openai in this file. The exact-match assertion is updated to
["image","music","tts","video"]; the other assertions (which only check
.includes for tts/video/music/llm) were already correct and untouched.
* fix(providers): extract minimax image-gen helpers to fix complexity ratchet
check:complexity-ratchets regressed 2056 -> 2058 (handleMinimaxImageGeneration:
complexity 25, max-lines-per-function 97). Split logging, upstream-error,
no-images, success and fetch-error branches into small named helpers so the
handler stays within the cyclomatic-complexity (15) and max-lines-per-function
(80) ratchets. No behavior change; existing minimax-image-provider-2482 and
minimax-media-servicekinds unit tests still pass.