Commit Graph

2174 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
fb612867fd feat: add Segmind image+video provider (#6656) (#7608)
* feat(providers): add Segmind image+video provider (#6656)

Segmind exposes 200+ hosted image/video models under a single
`POST https://api.segmind.com/v1/{model}` REST shape: x-api-key auth,
JSON request body, raw media bytes response (no JSON envelope).

- New IMAGE_PROVIDERS + VIDEO_PROVIDERS registry entries (format:
  "segmind") with a curated starter model list (Flux, SDXL, SD3.5,
  Kandinsky for image; Wan, Hunyuan, LTX, Kling for video).
- New connection-metadata entry in specialty-media.ts; segmind added
  to IMAGE_ONLY_PROVIDER_IDS and VIDEO_PROVIDER_IDS.
- Dedicated handlers (imageGeneration/providers/segmind.ts,
  videoGeneration/providers/segmind.ts) built on a shared REST client
  (utils/segmindClient.ts) that centralizes the fetch/error/log path
  so both stay under the complexity/max-lines ratchets.
- Extracted the pre-existing Alibaba DashScope video handler out of
  the frozen videoGeneration.ts into videoGeneration/providers/
  dashscope.ts (no behavior change) to make room for the new Segmind
  dispatch branch under the frozen file-size baseline.
- Error responses route through sanitizeErrorMessage() (Hard Rule
  #12) — verified by dedicated no-leak tests.
- Regenerated docs/reference/PROVIDER_REFERENCE.md (250 -> 251
  providers) and synced the plain-text provider counts in README.md/
  AGENTS.md/CLAUDE.md (anchors/badges left untouched).

Tests: tests/unit/segmind-image-video-provider-6656.test.ts (11
cases — registry shape, connection metadata, IMAGE_ONLY/VIDEO_
PROVIDER_IDS membership, mocked-fetch request mapping for both
image and video, and sanitized-error-path assertions for both
upstream error bodies and network exceptions). No live Segmind key
required; response shape (raw media bytes, x-api-key auth) is
sourced from https://docs.segmind.com/ and corroborated against
https://www.segmind.com/models/flux-schnell/api,
https://www.segmind.com/models/sdxl1.0-txt2img/api, and
https://www.segmind.com/models/wan2.1-t2v/api.

Gates run clean: check-file-size, check:complexity-ratchets
(2055/889, both under baseline), typecheck:core,
typecheck:noimplicit:core (no new errors), lint (targeted files),
check:cycles, check:docs-counts (STRICT provider-count drift
resolved), check:docs-sync, check:any-budget:t11,
check:tracked-artifacts, check:provider-consistency,
check:known-symbols.

* test(providers): align APIKEY_PROVIDERS count 167→168 for the new segmind provider (#6656)

Adding segmind to specialty-media.ts grows APIKEY_PROVIDERS by one;
providers-constants-split.test.ts hardcodes the family-partition total.
Legitimate count alignment, not a weakened assertion — all 4 partition/
dedup checks still enforced.
2026-07-17 12:13:47 -03:00
Diego Rodrigues de Sa e Souza
5bacb719d3 feat: add Microsoft Designer as image provider (#6672) (#7609)
* feat(sse): add Microsoft Designer as image provider (#6672)

Adds `microsoft-designer-web` — an unofficial, reverse-engineered
Bearer-token web-session image provider, modeled on the existing
`chatgpt-web`/`copilot-m365-web` "-web" provider category.

- Registers the provider in WEB_COOKIE_PROVIDERS (src/shared/constants/
  providers/web-cookie.ts) and IMAGE_PROVIDERS (open-sse/config/
  imageRegistry.ts, new "designer-web" format).
- New handler open-sse/handlers/imageGeneration/providers/designerWeb.ts
  implements the submit-then-poll DallE.ashx flow (Bearer access_token +
  ClientId/SessionId/UserId headers -> form POST -> poll for
  image_urls_thumbnail), wired into handleImageGeneration()'s dispatch.
- The upstream ClientId header is a fixed, publicly-shared value (not a
  secret) — routed through resolvePublicCred() per Hard Rule #11, never
  as a string literal.
- Registers the token-based credential requirement in
  webSessionCredentials.ts so the provider-connect UI asks for the
  right field; connection validation falls back to the existing generic
  web-cookie session-ping validator (no dedicated validator needed).
- Extracted the KIE image-model catalog into a co-located
  open-sse/config/providers/registry/kie/models.ts module (mirrors the
  existing lmarena/directModels.ts pattern) to keep imageRegistry.ts
  under the file-size cap while adding the new provider entry.
- Regenerated docs/reference/PROVIDER_REFERENCE.md (250 -> 251
  providers) and updated the plain-text counts in README.md, AGENTS.md,
  CLAUDE.md.

Tests (tests/unit/microsoft-designer-web-6672.test.ts, 16 cases):
registry-entry shape assertions, the resolvePublicCred() shape
assertion (Hard Rule #11), and the pure header/form-body/response-
parsing helpers plus the handler's submit/poll/error/timeout paths
against a mocked fetch — no live Designer session required.

Reverse-engineered from the g4f MicrosoftDesigner.py provider reference
(researched during #6672 triage); the exact upstream response shape has
not been validated against a live Designer session, so the poll-loop
implementation follows the documented g4f contract as closely as
possible without a live capture.

* fix(providers): satisfy web-cookie executor contract + document designer-web env vars (#6672)
2026-07-17 12:09:12 -03:00
Diego Rodrigues de Sa e Souza
93e217e763 feat(video): add Novita AI as video-generation provider (#6658) (#7606)
Adds Novita AI to the video-generation subsystem (VIDEO_PROVIDERS), alongside
its existing text/chat gateway registration. Novita's async video APIs are
per-model (POST /v3/async/<model-slug>, e.g. wan-t2v, kling-v1.6-t2v) sharing
one poll endpoint (GET /v3/async/task-result?task_id=...) — confirmed against
Novita's published API reference. Seeds Wan 2.1 T2V and Kling V1.6 T2V models;
reuses the stored novita provider Bearer apiKey (no separate credential flow).

To stay under the frozen videoGeneration.ts file-size cap, extracted the
existing Alibaba/DashScope handler into a co-located sibling module
(videoGeneration/dashscopeHandler.ts) alongside the new Novita handler
(videoGeneration/novitaHandler.ts) and its pure helpers (videoGeneration/novita.ts).

Also tags novita in VIDEO_PROVIDER_IDS (src/shared/constants/providers.ts) so
it surfaces as a video-capable provider in PROVIDER_REFERENCE.md and A2A
provider-discovery, and regenerates the provider reference doc.

Tests: tests/unit/video-novita-6658.test.ts (18 cases) covering registry
shape, pure helpers (URL building, param normalization, task-id/result
parsing), and full handler wiring (submit->poll->mp4, missing credentials,
missing task_id, task FAILED, task timeout).
2026-07-17 11:54:56 -03:00
Diego Rodrigues de Sa e Souza
76c3b3b8d3 feat: add Freepik (Magnific Mystic) image generation provider (#6654) (#7597)
* feat(providers): add Freepik (Magnific Mystic) image generation provider (#6654)

Adds an official, API-key-based Freepik image-gen provider using the Mystic
endpoint (POST /v1/ai/mystic -> async task_id -> GET /v1/ai/mystic/{id}
polling), modeled on the existing leonardo.ts generationId adapter pattern.

- open-sse/config/providers/registry/freepik/index.ts: new registry module
  (kept separate to avoid pushing the frozen imageRegistry.ts over the
  file-size cap) with the 6 real Mystic style models (realism, fluid, zen,
  flexible, super_real, editorial_portraits) — not the "Flux/Imagen3" list
  from the original feature request, which independent research showed was
  stale.
- open-sse/handlers/imageGeneration/providers/freepik.ts: submit+poll
  adapter; all error paths route through sanitizeErrorMessage() (Hard Rule
  #12), configurable poll interval/timeout via body.poll_interval_ms /
  poll_timeout_ms for fast, deterministic tests.
- Registered in providers.ts (IMAGE_ONLY_PROVIDER_IDS) and
  apikey/specialty-media.ts (catalog metadata), with the corrected free-tier
  note (one-time ~€5 credit, not a recurring "100/month" allotment).

Drops the "100 free credits/month" and "Flux/Imagen3 selectable models"
claims from the original issue - verification showed the free tier is a
one-time ~€5 API credit and Imagen 3 only underlies the `fluid` style, not a
separately selectable model. Domain: api.freepik.com is still live as of
this writing despite Freepik's April-2026 API-docs rebrand to Magnific
(docs.freepik.com -> docs.magnific.com); noted inline for future
re-verification.

Closes #6654

* test: align APIKEY_PROVIDERS count to 171 after freepik + release merge (#7597)
2026-07-17 11:49:50 -03:00
Diego Rodrigues de Sa e Souza
9b5415a414 feat: add Gladia as an async speech-to-text provider (#6657) (#7603)
* feat(providers): add Gladia as an async speech-to-text provider (#6657)

Adds Gladia's async pre-recorded transcription API (upload → POST
/v2/pre-recorded → poll result_url) following the existing
AssemblyAI/Kie.ai async-STT pattern:

- New `gladia` entry in AUDIO_TRANSCRIPTION_PROVIDERS
  (open-sse/config/audioRegistry.ts), authenticated via the
  `x-gladia-key` custom header.
- New `handleGladiaTranscription()` handler
  (open-sse/handlers/audioTranscription.ts) wired into the
  format dispatch table.
- New `x-gladia-key` case in `buildAuthHeaders()`
  (open-sse/config/registryUtils.ts).
- Registered `gladia` in AUDIO_ONLY_PROVIDERS
  (src/shared/constants/providers/audio.ts) so it appears in the
  auto-generated provider catalog; regenerated
  docs/reference/PROVIDER_REFERENCE.md (250 -> 251 providers) and
  synced the plain-text provider counts in README.md, AGENTS.md,
  and CLAUDE.md.

Real-time/streaming transcription is explicitly out of scope for
this change — OmniRoute has no WebSocket audio-ingestion layer
today; only the async/pre-recorded path (which covers every other
async STT provider already wired in) is implemented.

Tests: 5 new node:test cases in
tests/unit/audio-transcription-handler.test.ts covering the
upload→submit→poll happy path, a terminal Gladia error, and a
missing result_url guard, plus a buildAuthHeaders case in
tests/unit/registry-utils.test.ts for the new x-gladia-key header.

* chore(providers): sync provider counts to 253 + fix base-red APIKEY partition count 168→169 (#6657)

Rebasing gladia onto the advanced release surfaced two count drifts the
RUN_ALL suite trips on: (1) docs provider count is now 253 (multiple
providers merged since this branch was cut); (2) providers-constants-split
already expects 168 but the release has 169 APIKEY entries — a pre-existing
base-red from an earlier provider merge that didn't update the test. Gladia
is STT (adds no APIKEY entry), so 169 is the correct value; aligning it here
also un-reds the release. All 4 partition/dedup checks still enforced.
2026-07-17 11:45:00 -03:00
Diego Rodrigues de Sa e Souza
7eb0204901 feat: add FreeTheAi as OpenAI-compatible gateway provider (#6670) (#7602)
* feat(providers): add FreeTheAi as an OpenAI-compatible gateway provider (#6670)

FreeTheAi is a free-tier, Discord-signup gateway aggregator — same shape
as hackclub/chutes: OpenAI-compatible chat/completions + /v1/models
discovery, no custom executor/translator needed.

- Registry entry: open-sse/config/providers/registry/freetheai/index.ts
  (format: openai, executor: default, apikey/bearer auth, passthroughModels)
- Provider metadata: src/shared/constants/providers/apikey/gateways.ts
- Listed in AGGREGATOR_PROVIDER_IDS (src/shared/constants/providers.ts)
- Unit test verifying registry entry, getExecutor() resolution, aggregator
  classification, and provider metadata (tests/unit/provider-registry-freetheai.test.ts)

* chore(providers): sync counts (APIKEY 170, providers 253) after rebase onto advanced release (#6670)

The release advanced heavily since this branch was cut; realign the
family-partition count to the true post-rebase value (170) and the doc
provider totals to 253. freetheai adds exactly one gateway; the rest of
the delta is pre-existing release drift. All partition/dedup checks enforced.
2026-07-17 11:36:58 -03:00
Diego Rodrigues de Sa e Souza
6695cbbf7a feat: add EdgeTTS audio-tts provider (#6668) (#7605)
* feat(sse): add EdgeTTS audio-tts provider (#6668)

Registers Microsoft Edge "Read Aloud" as a new no-API-key AUDIO_SPEECH_PROVIDERS
entry — the first WebSocket-transport TTS provider in the registry. Reverse-
engineered/unofficial endpoint, same class of integration already accepted for
other "-web" style providers (chatgpt-web.ts, copilot-web.ts).

- open-sse/executors/edgeTts.ts: pure Sec-MS-GEC token construction (SHA-256
  over a public trusted-client-token + rounded Windows file-time ticks, ported
  from rany2/edge-tts drm.py), WS message framing (speech.config/ssml),
  binary-chunk demuxing, SSML building/escaping, and the WS synth call itself
  (injectable WebSocket ctor for tests, lazy `import("ws")` in production so
  it never enters esbuild's top-level CJS bundle graph). Per-client-IP
  sliding-window throttle (SlidingWindowLimiter) since there's no per-user key
  — one abusive deployment could otherwise get the shared trusted token
  rate-limited for everyone.
- open-sse/utils/publicCreds.ts: embeds the trusted-client-token via
  resolvePublicCred() (Hard Rule #11) — it's a constant hardcoded in every
  Edge build and every open-source edge-tts port, not a per-user secret.
- Extracted open-sse/utils/audioResponse.ts (shared response helpers) and
  open-sse/executors/awsPollyTts.ts (AWS Polly handler) out of
  open-sse/handlers/audioSpeech.ts to stay under its frozen file-size ratchet
  baseline while making room for the new branch — no behavior change to
  either extracted piece.
- src/app/api/v1/audio/speech/route.ts: thread the caller's IP through to the
  handler for the new throttle.

Tests: tests/unit/edgetts-provider.test.ts (23 cases) — Sec-MS-GEC determinism
and cross-check against a hand-derived reference vector, message framing,
binary demux, SSML escaping/injection-safety, registry lookup, publicCreds
shape, and the error path via an injected fake WebSocket (upstream failure ->
sanitized 502, no stack/path leak; Hard Rule #12), plus the per-IP rate limit.
No live upstream is required or used — the reverse-engineered protocol can't
be validated against real credentials, but every pure/testable seam is
covered per the TDD path in the bug/feature validation gate.

* test(mutation): register edgetts-provider.test.ts in stryker tap.testFiles (#6668)

The new provider's unit test covers a mutated module, so the strict
mutation-test-coverage gate requires it in stryker.conf.json's
tap.testFiles. Single-line addition (kept the file's existing formatting).
2026-07-17 11:32:25 -03:00
Diego Rodrigues de Sa e Souza
df1ed57876 feat(sse): add Notion AI Web (Unofficial/Experimental) provider (#6758) (#7600)
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.
2026-07-17 11:32:09 -03:00
Diego Rodrigues de Sa e Souza
7b564ab5db feat(providers): add Felo chat-aggregator provider (#6666) (#7599)
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.
2026-07-17 11:31:54 -03:00
Diego Rodrigues de Sa e Souza
a6d19cc4ba feat(providers): add Rev AI speech-to-text provider (#6655) (#7596)
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.
2026-07-17 11:31:31 -03:00
Diego Rodrigues de Sa e Souza
873e3da62e feat(dashboard): add 180D and 365D usage/cost analytics periods (#7213) (#7213)
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
2026-07-17 11:03:13 -03:00
Diego Rodrigues de Sa e Souza
ad7692b3d3 feat(quota): opt-in auto-ping to keep Codex quota windows warm (#6977) (#6995)
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)
2026-07-17 10:54:56 -03:00
Diego Rodrigues de Sa e Souza
32dd3a8249 fix(dashboard): include never-tested connections in combo builder active-provider list (port from 9router#2057) (#7118)
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)
2026-07-17 10:50:25 -03:00
Diego Rodrigues de Sa e Souza
21d5acbb40 feat(api): accept x-goog-api-key header for client-facing auth (#7034) (#7236)
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
2026-07-17 10:48:14 -03:00
Diego Rodrigues de Sa e Souza
50c2d632eb feat: add Mixedbread AI as embeddings provider (#6660) (#7595)
* 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.
2026-07-17 10:43:25 -03:00
Diego Rodrigues de Sa e Souza
6bb3207912 feat(api): structured X-Routing-Fallback-Reason header for relay routing (#6872) (#7262) 2026-07-17 10:41:30 -03:00
Diego Rodrigues de Sa e Souza
29bb59e18d feat(db): include xp_audit_log in automatic retention/prune (#6801) (#7260)
* 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.
2026-07-17 10:41:17 -03:00
Diego Rodrigues de Sa e Souza
6c8392fa45 feat(providers): let custom connections opt into prompt-cache capability (#6880) (#7257)
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.
2026-07-17 10:41:09 -03:00
Diego Rodrigues de Sa e Souza
ea32dcf863 feat(provider): add Chenzk API OpenAI-compatible gateway (#7246)
* 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>
2026-07-17 10:40:49 -03:00
Diego Rodrigues de Sa e Souza
624aba2498 feat(cli): add Grok Build CLI tool setup (~/.grok/config.toml) (#7241)
* 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>
2026-07-17 10:40:41 -03:00
Diego Rodrigues de Sa e Souza
3cd04afe62 feat: add Type filter and easiest-first sort to Free Provider Rankings (#6915) (#7240)
* 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.
2026-07-17 10:40:37 -03:00
Diego Rodrigues de Sa e Souza
c3fabf34ca fix(api): bulk-add API keys no longer overwrite existing connections (#7234)
* 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>
2026-07-17 10:40:29 -03:00
Diego Rodrigues de Sa e Souza
ef777d77e1 feat: editable ComfyUI base-URL field + per-connection override for image/video/music generation (#6928) (#7232)
* 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.
2026-07-17 10:40:22 -03:00
Diego Rodrigues de Sa e Souza
8143d8e3a6 feat: replace free-text model inputs with hidePaid-aware Selects (#6540) (#7229)
* 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.
2026-07-17 10:40:18 -03:00
Diego Rodrigues de Sa e Souza
ca03d619a4 feat(mitm): add Antigravity reasoning-effort overrides (#7228)
* 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>
2026-07-17 10:40:14 -03:00
Diego Rodrigues de Sa e Souza
0f10225f1d feat(dashboard): add compression-mode selector to Context & Cache combos page (#6760) (#7219)
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.
2026-07-17 10:40:05 -03:00
Diego Rodrigues de Sa e Souza
69c778eb45 feat(api): expose GET /api/usage/model-latency-stats (#6873) (#7218) 2026-07-17 10:40:01 -03:00
Diego Rodrigues de Sa e Souza
57ac712772 feat(api): add Vary: Accept-Encoding to token-authenticated /v1* responses (#6737) (#7217) 2026-07-17 10:39:58 -03:00
Diego Rodrigues de Sa e Souza
a2df195d5e fix: honor PROVIDER_LIMITS_SYNC_SPACING_MS for local/API-key connections (#6916) (#7214) 2026-07-17 10:39:54 -03:00
Diego Rodrigues de Sa e Souza
eb529cfa12 feat(dashboard): add reorder connections by availability button (#7211)
* 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>
2026-07-17 10:39:50 -03:00
Diego Rodrigues de Sa e Souza
97f993013d feat(dashboard): show Codex plan label in provider and quota views (#7210)
* 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>
2026-07-17 10:39:46 -03:00
Diego Rodrigues de Sa e Souza
2dc4a92be7 feat(kiro): register GPT-5.6 Sol/Terra/Luna model family (#7209)
* 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>
2026-07-17 10:39:42 -03:00
Diego Rodrigues de Sa e Souza
2b24448bd6 fix(oauth): resolve Kiro AWS SSO cache client credentials by clientId match (port from 9router#1253) (#7122)
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)
2026-07-17 10:39:29 -03:00
Diego Rodrigues de Sa e Souza
b914eb1b0f feat(providers): curated OpenRouter embeddings catalog + specialty merge in live discovery (#6976) (#6994)
* feat(providers): curated OpenRouter embeddings catalog + specialty merge in live discovery (#6976)

OpenRouter serves embeddings via a dedicated OpenAI-compatible
/api/v1/embeddings endpoint that is omitted from /v1/models, and the
embeddingRegistry entry for it was stale (3 legacy ids). Meanwhile
providerModelsConfig gives openrouter a live discovery config, so
buildApiDiscoveryResponse's success path returned only the live chat
catalog verbatim — the specialty (embeddings/rerank) static catalog was
only ever merged in on the no-config local_catalog fallback, so OpenRouter
embeddings never surfaced through model discovery.

Refreshed the curated openrouter embeddingRegistry lineup (ids verified
against https://openrouter.ai/docs/api/reference/embeddings and the
collections page) and added a scoped, additive merge
(mergeSpecialtyCatalogIntoLiveModels, allowlisted to openrouter) that folds
embeddings/rerank entries from getStaticModelsForProvider() into the live
discovery response, deduped by id. Scoped as an allowlist rather than a
blanket merge because some providers (e.g. Gemini) already return
embedding models directly from their live /v1/models endpoint, where a
blind merge would risk stale/duplicate entries.

* test(providers): type the models discovery payload instead of any (#6976)

no-explicit-any is an error under tests/ (#6218), so the 4 `any`
usages in the new discovery assertions failed the max-warnings-0
lint gate. Replace them with an explicit ModelsResponseBody shape —
type-only change, all 13 assertions unchanged and still passing.

* test(providers): type the openrouter merge assertion callback (#6976)

The new #6976 assertion added a 56th explicit `any` to this file,
one over the 55 frozen in config/quality/eslint-suppressions.json,
tripping the max-warnings-0 lint gate. Type the callback param
instead of raising the frozen count — the debt ratchet only decreases.
All 59 tests still pass.
2026-07-17 10:39:21 -03:00
Diego Rodrigues de Sa e Souza
c46d35bcb4 fix(dashboard): hide disabled provider connections from combo builder (#6984)
* fix(dashboard): hide disabled provider connections from combo builder

The combos page's fetchData() only filtered available connections by
testStatus ("active"/"success"), so a connection the user had
explicitly disabled (isActive: false) could still show up in the
combo builder if it carried a stale testStatus from before it was
disabled.

Add filterActiveConnections() in src/shared/utils/connectionStatus.ts
and apply it ahead of the existing testStatus filter.

Co-authored-by: itolstov <attid0@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2526

* chore(changelog): fragment for #6984

* fix(combos): keep combos page within frozen size cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(combos): extract filterUsableConnections to shrink the combos god-file

The combos page only filtered provider connections on testStatus, so a
connection the user had explicitly disabled survived with a stale
"active"/"success" status. The isActive + testStatus gate now lives in
the shared connectionStatus util as filterUsableConnections(), which the
page calls in a single line.

This keeps src/app/(dashboard)/dashboard/combos/page.tsx BELOW its frozen
file-size cap (4653 vs 4655 congelado — the file shrinks by 2 lines vs the
release tip) without touching config/quality/file-size-baseline.json, as
the gate asks ("modularize/extraia (DRY) para encolher").

The regression test now exercises filterUsableConnections directly instead
of hand-mirroring the page's filter chain, so it guards the real code path.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* fix(combos): drop nullish entries in filterActiveConnections

`connection?.isActive !== false` evaluated to true for null/undefined
entries, so nullish elements survived the filter. Callers read properties
off the result — filterUsableConnections() reads `connection.testStatus`
— which would throw "TypeError: Cannot read properties of null".

Guard with an explicit truthiness check. Covered by a test that fails
against the previous predicate.

Reported-by: gemini-code-assist
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: itolstov <attid0@gmail.com>
2026-07-17 10:39:18 -03:00
Diego Rodrigues de Sa e Souza
2013103765 fix(providers): derive static model catalogs for search providers from searchTypes (#7589)
getStaticModelsForProvider() only defined literal catalogs for
linkup-search, ollama-search, and searchapi-search out of the 12 ids in
SEARCH_PROVIDERS. The other 9 (serper-search, brave-search,
perplexity-search, exa-search, tavily-search, google-pse-search,
youcom-search, searxng-search, zai-search) returned undefined and hit
the 400 "does not support models listing" tail in the models route
during the "Import Models" step.

Instead of adding 9 more one-off literal entries, generalize the class:
when a provider has no dedicated STATIC_MODEL_PROVIDERS entry, fall
back to a catalog derived from SEARCH_PROVIDERS[id].searchTypes (every
search-registry entry already declares this). Future search providers
added to searchRegistry.ts automatically get a usable catalog with zero
extra code.

Closes #7529
2026-07-17 07:12:38 -03:00
Diego Rodrigues de Sa e Souza
265d00c0e1 fix(sse): honor per-model targetFormat override for zai/glm-coding-apikey (#7364) (#7584)
DefaultExecutor.buildUrl()'s "zai"/"glm-coding-apikey" case always returned
the Anthropic Messages URL, ignoring a per-model targetFormat override
(custom-model dropdown, #2905) that resolves to "openai" — e.g. for a vision
model like glm-4.6v. chatCore/executionCredentials.ts now threads the
resolved override onto providerSpecificData.targetFormat so buildUrl (via
the new default/zaiFormatOverride.ts helper, extracted to respect the
file-size ratchet) can route to the OpenAI-compatible endpoint instead.

Separately, custom-model id lookup (lookupCustomModelMeta in
src/sse/services/model.ts, getCustomModelRow in src/lib/db/models.ts) did an
exact, case-sensitive match, so a model saved as "glm-4.6v" was invisible
when looked up as "glm-4.6V". Both now fall back to a case-insensitive match
after the exact match fails.

Regression tests: tests/unit/zai-glm-target-format-override.test.ts (reused
from the triage plan-file's RED probe) and
tests/unit/zai-execution-credentials-target-format-7364.test.ts (production
wiring in executionCredentials.ts).

Gates run: check-file-size, check-complexity, check-cognitive-complexity,
typecheck:core, eslint (suppressions), tests/unit/zai-glm-target-format-override.test.ts,
tests/unit/zai-execution-credentials-target-format-7364.test.ts,
tests/unit/executor-default-base.test.ts, tests/unit/custom-model-target-format.test.ts,
tests/unit/chatcore-execution-credentials.test.ts, tests/unit/chatcore-target-format.test.ts,
tests/unit/model-resolver.test.ts, tests/unit/model-alias-provider-resolution.test.ts,
tests/unit/combo-custom-provider-resolution.test.ts — all green.

Refs #7364
2026-07-17 06:46:50 -03:00
Diego Rodrigues de Sa e Souza
6459dde35c fix(cli): reuse win32-aware locateCommand in tool-detector (#7279) (#7569)
detectBinary() in tool-detector.ts never checked process.platform and never
passed shell:true, so on native Windows an installed CLI (npm installs
claude/codex/opencode as .cmd shims) was reported as NOT installed:
1. execFileImpl(binary, ["--version"]) fails without shell:true for .cmd
   shims (Node's CVE-2024-27980 hardening).
2. the `which` fallback doesn't exist on native Windows (no WSL/git-bash).

Both threw, both were swallowed by empty catches, detectBinary returned
{installed: false}. cliRuntime.ts::locateCommand already solved this for the
runtime-spawn path (#968) but never propagated here — re-drift, per the
issue title.

Exports locateCommand from cliRuntime.ts and reuses it (+ shouldUseShellForCommand,
+ getLookupEnv) for the win32 existence/path probe in tool-detector.ts, keeping
the --version probe local but shell-gated. Also routes the which fallback through
the injectable execFileImpl hook (it previously called the raw execFileAsync,
making it unmockable and prone to false-positives from a real system which).
2026-07-17 06:36:04 -03:00
Diego Rodrigues de Sa e Souza
6b0c295b95 fix(sse): stop per-byte enumeration of binary image bytes in log redaction (#7297) (#7576)
captureCurrentProviderRequest mirrors every Bedrock Converse request into the
pending-request log tracker right after openAIToBedrockConverse() builds it,
including the decoded image.source.bytes Uint8Array. sanitizePayloadPII() and
redactPayload() in src/lib/logPayloads.ts both gate their recursive walk on
Array.isArray(), which is false for typed arrays, so each image fell into the
generic-object branch and got enumerated one JS key per decoded byte (twice,
once per function) before any truncation bound applied. For 3x ~1MB images
this took ~4s of synchronous, event-loop-blocking work, matching the
reporter's "1-2 images OK, 3+ fails" threshold and their --stack-size
observation (data-width pressure, not call-depth).

Add an opaque-binary short-circuit (ArrayBuffer.isView) ahead of the
Array.isArray branch in both functions, returning a fixed-size placeholder
instead of recursing. Apply the same guard to cloneBoundedForLog() in
open-sse/utils/requestLogger.ts for defense-in-depth (same blind spot, only
accidentally safe today via its own key-count slice).

Regression test reproduces the exact reporter shape (3x 1MB images) through
the real openAIToBedrockConverse() converter and protectPayloadForLog(),
asserting completion well under the previous ~4s and that binary bytes are
never expanded into per-byte object keys.
2026-07-17 06:12:07 -03:00
Diego Rodrigues de Sa e Souza
eb92e626d2 fix(api): resolve provider display name and dedup byModel on normalized key (#7534, #7535) (#7573)
- byProvider now resolves the internal provider id to its configured
  display name via getProviderById() (fallback: raw id for providers
  not in the static registry). Fixes the Usage page showing "codex"
  instead of "OpenAI Codex".
- byModel's in-memory dedup key now uses the normalized model name
  instead of the raw one, so the same logical model recorded under a
  bare and a provider-prefixed spelling (e.g. "glm-5.2" vs
  "z-ai/glm-5.2") merges into a single aggregated row instead of
  appearing twice with the same displayed name.
- Introduces a local UsageRows type alias in route.ts to shrink the
  repeated "as Array<Record<string, unknown>>" casts back under the
  frozen file-size baseline once the file was touched.
2026-07-17 06:11:43 -03:00
Diego Rodrigues de Sa e Souza
63c85ea76d fix(dashboard): resolve costs page 500 from out-of-scope t() in TopListCard (#7272) (#7564)
* fix(dashboard): resolve costs page 500 from out-of-scope t() in TopListCard (#7272)

TopListCard (CostOverviewTab.tsx) referenced the bare identifier `t`
from an outer component's scope when rendering the zero-cost /
!hasCostData branch, throwing "ReferenceError: t is not defined" and
crashing /dashboard/costs?range=all&apiKeyIds=...&groupBy=model
whenever a filtered slice landed only $0-cost rows.

Extracted TopListCard into its own component file and threaded the
resolved legacyFreeLabel string in as a prop, mirroring the existing
CostBreakdownTable pattern in the same file. Also fixes a case of the
typecheck:core dashboard .tsx coverage gap tracked in #7033.

* test(dashboard): move TopListCard #7272 regression test to vitest UI project

The node:test unit runner cannot load TopListCard's import chain
(@/shared/components -> ProviderIcon -> @lobehub/icons ESM), which made
the "Impacted unit tests (TIA subset; blocking)" CI job red with
"Unexpected token 'export'" on tests/unit/costs-toplistcard-legacy-free-label-7272.test.ts.

Moved the regression test to tests/unit/ui/ and rewrote it against the
vitest UI project (test:vitest:ui, blocking in the test-vitest CI job),
which handles the ESM import chain natively. Verified the test still
fails with "ReferenceError: t is not defined" against the pre-#7272
TopListCard body and passes against the fixed component.
2026-07-17 06:11:35 -03:00
Diego Rodrigues de Sa e Souza
ff15646f9b fix(db): pre-init sql.js WASM ahead of any getDbInstance() consumer (#7288) (#7562)
* fix(db): pre-init sql.js WASM ahead of any getDbInstance() consumer (#7288)

* fix(db): close sqljs preinit ordering gap without top-level await (#7288)

The previous fix added a top-level await barrier at the bottom of
src/lib/db/core.ts to guarantee sql.js pre-init before any consumer
reached getDbInstance(). That made core.ts an async ES module, which
broke esbuild's CJS require() bundling for every test file that does
require("../../src/lib/db/core.ts") (tsx's CJS require hook rejects
requiring a transitive dependency with a top-level await), and caused
unrelated tests running in the same node:test process to fail with
"Promise resolution is still pending but the event loop has already
resolved".

Move the fix to the real startup entrypoint instead: registerNodejs()
(src/instrumentation-node.ts) now awaits ensureDbReadyForBoot() before
ensureSecrets()/clearStaleCrashCooldowns()/getSettings()/initAuditLog(),
all of which reach getDbInstance() transitively. ensureDbInitialized()
is idempotent, so later getDbInstance() calls are free cache reads.

The driverFactory.ts error-surfacing improvements from the original
#7288 fix (logging swallowed sync-driver errors, surfacing the real
sql.js pre-init failure instead of the generic "not pre-initialized
yet" message) are unchanged.

Updated tests/unit/db-sqljs-preinit-ordering-gap-7288.test.ts to prove:
no top-level await in core.ts, the corrected call order in
registerNodejs() (source-order assertion), and the original
getDbInstance()-no-longer-throws-the-misleading-message behavior driven
via the same warm-up path ensureDbReadyForBoot() now guarantees ahead
of every other startup step.

* refactor(db): drop the orphaned preInitSqlJsIfSyncDriversUnavailable helper (#7288)

Moving the ordering guarantee to registerNodejs() left this exported
helper with zero production callers — its own docblock still claimed it
was 'Chamada no top level de core.ts', describing an architecture the
hotfix removed. It was also redundant: ensureDbInitialized() already does
tryOpenSync-then-preInitSqlJs on the real boot path (core.ts:1358).

Its two tests exercised the helper as a stand-in for the real warm-up
('Simulates the fixed ordering'), so they proved a simulation rather than
production behaviour. They now drive ensureDbReadyForBoot()/tryOpenSync()
directly. The ordering guard still fails against the unfixed
instrumentation-node.ts (verified) and the whole file is 4/4 green.

The live parts of the driverFactory change (logSwallowedDriverError,
getSqlJsPreInitError) are untouched — both have real callers.
2026-07-17 06:11:27 -03:00
Diego Rodrigues de Sa e Souza
b9847bf791 fix(dashboard): surface rate-limit warning on 429 chat-probe (#7284) (#7565)
Root cause: validateOpenAILikeProvider's chat-probe status handling
(src/lib/providers/validation/openaiFormat.ts) only special-cased
401/403, 404/405, and >=500 — every other status, including 429, fell
through to the unqualified return { valid: true, error: null }. For
permanently-throttled free tiers (e.g. opencode-zen, classified
'avoid' in freeTierCatalog.ts), the dashboard connection Test reported
green forever while real traffic hit 429 on every request, with no
signal to the user.

Fix mirrors the existing validateBedrockProvider 429 precedent in the
same file: keep valid:true (the key is accepted) but add a warning
field describing the rate limit, instead of an indistinguishable pass.

Regression test: tests/unit/issue-7284-connection-test-masks-429.test.ts
mocks a 404 /models probe followed by a 429 chat probe and asserts the
result now carries { valid: true, error: null, warning: <rate-limit string> }.

Gates run (all green):
- node --import tsx/esm --test tests/unit/issue-7284-connection-test-masks-429.test.ts
- node --import tsx/esm --test tests/unit/provider-validation-firepass-403.test.ts tests/unit/validation-format-validators-split.test.ts (existing tests of the touched area, unaffected)
- node scripts/check/check-file-size.mjs
- node scripts/check/check-complexity.mjs (2054/2056 baseline, unchanged)
- node scripts/check/check-cognitive-complexity.mjs (889/890 baseline, unchanged)
- npm run typecheck:core
- npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>
2026-07-17 05:32:25 -03:00
Diego Rodrigues de Sa e Souza
851582a88a fix(dashboard): providers model-name filter matches live/synced catalog (#7250) (#7561)
Root cause: the Providers page model-name filter
(filterConfiguredProviderEntries) matched only against
getModelsByProviderId(...), the static curated model registry, never
the live/synced catalog for the connection. Aggregator providers
(openrouter, kilocode, theoldllm...) declare a single-entry static
placeholder (e.g. openrouter's {id:'auto',name:'Auto (Best Available)'}),
so searching for any real upstream model name could never match and the
whole provider silently disappeared from the list.

Fix: source the live/synced catalog (already persisted per-connection via
GET /api/synced-available-models, the same store the combo builder's model
picker already relies on) via a new useSyncedModelsByProvider hook, and
union it with the static registry inside the filter. An empty/never-synced
catalog falls back to the static-only match so already-correct static
providers are unaffected.

Regression test: tests/unit/provider-model-filter-live-catalog-7250.test.ts
reproduces the original bug (static-only match returns 0 results for a
real model name) and proves the fix (live catalog match returns 1),
plus non-regression coverage for the static-only fast path and unrelated
providers.
2026-07-17 05:01:23 -03:00
Diego Rodrigues de Sa e Souza
0bd60f3fb2 fix(cli): Windows cert check/uninstall key off the real CA identity, not a hardcoded legacy host (#7275) (#7557)
* fix(cli): Windows cert check/uninstall key off the real CA identity, not a hardcoded legacy host (#7275)

checkCertInstalledWindows()/uninstallCertWindows() queried the Windows Root
store by the literal legacy hostname daily-cloudcode-pa.googleapis.com
regardless of the certPath passed in (the check's param was even
underscore-prefixed/unused). It only worked because that hostname happens to
be the generated CA's own commonName today (generate.ts derives it from
ANTIGRAVITY_TARGET.hosts[0]) -- a coincidence, not a derivation, with no
shared symbol coupling the two. installCertWindows() was already correct.

Both functions now derive a SHA-1 thumbprint straight from the certPath file
via the new exported certutilThumbprint() helper (reusing the existing
getCertFingerprint() logic checkCertInstalledMac() already keys off), so the
Windows store lookup/delete always matches the real generated CA regardless
of any future rename/reorder of ANTIGRAVITY_TARGET.hosts in generate.ts. Same
anti-pattern class as #6338 (DNS side).

* test(cli): assert certutil argv precisely instead of scanning for the legacy host (#7275)

The two negative substring checks tripped CodeQL's
js/incomplete-url-substring-sanitization (high) by looking like URL
sanitization while actually asserting a certutil argv. Replaced with
assertions on the exact argv / extracted certId, which are strictly
stronger: pinning the value proves no other identity can be passed.
Both still fail against the unfixed install.ts (5/5 RED) and pass with
the fix (5/5 GREEN).
2026-07-17 05:01:16 -03:00
Diego Rodrigues de Sa e Souza
12b25d5e0d fix(i18n): treat __MISSING__ sync placeholders as absent in EN fallback (#7258) (#7556)
deepMergeFallback in src/i18n/request.ts only substituted the English
fallback value when a key was entirely undefined. Keys backfilled by
scripts/i18n/sync-ui-keys.mjs with the __MISSING__:<english> sentinel
exist on the target object, so they passed through untouched and were
rendered raw to the user (395 zh-TW keys, 337 pt-BR keys, systemic
across locales).

Now any target leaf that still carries the __MISSING__: prefix is
treated the same as an absent key, so the clean EN value wins.

Does not touch the underlying translation content (395/337 strings) -
that is a separate content workstream.
2026-07-17 04:23:30 -03:00
Diego Rodrigues de Sa e Souza
197f726c62 fix(oauth): surface tunnel hint when Codex OAuth runs on a remote host (#7523) (#7527)
The PKCE callback server binds the SERVER's loopback (localhost:PORT). When
the operator drives the OAuth flow from a different machine (OmniRoute on a
remote host/VPS), the provider redirects the browser to the operator's OWN
localhost:PORT — the confirmation screen hangs forever with no explanation.

start-callback-server now inspects the request Host: on a non-loopback host it
returns { remoteHost, tunnelCommand, message } so the UI can show the
'ssh -L PORT:127.0.0.1:PORT' instruction (or steer to the paste/import flow)
instead of a silent hang. Loopback access is unaffected. The Host header is
spoofable, so this drives only a UI hint — never an auth decision.

Logic extracted to remoteOAuthHint.ts (keeps the god-route under its size
budget and makes it unit-testable). TDD: 4 tests covering loopback (no hint),
null host (fail-open), and remote host (correct tunnel command for both the
fixed 1455 and OS-assigned ports).

Closes #7523
2026-07-17 02:39:30 -03:00
Diego Rodrigues de Sa e Souza
ab01d74306 fix(codex): validate refresh_token on import before persisting (#7522) (#7525)
POST /api/oauth/codex/import accepted a payload with an already-invalidated
refresh_token and persisted it as an 'active' connection that could never
work — the failure surfaced confusingly only on first real use, long after
the import looked successful.

Validate each record's refresh_token against OpenAI's OAuth endpoint before
persisting, reusing refreshCodexToken() (free exchange, no quota). On an
unrecoverable refresh error the record is rejected with a clear re-auth
message; a valid token imports as before, with any rotated tokens applied.
Bulk import still processes each record independently — one dead token no
longer blocks the valid ones.

Reproduced live 2026-07-16: a 2026-07-10 auth.json imported clean but its
refresh_token returned 401 refresh_token_invalidated. TDD: 3 tests RED against
the old route, 5/5 GREEN with the fix.

Closes #7522
2026-07-17 02:39:11 -03:00
Diego Rodrigues de Sa e Souza
7974d03b9d fix(codex): Test probe uses a ChatGPT-account-supported model (#7521) (#7524)
The connection Test button always reported success for a Codex connection
backed by a ChatGPT account: the probe sent `gpt-5.3-codex`, a codex-only
model the ChatGPT-account backend rejects outright with a 400 — the same
status the probe treats as 'auth accepted, body invalid'. A bad token and a
good token both came back 400, so Test could never fail on a bad token.

Probe with `gpt-5.5` (confirmed served for ChatGPT-account sessions via live
VPS test 2026-07-16) instead; `input: []` still yields the intended 400 for a
good token, 401/403 for a bad one.

Live verification (VPS): gpt-5.3-codex, gpt-5.6-sol and the gpt-5*-codex ids all
return 'not supported when using Codex with a ChatGPT account'; gpt-5.5 and
gpt-5.6-terra answer normally on the same account.

Closes #7521
2026-07-17 02:39:02 -03:00
Wibias
88507a6edc [needs-vps] fix(dashboard): align onboarding tier content (#7125)
* fix(dashboard): align onboarding welcome feature cards vertically

* fix(dashboard): align onboarding tier content

* chore: scope onboarding PR to UI fix

* i18n(pt-BR): add onboarding.tier.flowCaption + afterSetup keys

The two new tier keys added to en.json were missing from pt-BR.json, tripping
the i18n-pt-br no-drift test (#6695). Add their pt-BR translations.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-17 01:01:29 -03:00