mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 21:32:10 +03:00
4a332fe2bd327b2f4c3137df70e9a8dcf0c2041a
840 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ed7db3ee5f |
feat(dashboard): copy-paste settings.json block for Claude Code discovery (#8722)
The discovery-alias info button explains the gate and links to the flag, but the operator still had to assemble the Claude Code config by hand from the guide. Render it instead, on the same Claude tool card, from the base URL the card has already resolved (custom override included, normalized — no /v1, no trailing slash), with a copy button. The key slot holds a placeholder, never the real key: this card renders before a key is necessarily selected, and a real key in copyable text is an easy way to leak one into a screenshot or a pasted snippet. buildClaudeDiscoverySettingsSnippet is a pure builder in claudeCliConfig so the shape is unit-tested (7 cases, including that no `sk-` ever reaches the output and that an invalid CLAUDE_CODE_AUTO_COMPACT_WINDOW is dropped rather than emitted). Guide gains the matching section; the five new strings are translated for pt-BR and vi (the locales with strict parity/marker tests) and marked in the rest, per the repo's convention. |
||
|
|
cd9a631464 |
feat: Claude Code discovery aliases (surface non-Claude models in the /model picker) (#8666)
* feat(db): cc discovery alias gate storage + EXPOSE_CC_DISCOVERY_ALIASES flag
Adds the gate for claude/<provider>/<model> discovery-alias mirror ids on
the /v1/models catalog: a new runtime feature flag (env forces on and wins
over the dashboard DB override), per-provider and per-model "on"/"off"/null
overrides stored in key_value under the ccDiscoveryAliases namespace, and a
pure precedence resolver (model > provider > global). Catalog wiring is a
separate follow-up task; this only lands the gate + storage.
* feat(sse): synthesize claude/ discovery aliases for the model catalog
* feat(api): advertise cc discovery aliases on /v1/models behind the 3-level gate
* feat(sse): resolve claude/ discovery aliases on the request path
* fix(sse): import getComboByName from db/combos, not the localDb barrel
* fix(sse): cover custom-node prefixes and the Codex WS bridge in cc discovery alias resolution
* feat(dashboard): cc discovery alias toggles + flag-screen env warning
Adds the operator-facing UI/API layer for the Claude Code discovery-alias
gate (claude/<provider>/<model> mirror ids on /v1/models): REST endpoint
for provider/model overrides, a provider-detail card with 3-state
(inherit/on/off) toggles, an info button on the Claude Code tool card
linking to Feature Flags, and an env-source warning on the
EXPOSE_CC_DISCOVERY_ALIASES flag card when it's forced on via env.
* feat(api): cc discovery usage metrics
* fix(api): record cc alias metric in the production wrapper + atomic counter upsert
* docs: document cc discovery aliases (Claude Code guide + feature flag catalog)
* fix(sse): don't mirror built-in auto/* combos as discovery aliases (advertised-but-unroutable)
* i18n(vi): translate the discovery-alias strings instead of shipping placeholders
vi is the one locale with a strict "no internal missing markers" test, so the 17
__MISSING__ entries this branch added (the provider ccAlias panel, the info
button, the feature-flag description and the env warning) would have turned that
test red the moment the base itself was repaired. Translated, keeping every ICU
placeholder ({modelId}, {error}) and the literal claude/<provider>/<model> id
shape intact.
* chore(quality): raise the frozen caps this feature legitimately grows
catalog.ts 1615 -> 1639: the alias synthesis is wired into the catalog builder,
which is where the per-key-filtered list is assembled — the only place the mirror
entries can be appended after model hiding has been applied.
localDb.ts 808 -> 810: two re-export lines for the new ccDiscoveryAliases db
module, which is exactly what the "Adding a New DB Module" recipe prescribes.
* refactor(dashboard,api): keep the complexity ratchets flat
The feature added four cyclomatic violations and one cognitive one, which the
ratchets reject — the baseline only moves when a metric improves. Split the new
code instead:
- appendCcDiscoveryAliases: the four skip-guards become isMirrorableId().
- resolveCcDiscoveryAliasStripWith: alias parsing and gate resolution become
parseCcAliasTarget() and resolveGateFor(), replacing a chain of ternaries that
each re-tested isComboAlias.
- FeatureFlagCard: the env-precedence warning becomes its own component instead
of a conditional branch inside an already-large render.
- ProviderCcAliasSection: the loader moves to useCcAliasData(), and the override
list and add-row become ModelOverrideList / AddOverrideRow, bringing both
oversized functions back under the 80-line rule.
Behavior unchanged — the 74 discovery-alias tests pass untouched. Both ratchets
now sit exactly at baseline (2188 / 971).
|
||
|
|
a61ed5deb1 |
fix(oauth): actionable guidance for LAN-origin loopback mismatches (codex + Antigravity) (#8463)
* fix(oauth): explain the LAN-IP loopback mismatch with an actionable panel #8046 already stops the doomed login when a PKCE_CALLBACK_SERVER_PROVIDERS provider (codex / xai-oauth / grok-cli) is connected from a LAN IP, but it explained itself as one long English sentence rendered in the generic red "Connection failed" step. Two concrete problems with that: - the operator had to parse prose to work out WHICH ports to forward, and the command shipped with `<port>` / `<omniroute-host>` placeholders to resolve by hand; - it forwarded a single port. Both are required: the dashboard port is what makes the origin true-localhost (a LAN origin never reaches the callback-server branch at all), and the provider's fixed callback port is where the browser is actually sent back to. Forwarding either one alone still fails. buildPkceLoopbackMismatchHint() now returns the diagnosis as structured data with the detected host and both ports already filled in, and a dedicated OAuthLoopbackMismatchPanel renders it as: what happened -> how to fix, in three numbered steps with copy-to-clipboard fields. No "Try again" button — retrying the same origin fails identically. The panel yields to the paste-token tab so grok-cli (which is in both provider sets) never stacks the two views. The flat warning string stays exported for non-UI callers. docs: REMOTE-MODE.md gains a "Connecting Codex / Grok on a remote install" section with the fixed-callback table and the two-port tunnel, mirroring the existing Antigravity section. i18n: 9 new oauthModal keys, hand-written for en + pt-BR and propagated to the remaining 40 locales as `__MISSING__:` sentinels (runtime falls back to the clean English value per #7258). * fix(oauth): correct the Antigravity remote-login guidance and drop the stale i18n copy Same LAN-origin family as the codex fix in this branch, different mechanism and a worse failure mode. Google providers (antigravity / agy) have no fixed foreign port: OAuthModal builds `http://127.0.0.1:<dashboardPort>/callback`. On a LAN origin that 127.0.0.1 is the BROWSER's machine, and Google's firstparty/nativeapp consent only releases the code once the loopback is reachable from the approving browser. When it is not, the consent never redirects at all — it hangs. So unlike an ordinary provider there is no error page and no callback URL in the address bar. That made the existing copy actively wrong. `googleOAuthWarning` was corrected when the login helper shipped (#5203), but a changed English value does not invalidate existing translations and `i18n:sync-ui` only fills keys that are ABSENT, never ones that are STALE — so 39 of 43 locales (pt-BR, pt, es, de, fr, ja, zh-CN, …) kept the original "wait for the redirect, copy the full URL and paste it below", instructing a flow that cannot complete. The drift gate that should have caught this is a no-op: `check-translation-drift.mjs` needs `.i18n-state.json`, which is not in the repo, and it runs `--warn`. Because the key's MEANING changed, it is renamed rather than edited — a new key cannot inherit a stale translation. `googleOAuthWarning` is removed from all 43 locales and replaced by 7 `googleLoopback*` keys, hand-written for en + pt-BR and marked `__MISSING__:` elsewhere so the runtime falls back to correct English (#7258). UI: `OAuthGoogleLoopbackNotice` states what is happening and surfaces both real remedies with the detected host and port filled in — the local login helper (recommended; its blob is what the Step 2 field accepts) and a single-port SSH forward. It also REPLACES `remoteAccessInfo` for this family instead of stacking on top of it: that notice promises an error page whose URL you copy, true for ordinary providers and false here. `agy` deliberately gets no helper command. bin/cli/commands/login.mjs pins PROVIDER = "antigravity" and parsePastedCredentials() rejects a blob whose embedded provider does not match the route provider, so advertising the helper there would send the operator to a blob guaranteed to be refused. It keeps the tunnel path. Refactor: the shared `resolveDashboardPort` / `buildSshLocalForward` helpers move to `loopbackTunnel.ts`, used by both hint builders. The codex builder's behaviour is unchanged (its 11 tests still pass untouched). docs: REMOTE-MODE.md notes that the dashboard now surfaces the remedies, states that one forward is enough for Antigravity (contrasting the two-port codex case), and aligns Option B's command on 127.0.0.1 to match what the UI generates. --------- Co-authored-by: ikelvingo <im.kelvinwong@gmail.com> |
||
|
|
da20b96dc0 |
feat(providers): weekly quota for xAI OAuth (Grok) (#8471)
* feat(providers): weekly quota for xAI OAuth (Grok) Live weekly credit pool for xai-oauth (alias xao) via the shared cli-chat-proxy billing API (creditUsagePercent), using the connection OAuth access token. - Export fetchGrokBillingWithToken from grokQuotaFetcher for reuse - xaiOauthQuotaFetcher: 60s cache, fail-open, preflight + monitor - Provider Limits allowlist and weekly window * test(providers): cover xai-oauth usage dispatch + fix changelog Address PR review: - fix changelog file & rename to 8471-xai-oauth-weekly-quota.md - export getXaiOauthUsage via __testing - add xai-oauth-usage.test.ts --------- Co-authored-by: allanvb <allanvb@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
f6f0303be4 |
feat(log): Added new visual scrolling log page (#8354)
* feat(log): Added new visual scrolling log page * chore(quality): rebaseline sections.ts own-growth for #8354 (logs-timeline sidebar item) * feat(log): direct-link a request from the scrolling timeline Clicking a request bar now sets ?id= on the URL (matching the regular request log page), and the timeline opens the deep-linked request on mount. The open/close handlers arm the same guard so a stale initialSelectedId (router.replace() commits the URL after the render it triggers) can never reopen the modal right after the user closes it. * fix(dashboard): propagate logsTimelineSubtitle across all 43 locales en.json was missing the logsTimelineSubtitle key entirely, breaking the default locale for the new /dashboard/logs/timeline sidebar entry. Add the real English string to en.json, add __MISSING__: placeholders to the 11 locales that lacked the key outright, and convert the 30 locales that had copied the English text literally to the repo's __MISSING__: convention for untranslated strings. Also pause the RequestTimeline 2s poll while the tab is backgrounded (document.visibilityState), matching the existing pattern in RequestLoggerV2 and UsageStats. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(i18n,sidebar): add missing logsTimelineSubtitle + update sidebar tests Address maintainer feedback on #8354: - Add logsTimelineSubtitle key to en.json + 11 locales (ar, az, bg, bn, cs, da, de, es, fa, fi, fr) that were missing it - Add logs-timeline to sidebar-visibility.test.ts expected arrays - Add logs-timeline to sidebar-monitoring-reorg.test.ts logs group - Add compression-exclusions to sidebar-visibility.test.ts (pre-existing) * feat: add touch support for mobile panning and pinch-to-zoom on timeline --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
8115867c2d |
fix(dashboard): include OMNIROUTE_BASE_PATH in displayed API base URL (#8514)
* fix(dashboard): include OMNIROUTE_BASE_PATH in displayed API base URL When OmniRoute is served under a reverse-proxy subpath (OMNIROUTE_BASE_PATH), the Endpoints UI built display URLs from window.location.origin alone and appended /v1, producing https://host/v1 instead of https://host/omniroute/v1. - Prefer NEXT_PUBLIC_BASE_URL when it already includes a non-root path - Append NEXT_PUBLIC_OMNIROUTE_BASE_PATH (mirrored from OMNIROUTE_BASE_PATH at build time) when resolving a bare public origin - Document subpath display behavior in .env.example - Add unit coverage for basePath-aware resolution * docs(changelog): add fragment for #8514 display basePath fix --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> |
||
|
|
63341f2aed |
feat(combos): add select all / unselect all in Browse Catalog (#8526)
* feat(combos): add select all / unselect all in Browse Catalog * fix(dashboard): guard combo Select all + test the real batch handlers (#8526) Select all had no cap — with "Show configured only" off, or a large provider catalog, one click could add hundreds of models to a combo. ModelSelectModal now confirms above SELECT_ALL_CONFIRM_THRESHOLD (20) before batch-adding, matching the native confirm() pattern already used for bulk/destructive actions elsewhere in the dashboard. Also extracts ComboFormModal's handleAddModels/handleDeselectModels batching logic into computeBatchAddModelSteps/computeBatchDeselectModelSteps (src/lib/combos/builderDraft.ts) so unit tests exercise the real implementation instead of a hand-maintained mirror that could drift from the component and stay green while production code broke. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> |
||
|
|
d4b9ce6016 |
fix(kiro): harden auth flows, quota lookup, and model discovery (#8565)
* fix(kiro): fetch builder id quota without profile arn * fix(kiro): harden auth imports polling and model discovery * fix(kiro): preserve auth identity and OAuth polling semantics * docs(changelog): add Kiro auth and model discovery fix --------- Co-authored-by: Nguyễn Thanh Hà <nguyenha@Mac-mini-M4.local> Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> |
||
|
|
909642879f | feat: add Claude Opus 5 support (#8464) | ||
|
|
9a0e764459 |
feat(cli): replace ANTHROPIC_SMALL_FAST_MODEL with Fable default (#8343)
Claude Code retired ANTHROPIC_SMALL_FAST_MODEL; expose ANTHROPIC_DEFAULT_FABLE_MODEL from the claude registry instead. |
||
|
|
1cafd328c7 | fix(dashboard): persist compression engine detail settings (Headroom / session dedup / CCR) instead of dropping them on save (#8388) (#8404) | ||
|
|
fa46d93941 | fix(api): accept the current compatible-provider connection id scheme in the models test route (#8326) (#8359) | ||
|
|
70275be59b | fix(dashboard): show custom provider_nodes providers in the Topology view instead of only AI_PROVIDERS (#8328) (#8357) | ||
|
|
07067841b5 |
feat(combo): enforce provider and model family invariants (#8304)
* feat(combo): enforce provider and model family invariants Closes #8279 Co-Authored-By: Ravi Tharuma <ravitharuma@users.noreply.github.com> * fix(combo): complete invariant enforcement paths Map invariant failures to structured API errors, correct target diagnostics, and validate restored combos inside the existing migration transaction. Co-Authored-By: Ravi Tharuma <noreply@github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Ravi Tharuma <noreply@github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
97d948cc9e |
fix(antigravity): add missing gemini-3.6-flash pricing rows to ag OAuth pricing (#8290)
release/v3.8.49 already ships the Gemini 3.6 Flash catalog entries (AGY_PUBLIC_MODELS, ANTIGRAVITY_PUBLIC_MODELS, MODEL_SPECS with supportsThinking: false — Antigravity still rejects client-supplied thinking params) via #8013. What was still missing: the `ag` pricing rows in DEFAULT_PRICING_OAUTH, so getPricingForModel("ag", id) returned null for the three tiers and cost/quota calculations silently fell back to $0. Pricing: $1.50 input / $7.50 output / $0.15 cached per MTok (Google's 2026-07-21 announcement), matching the existing 3.5-flash schedule shape. Thinking tokens billed at output rate. Extends the existing pricing-ag-flash-tiers.test.ts (RED-first: all three tiers failed the "non-null pricing row" assertion before this change) rather than re-adding the already-shipped catalog/modelSpecs entries. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
af60f41e99 |
fix(providers): reconcile Kimi K3 vision when attachment contradicts modalities (#8250) (#8313)
* fix(providers): reconcile Kimi K3 vision when attachment contradicts modalities Synced models.dev rows for kimi-coding*/k3 can ship attachment=false while modalities_input still lists image/video. Prefer the modality signal (and normalize at sync + resolve) so supportsVision, attachment, and exposed modalities agree. Closes #8250 * fix(providers): keep Kimi K3 static fallback text-only (#8250) The Kimi K3 vision reconciliation added supportsVision=true directly to the kimi-coding registry entry for id "k3". That entry is the static/stable fallback catalog used when discovered capabilities are unavailable, and it must stay text-only per #4071 — the vision fix is already applied correctly on the discovered path via MODEL_SPECS["kimi-k3"] (aliases: ["k3"]) and modelCapabilities.ts::resolveVisionCapability. Restores the invariants guarded by tests/unit/kimi-k2.7-code-registration.test.ts ("Kimi Code k3 fallback leaves discovered capabilities unset") and tests/unit/catalog-updates-v3829-kimi-qwen.test.ts ("kmca stable fallback only carries documented static capabilities"). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
406f41de30 |
fix(translator): cap thinking budget on explicit budget_tokens path (#8312)
* fix(translator): cap thinking budget on explicit budget_tokens path * fix(translator): stop dropping thinkingConfig on cap-0 reasoning_effort path The thinking-budget-cap guard added in this branch skipped thinkingConfig entirely whenever a model's thinkingBudgetCap was 0 (e.g. gemini-3-flash), including on the reasoning_effort/budgetMap path. That regressed the pre-#6943 native-defaults contract (thinkingBudget 0 / includeThoughts false must still be present) and crashed callers that read `.thinkingConfig.thinkingBudget` unconditionally (translator-openai-to-gemini-defaults.test.ts). Also restore includeThoughts:true on the Claude-format explicit thinking.budget_tokens path (openai-to-gemini.ts's Claude-format field and claude-to-gemini.ts's native thinking field): budget_tokens:0 there is the client's dynamic-thinking sentinel (#6813), not an off-switch, and must stay true even after the new capping — the cap must only clamp positive explicit values, never flip the zero sentinel's semantics. Updates two tests this branch added that encoded the incorrect "omit thinkingConfig / includeThoughts:false for the 0 sentinel" behavior, to match the pre-existing, still-required contracts above. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): revert scope-creep flip of Gemini 3.5/3.6 Flash supportsThinking The thinking-budget-cap fix accidentally expanded 5 shorthand modelSpecs entries (gemini-3.5-flash, gemini-3.5-flash-low, gemini-3.6-flash-high/ medium/low) into explicit objects setting supportsThinking:true and thinkingBudgetCap:24576. That flip was unrelated to the two proven test regressions (translator-openai-to-gemini-defaults.test.ts and claude-to-gemini-budget-tokens-zero-6813.test.ts, which only exercise gemini-3-flash-preview, gemini-3.1-pro and gemini-2.5-pro) and reopens a deliberately closed path from #8013: Antigravity still rejects client-supplied thinking params for these Gemini 3.5/3.6 Flash tier ids, so supportsThinking must stay false (inherited from GEMINI_35_FLASH_MODEL_SPEC). Reverted all 5 entries back to the release shorthand `{ ...GEMINI_35_FLASH_MODEL_SPEC }`. gemini-3-flash, gemini-3.1-pro and gemini-2.5-pro (the models the regression tests actually exercise) were already correctly specced in the release baseline and are untouched. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
08a21bcf27 |
fix(backend): add structure-aware chat admission (#8296)
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
869d08ff8b |
Add Alibaba-family media model support (#8266)
* Add Alibaba-family media models * chore(quality): rebaseline imageRegistry+cognitive for #8266 media own-growth --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
069d5a7925 |
fix(dashboard): stop sidebar RSC prefetch storms (#8292)
* fix(dashboard): disable sidebar route prefetch Co-Authored-By: Claude <noreply@anthropic.com> * test(dashboard): cover sidebar prefetch traffic Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2f65339378 |
fix(providers): limit Gemini CLI to legacy OAuth refresh (#8275)
#8232 correctly targeted OAuth auto-refresh for legacy stored Gemini CLI connections, but exceeded that compatibility goal by recreating a complete routable and UI-visible provider with an Antigravity model catalog. Preserve legacy refresh by mapping gemini-cli rows to the existing Gemini OAuth credentials. Remove the public provider registry, OAuth preset, model routing snapshot, and canonical-provider tests while keeping Gemini API and Antigravity unchanged. |
||
|
|
a6eb4d8166 |
fix: normalize Codex URLs and dashboard regressions (#8233)
Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
2a865aaaa7 |
feat(settings): configurable model catalog cache TTL (#8219)
* feat(settings): configurable model catalog cache TTL Add modelCatalogCacheTtlMs to DatabaseSettings with default 1500ms. Extend cache-config API route to accept the new field. Replace hardcoded catalog cache TTL with dynamic settings value. Add 'Cache' settings tab at /dashboard/settings/cache with sidebar entry, i18n keys, header description, and legacy route redirect. * feat(combo): add model connection filter toggle to ModelSelectModal Adds a 'Show configured only' checkbox below the search bar in ModelSelectModal that filters each provider group's models through hasEligibleConnectionForModel. Toggle state persists in localStorage. - Import hasEligibleConnectionForModel from domain/connectionModelRules - showConfiguredOnly state + localStorage persistence - connectionFilteredGroups memo layered on filteredGroups - Renders both provider section and empty state from connectionFilteredGroups * test(combo): add connection filter toggle tests for ModelSelectModal Three test cases: (1) hide excluded models when toggle on, (2) show empty state when all models excluded, (3) drop provider group when all its models excluded. All 3 tests pass. * fix(settings): correct cache-config route import + add route/tab coverage The cache-config route imported get/update helpers from a nonexistent module (@/lib/localDb/databaseSettings) and called an undefined updateSettings() in PUT, crashing every request. Import the real databaseSettings module (matching the sibling database/route.ts convention) and call updateDatabaseSettings(); idempotencyWindowMs is routed through the flat @/lib/db/settings module instead, since that is where it is actually read at runtime (idempotencyLayer.ts, runtimeSettings.ts) — it was never part of the databaseSettings "cache" section type. Also fixes a dashboard-typecheck regression in ModelSelectModal.tsx: the new connection-filter toggle called hasEligibleConnectionForModel() with activeProviders entries typed too narrowly to include providerSpecificData, which real connection objects carry at runtime. Adds: - tests/unit/cache-config-route-8219.test.ts: GET/PUT resolve without crashing, modelCatalogCacheTtlMs and idempotencyWindowMs round-trip. - tests/unit/ui/cache-settings-tab-bounds-8219.test.tsx: CacheSettingsTab min/max TTL bounds (100ms/60000ms) gate the Save button and surface a validation message; in-bounds values PUT correctly. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): rebaseline sections.ts for #8219 own-growth --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
81ade9e122 |
feat(providers): add Typhoon (Thailand) and Inception Mercury diffusion LLM (#8170)
* feat(providers): add Typhoon and Inception Mercury API-key providers Two OpenAI-compatible API-key providers, each verified against a live endpoint smoke test with a negative control before registration: an unknown route answers 404 while /v1/chat/completions answers 401, which rules out gateways that reply identically to every path. - typhoon (SCB 10X, Thailand): first Thai-first provider in the catalog. /v1/models answers 200 unauthenticated and serves exactly one chat model, typhoon-v2.5-30b-a3b-instruct (128K ctx). The docs also list typhoon-v2.1-12b-instruct, but the live endpoint no longer serves it, so it is deliberately not registered. The typhoon-ocr* and typhoon-asr* entries are OCR and speech models, not chat, and are omitted. - inception (Inception Labs): first diffusion LLM (dLLM) in the catalog. mercury-2 has a 128K context, 50K max output, and supports tools, json_mode and structured outputs. The mercury-coder models advertised on the vendor blog are no longer served by the live endpoint and are therefore not registered. Both expose a working /v1/models catalog, so they are added to NAMED_OPENAI_STYLE_PROVIDERS for discovery and key validation. Free-tier metadata is claimed only where it is documented and durable: Typhoon issues a free API key rate-limited to 5 req/s and 200 req/m, and Inception grants 10M tokens on signup with no card, so both are registered with hasFree: true. Provider count moves from 280 to 282; README/AGENTS/CLAUDE counters were stale at 278 and are resynced against docs/reference/PROVIDER_REFERENCE.md. * fix(8170): union frontier-labs Inception+Writer, regen ref+golden * fix(8170): close inception object + regen ref/golden --------- Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
7ca821aeee |
feat(providers): add Sarvam AI, Writer Palmyra and PLaMo API-key providers (#8161)
* feat(providers): add Sarvam AI, Writer Palmyra and PLaMo API-key providers Three OpenAI-compatible API-key providers, each verified against a live endpoint smoke test before registration: - sarvam (India): /v1/models answers 200 unauthenticated and lists sarvam-105b (128K ctx) and sarvam-30b (64K ctx). The older sarvam-m is discontinued upstream and is deliberately not registered. - writer (Palmyra): api.writer.com exposes the OpenAI alias /v1/chat/completions alongside its native /v1/chat — confirmed with a negative control, since an unknown route answers 404 'endpoint not available via API gateway' while /v1/chat/completions answers 401. Registers palmyra-x5 (1M ctx) and palmyra-x4 (128K ctx); the medical/financial/creative/vision variants are deprecated upstream and are omitted. - plamo (Preferred Networks, Japan): only plamo-3.0-prime (262K ctx) is registered. plamo-3.0-prime-beta is discontinued on 2026-07-31 and plamo-2.2-prime on 2026-09-30, so neither is worth wiring up. Free-tier metadata is claimed only where it is documented and durable: Sarvam ships a permanent signup credit, while PLaMo's 10M-token grant is a campaign that expires on 2026-07-31 and Writer documents no free tier, so both are registered with hasFree: false. * regen golden+ref --------- Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
6ba2260178 |
feat(providers): add CLOVA Studio, InternLM and Ant Ling API-key providers (#8077)
* feat(providers): add CLOVA Studio, InternLM and Ant Ling API-key providers Adds three OpenAI-compatible frontier-lab providers, closing regional gaps in the catalog (Korea had none; the Shanghai AI Lab and Ant Group families were both missing). - clova-studio: Naver HyperCLOVA X (HCX-007 reasoning, HCX-005 multimodal) on the current clovastudio.stream.ntruss.com host. The legacy clovastudio.apigw.ntruss.com endpoint is being deprecated and is not used. - internlm: Shanghai AI Lab Intern-S1 family (intern-s1-pro is a 1T MoE). Ships a free monthly quota, so it is flagged hasFree. - ant-ling: Ant Group / inclusionAI Ling-2.6-1T and Ring-2.6-1T. All three endpoints were smoke-tested: each returns HTTP 401 on <baseUrl>/models (endpoint live, awaiting auth) and resolves against public DNS. All three are registered for live model discovery, so their catalogs refresh from upstream. Known limitation: the ant-ling model ids are best-effort from public docs and are NOT verified against a live /v1/models response, which requires an API key. This is recorded in the registry comment and in the provider authHint so it is visible to operators rather than silently assumed. Its baseUrl is likewise not published in the public docs and was found by smoke test. Two AI SUTRA was evaluated for this batch and deliberately excluded: its documented endpoint api.two.ai does not resolve in public DNS (ENOTFOUND against 1.1.1.1 and 8.8.8.8, with www.two.ai resolving as control). The translate-path golden snapshot is regenerated; the change is additive only (209 -> 212 keys, exactly the three new providers, none removed or altered). * feat(providers): verify ant-ling against official docs, add Ling-2.6-flash and free tier The ant-ling entry was added with model ids marked best-effort because they could not be checked without an API key. Ant Ling's own documentation turns out to publish enough to verify them, so the uncertainty is now resolved: - The quickstart sample uses base_url "https://api.ant-ling.com/v1" with model "Ling-2.6-1T", confirming both the endpoint and the exact casing. - The pricing page bills exactly three models over the API, so Ling-2.6-flash was missing from the catalog and is added. - Each account gets 500,000 free tokens per day (resets 02:00 UTC+8, no rollover), so the provider is flagged hasFree with a freeNote. The Ming family (Ming-Flash-Omni, Ming-Light) is deliberately left out: it is documented as open-source / Ling Studio only and does not appear on the pricing page, so it is not served over this chat-completions API. That reasoning is recorded in the registry so it is not re-litigated later. The authHint no longer claims the ids are unverified, and now points at the API console (https://chat.ant-ling.com/open) where keys are actually created. Same correction applied to the en, pt-BR and vi message catalogs. * docs: sync provider counts to 283 and regenerate the provider reference --------- Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
9b2968fc07 |
fix(resilience): add max/step to NumberField for provider cooldown inputs (#8107) (#8203)
* fix(ci): resolve upstream-inherited check failures * fix(resilience): add max/step to NumberField for provider cooldown inputs Fixes #8107: integer input rejects typed value on Chrome/Windows because frontend allowed values exceeding backend zod schema limits. - Add and props to NumberField component - Apply correct limits for provider cooldown (min: 300000ms, max: 3600000ms) - Apply correct limits for waitForCooldown (maxRetries: 10, maxRetryWaitSec: 300) - Apply correct limits for requestQueue (requestsPerMinute: 1000, minTimeBetweenRequestsMs: 10000, concurrentRequests: 100, maxWaitMs: 300000, maxQueueDepth: 100000) - Apply correct limits for connection cooldown (baseCooldownMs: 3600000, maxBackoffSteps: 100) - Apply correct limits for provider breaker (failureThreshold: 1000, degradationThreshold: 1000, resetTimeoutMs: 300000) - Apply correct limits for combo cooldown (maxWaitMs: 30000, maxAttempts: 10, budgetMs: 300000) - Apply correct limits for quota share concurrency (enabled only - no numeric limits) * fix(resilience): clamp cooldown bounds before save + regression test (#8107) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
b640b69078 |
feat(codex): support reference image edits (#8122)
* feat(codex): support reference image edits * docs(changelog): add Codex edit fragment * fix(codex): harden image edit admission * fix(security): redact image error credentials * fix(security): close error redaction bypasses * feat(codex): support multiple image references * fix(codex): preserve reference candidate semantics * chore(quality): rebaseline image-generation-handler.test.ts for #8122 codex image edits own-growth Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
71887ae529 |
fix: restore OAuth auto-refresh for gemini-cli connections (#8232)
* fix: restore OAuth auto-refresh for gemini-cli connections
gemini-cli OAuth connections had no PROVIDERS registry entry at all, so
the token-refresh health check permanently skipped them once the access
token expired, forcing a full re-authentication instead of using the
still-valid refresh token.
Two layered gaps, both required:
1. supportsTokenRefresh()'s explicit allow-set had "gemini" but not
"gemini-cli" (the id actually stored on these connections), and its
PROVIDERS[e].tokenUrl fallback also failed since...
2. ...open-sse/config/providers registry had zero entry for "gemini-cli"
at all: no clientId/clientSecret/tokenUrl/refreshUrl, so even the
generic refresh path had nothing to refresh with.
Adds a "gemini-cli" registry entry mirroring antigravity's Google
Cloud Code OAuth shape, reusing the same well-known public Gemini CLI
client credentials already embedded (and already used, unchanged, by
the Gemini Studio API-key provider's own oauth block) via
resolvePublicCred("gemini_id"/"gemini_alt"). Adds "gemini-cli" to the
explicit refresh allow-set, the Google-refresh dispatch case, and the
15-minute non-rotating-token proactive lead alongside antigravity/agy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: register gemini-cli in canonical provider list (provider-consistency gate)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(gemini-cli): use ANTIGRAVITY_RUNTIME_BASE_URLS (renamed by #8013 antigravity split)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: seanford <seanford@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
cc17b304ab |
fix(sse): Gemini TPM/RPD quota classification + combo cooldown-wait resilience (#8213)
* fix(sse): Gemini TPM classification, combo-cooldown-wait for auto/quota-share, and target-timeout floor
Gemini TPM/RPM 429s were misclassified as QUOTA_EXHAUSTED because
sanitizeErrorMessage() truncates to the first line, hiding Google's
metric name and retry hint on lines 2-3. Added a rawMessage field
(internal-only, never reaches the client) and classifyGeminiQuotaMetricFromText()
to classify from the untruncated text, reordered ahead of the generic
credits/daily-quota checks.
Widened comboCooldownWaitEnabled (wait out a short transient cooldown
instead of crystallizing a 429/503) from quota-share-only to also cover
auto-strategy combos, and raised the wait ceiling to 65s/130s-budget/90s-cap
to match Gemini's ~60s TPM/RPM windows.
The per-target timeout (DEFAULT_COMBO_TARGET_TIMEOUT_MS, 120s) was shorter
than the new 130s cooldown-wait budget, so a target could get cut off
mid-wait with a synthetic 524 instead of completing the retry. Added
resolveComboTargetTimeoutMsForCombo()/isComboCooldownWaitEligible() in
comboConfig.ts to raise the per-target floor to budgetMs+buffer only for
wait-eligible strategies (auto/quota-share), verified live: a 12-request
concurrent burst against a TPM-exhausted combo went from 2/12 succeeding
(10 x 524) to 12/12 succeeding with zero 503/524.
Also: liveGeminiShared.ts's sendAndValidate now fails fast on a 503
instead of retrying past it, and the health dashboard + request logger
surface TPM stats alongside RPM/RPD.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo-exhausted rejection logs now capture request body + attempted models
recordRejectedRequestUsage() (the fast path for combo requests that never
reach handleChatCore, e.g. all targets locked by resilience cooldown)
hardcoded provider: "-" and never passed a request body to saveCallLog(),
so /dashboard/logs entries for these failures were nearly useless for
debugging: no way to see the client's request or which models were tried.
- recordRejectedRequestUsage() now accepts requestBody and persists it
through the existing saveCallLog() artifact mechanism (same path
handleChatCore's own logging uses).
- Added summarizeComboAttemptedModels(), which reads the combo's own model
list (always available, unlike the response's combo-diagnostics headers —
a model-level resilience-lockout skip never touches the
exhaustedProviders/exhaustedConnections sets those headers are built
from) to populate a real "provider" value instead of "-".
- Wired both into the call site in src/sse/handlers/chat.ts.
NOTE: unrelated to the Gemini TPM/combo-cooldown-wait fix on this branch —
landed here per operator request, to be split into its own branch/PR.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* feat(sse): synthetic streaming keep-alive event + 5-minute Gemini cooldown-wait ceiling
Many clients enforce a first-SSE-byte timeout, which made it unsafe to wait
out a longer upstream rate-limit cooldown on a streaming request — the
client would abandon the connection before any bytes arrived. This landed
in two parts:
1. Synthetic startup "thinking" event (OpenAI chat/completions format):
the already-existing withEarlyStreamKeepalive wrapper (open-sse/utils/
earlyStreamKeepalive.ts, wired into /v1/chat/completions, /v1/messages,
/v1/responses since #2544) opens the SSE stream immediately once a
request runs past its threshold, but only ever sent empty/no-op
keepalive frames. Added a `startupFrame` option (defaults to
`keepaliveFrame` — zero behavior change unless a route opts in) so the
very first frame can carry real content instead. Wired
OPENAI_STARTUP_THINKING_FRAME (a reasoning_content delta: "OmniRoute:
got request, sending to provider") into /v1/chat/completions only —
Claude Messages and Responses API formats both require a preceding
envelope event (message_start / response.created) that a synthetic
pre-dispatch frame can't safely fabricate without risking a duplicate
envelope once the real stream arrives, so those two routes keep their
existing (safe, proven) keepalive frames unchanged.
2. Raised the "wait out a known cooldown, then retry" ceiling to 5 minutes
for both retry mechanisms, now that a client-side first-byte timeout is
no longer a risk on the (opted-in) route:
- comboCooldownWait (auto/quota-share combos, open-sse/services/combo.ts):
maxWaitMs hard clamp raised 90s -> 300s (src/lib/resilience/settings/
normalize.ts); defaults raised to maxWaitMs:90s/maxAttempts:5/
budgetMs:300s. comboConfig.ts's resolveComboTargetTimeoutMsForCombo
already derives the per-target timeout floor from budgetMs, so it
tracks the new ceiling with no further changes.
- waitForCooldown (direct, non-combo model requests, src/sse/handlers/
chat.ts): this mechanism had NO cumulative cap before — only a
per-wait cap (maxRetryWaitMs) and a retry count (maxRetries), so
maxRetries x maxRetryWaitMs could exceed 5 minutes with no ceiling.
Added a budgetMs field (mirrors comboCooldownWait) to
WaitForCooldownSettings/CooldownAwareRetrySettings, threaded a
requestRetryBudgetLeftMs tracker through chat.ts's requestAttemptLoop
(mirrors combo.ts's comboCooldownBudgetLeftMs), and made
getCooldownAwareRetryDecision refuse to wait once the cumulative
budget is exhausted even if the single wait is under maxRetryWaitMs.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): extend the synthetic keep-alive thinking event to /v1/responses
Live incident (OpenClaw, log id 1784407081908-cbc24f): a /v1/responses
request to gemini/gemma-4-31b-it took 56s to produce a first byte and the
client disconnected (499 request_signal_aborted) — the same client-first-byte-
timeout problem the previous commit fixed for /v1/chat/completions, but
/v1/responses only had the generic bare-comment keepalive (no content), so it
wasn't covered.
Added RESPONSES_STARTUP_THINKING_FRAME: a self-contained synthetic reasoning
item (response.output_item.added -> reasoning_summary_part.added ->
reasoning_summary_text.delta -> reasoning_summary_part.done), opened AND
closed within this one frame rather than left dangling — it never carries a
response_id, so it can't collide with the real upstream response's own
independent response.created lifecycle that follows. Mirrors the abbreviated
delta+part.done close pattern open-sse/utils/stream.ts's own
emitSyntheticResponsesReasoningSummary already uses for real mid-stream
reasoning content.
Wired into src/app/api/v1/responses/route.ts via the startupFrame option
added in the previous commit.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo cooldown-wait vars reset every setTry, crystallizing a bogus 503 instead of waiting
Live incident (log id 1784416706646-51): a request to the "default" combo
(strategy=auto, maxSetRetries=3) hit a real Gemini TPM 429 on both gemma-4
targets, correctly classified as a short 40s rate_limit lockout — then
crystallized a 503 "all upstream accounts are inactive" in 6.9s instead of
ever reaching the cooldown-aware wait.
Root cause: `lastError`/`earliestRetryAfter`/`lastStatus` were declared with
`let` INSIDE the `for (setTry...)` loop body, so they reset to null at the
start of every set-try. When both targets lock out on setTry 0, every
subsequent setTry (1..maxSetRetries) pre-skips both targets via the
isModelLocked check with no real dispatch — so on the FINAL setTry (the only
one whose values the post-loop decision reads, since it's gated behind
`if (setTry < maxSetRetries) continue`), lastStatus was null, hitting the
"!lastStatus" branch (ALL_ACCOUNTS_INACTIVE 503) and completely bypassing the
comboCooldownWaitEnabled / earliestRetryAfter wait logic — even though a
real 429 with a known ~40s retry-after WAS observed on setTry 0.
This bug predates today's Gemini TPM work (any combo with maxSetRetries > 0
whose targets all lock out on the first pass was affected) but was masked in
existing tests: the "auto strategy (2 models...)" regression test uses
maxSetRetries: 0, so it only ever runs ONE setTry iteration and never
exercises the reset-on-retry path. It also explains why the dedicated
12-concurrent-request burst test passed cleanly — with concurrent requests,
timing variance meant some request's FINAL setTry iteration still had a live
target to dispatch to, giving lastStatus/earliestRetryAfter fresh data. A
single isolated request has no such luck.
Fix: hoist lastError/earliestRetryAfter/lastStatus to just inside
dispatchWithCooldownRetry, before the setTry loop, so they persist across
set-tries (still reset fresh on each recursive dispatchWithCooldownRetry()
call after a wait, which is correct). recordedAttempts/fallbackCount/
exhaustedProviders etc. are intentionally left per-iteration (unrelated to
this bug).
New regression test in tests/unit/combo-quota-share-cooldown-wait.test.ts
reproduces the exact live scenario (2 targets, both lock out on setTry 0,
maxSetRetries: 3) — confirmed red (503) against the pre-fix code, green
(200, waits and retries) against the fix.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* test(sse): extend live Gemini workload to Responses API + add large-context TPM test
Two additions to the live Gemini test suite, both live-verified against the
dev instance:
1. sendAndValidate() (tests/integration/liveGeminiShared.ts) now accepts an
apiFormat: "chat" | "responses" parameter, building the Responses-API
request shape (input array, max_output_tokens) and parsing its SSE events
(response.output_text.delta / response.reasoning_summary_text.delta /
response.completed) via the new readResponsesSSEStream(). Wired into two
new tests in live-gemini-workload.test.ts ([30]/[31]), mirroring the
existing Chat Completions streaming coverage. Verified live: 24/25 + 5/5
payloads succeeded end-to-end through the new code path (the one failure
was a ~300s test-client fetch timeout unrelated to the Responses API code
itself — a separate, not-yet-addressed test-harness limitation).
2. genHugeContextMessage() builds a single message large enough (~4
chars/token estimate) to approach or exceed Gemini's free-tier TPM ceiling
(16000 input tokens/min for gemma-4) by itself. Every other prompt
generator in this file tops out around 1-2k tokens — nowhere near that
ceiling — so none of the existing workload tests ever exercised a REAL TPM
429, only RPM-style rate limiting. tests/integration/gemini-large-context-tpm.test.ts
sends two ~12-13k-token requests back-to-back (comfortably exceeding
16000/min together) to exercise the full path against production Gemini:
TPM classification, the comboCooldownWait retry, and the synthetic
keep-alive frame on a genuinely slow request.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): abandoned combo target dispatch now observes its own per-target timeout, fixing a permanent "pending" dashboard leak
Live incident (dashboard log id 1784418258231-14961a, reported as "an ongoing
request even though there's already a 200"): a combo target dispatch
abandoned by comboTargetTimeoutMs (open-sse/services/combo/targetTimeoutRunner.ts)
left a permanent phantom "pending" entry in the dashboard, even after the
overall combo request had already succeeded via a different retry.
Root cause: chatCore.ts's createStreamController — and everything downstream
that depends on it (withRateLimit's Promise.race against Bottleneck,
acquireAccountSemaphore) — only ever watches clientRawRequest.signal, which
is the ORIGINAL client's request signal (set once via buildClientRawRequest
and reused unchanged across every target dispatch in a combo). It has no
connection to targetTimeoutRunner.ts's OWN AbortController
(target.modelAbortSignal), which is what actually fires when
comboTargetTimeoutMs (300s) elapses. src/sse/handlers/chat.ts's
handleSingleModel bridge between combo.ts and handleSingleModelChat received
`target.modelAbortSignal` but silently dropped it — never forwarded it
anywhere. So when a target got abandoned (e.g. stuck inside a wedged
Bottleneck rate-limiter queue, see the WEDGED force-reset log line from the
same incident), its per-target timeout fired and let the COMBO move on and
retry successfully elsewhere — but the abandoned dispatch's own promise
chain never learned it had been superseded, so it hung forever waiting on a
signal that was never going to fire, and trackPendingRequest(false) (the
finalize call) never ran.
Fix: thread target.modelAbortSignal through as a new modelAbortSignal
runtimeOption, and merge it into clientRawRequest.signal (via the existing
mergeAbortSignals helper from open-sse/executors/base.ts) right before
dispatch, so an abandoned target's own promise chain now observes its abort
and can reach its cleanup path — new resolveDispatchClientRawRequest() makes
this mechanically testable in isolation.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo cooldown-wait state recording, rate-limit wedge recovery, OpenAI-format SSE error frames
Five related fixes surfaced by live incidents (dashboard log ids 1784457764961-73,
1784465227489-a2cbc0, 1784504040241-6f8b9a) while validating the Gemini TPM/cooldown-wait
work on this branch against real OpenClaw traffic:
- combo.ts: the model-lockout bail-out branches in dispatchWithCooldownRetry never
recorded lastStatus, so once every target in a set hit an existing lockout the final
check crystallized a bogus ALL_ACCOUNTS_INACTIVE 503 instead of reaching the
cooldown-wait decision, even with a real 429 + short retry-after observed.
- combo.ts/combo/types.ts: the "all credentials cooling down" pre-dispatch rejection
(buildModelCooldownBody) nests its retry hint as error.retry_after/reset_seconds, not
the top-level retryAfter every other 429 shape uses — combo's extraction only read the
latter, so earliestRetryAfter stayed null for this shape even after lastStatus was fixed.
- rateLimitManager.ts: the wedge-recovery watchdog used disconnect(), which releases the
heartbeat timer but never rejects jobs already QUEUED on that instance — orphaned
dispatches hung until the outer ~300s per-target timeout, well past real clients'
patience. Switched to stop({ dropWaitingJobs: true }), safe because the wedge condition
already requires RUNNING===0 && EXECUTING===0.
- earlyStreamKeepalive.ts: the in-band error frame emitted after committing to a 200 SSE
stream was hardcoded to Anthropic's `event: error` convention for every route, including
the OpenAI-format ones (/v1/chat/completions, /v1/responses) where that framing is
either invisible or malformed to a plain data-line parser. Added per-route
OPENAI_CHAT_ERROR_FRAME / OPENAI_RESPONSES_ERROR_FRAME and wired them in.
- chatCore.ts: persisted a synthetic clientResponse error body even when the client had
already disconnected (AbortError) before that body was ever computed — misleading the
dashboard into showing "what the client received" for a response that was never sent.
Also: RequestLoggerDetail.tsx — Provider/Client Event Stream panes lost their collapse
toggle when StreamSection replaced the collapsible PayloadSection (
|
||
|
|
5d764a40ee |
fix(logs): stop the async-EPIPE log-flood loop at its ignition point (#8207)
* fix(logs): stop an async EPIPE becoming an uncaughtException loop A raw process.stderr.write into a broken pipe fails asynchronously, so the try/catch around it never sees the failure. The stream emits 'error'; with no listener on process.stderr Node re-throws it as an uncaughtException; the framework's handler logs that through console.error; and the patched console writes back into the same dead stream. That closes a self-sustaining loop. Attach an 'error' listener to process.stdout and process.stderr. Node only converts a stream 'error' into an uncaughtException when the emitter has no listener, so the listener alone terminates the cycle. Measured in a spawn harness over 1.5s: 3,387 uncaught exceptions before, 0 after. Absorb EPIPE only. Attaching a listener otherwise makes every stream error on those streams non-fatal process-wide, so ENOSPC, EBADF and the rest are re-raised on a fresh stack to preserve today's crash semantics. The accompanying test asserts that in a child process, because node:test attributes any in-process uncaughtException to the running test. Add a test-only reset() to undo the patched console and the listeners: test:unit:fast runs --test-isolation=none, so leaked state would reach every subsequent test file. Refs #8181 * fix(logs): bound interceptor disk writes and self-heal a missing log dir Two write-path defects in the same file, both independent of the loop itself. writeEntry appended with no rate limit, so while the loop spun it wrote unbounded lines to disk (4.3 GB in 90 minutes in the reported incident). Apply the same policy #1006 established in structuredLogger -- 50 writes/sec, a 5s dedup window, a bounded tracking map -- but scoped to `error` entries only. That scoping is deliberate: structuredLogger applies its limiter solely to error() and fatal(), whereas writeEntry serves all five of log/info/warn/error/debug across ~800 non-error call sites. Capping those would silently drop routine logging from the Console Log Viewer's file. A test asserts non-error levels stay unlimited. ensureDir() ran once in initConsoleInterceptor and never again, so a log directory removed while the process was alive made every later append throw ENOENT into a bare catch -- console file-logging then stopped permanently with nothing surfaced anywhere. Recreate the directory and retry once, and report the failure exactly once through the unpatched stderr so it cannot recurse through the patched console or become a flood of its own. Refs #8181 * fix(logs): skip raw stderr writes to a stream already known dead error() and fatal() write with a raw process.stderr.write wrapped in try {} catch {}. The comment on that line says the raw write exists to avoid Next.js console patching "that triggers EPIPE loops" -- but on a broken pipe the write fails asynchronously, so the catch never sees it, and the resulting stream error is what ignites the loop. Skip the write when the stream is already destroyed or ended, falling through to the file sink as before. The listener added earlier is what breaks the cycle; this stops the ignition point firing into a dead stream in the first place. The existing try/catch is retained for the synchronous cases it always covered. The #1006 suppression policy and its call sites are untouched. Refs #8181 * fix(logs): install the stdio guard independently of console interception initConsoleInterceptor() returns early when APP_LOG_TO_FILE=false, and when the log directory cannot be created. The stdio 'error' listeners were installed after that return, so in those supported configurations no listener was attached at all. structuredLogger's raw stderr writes still happen there, and an ordinary broken pipe raises an async EPIPE without destroyed or writableEnded being set first, so the guard in safeStderrWrite does not cover it either. The loop this change exists to prevent was therefore still reachable with file logging turned off. Extract installStdioErrorGuard() and call it before the early return. It is idempotent and cleared by reset(). A new test asserts, in a child process, that both listeners are present when APP_LOG_TO_FILE=false. Also restore APP_LOG_TO_FILE and APP_LOG_FILE_PATH in the test's after() hook. test:unit:fast runs with --test-isolation=none, so the previous top-level mutations leaked into later test files, leaving file logging enabled against a path this file deletes. Refs #8181 |
||
|
|
4fd1f0f15c |
fix(guardrails): align INPUT_SANITIZER request masking gate (#8093) (#8124)
Scoped to guardrails/security: dropped the unrelated js-yaml/tar/shell-quote/ brace-expansion override bumps, and isolated sanitizer-residual-policy.test.ts to a tmp DATA_DIR so it no longer touches the real storage.sqlite. Co-authored-by: RaviTharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: rafaumeu <rafael.zendron22@gmail.com> |
||
|
|
888c872459 |
refactor(antigravity): align official clients and callable catalog (#8013)
* fix(antigravity): preserve protocol fidelity and fail closed * chore: add PR-numbered changelog fragment * test: split oversized Antigravity suites * refactor(antigravity): align official IDE and CLI identities * fix(antigravity): align catalog with callable models * test(antigravity): update 2 test files to renamed version-cache API (#8013 fix) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Co-authored-by: backryun <backryun@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com> Co-authored-by: Probe Test <probe@example.com> |
||
|
|
5660bdefbd |
fix(windows): add windowsHide to all child process spawns (#8131) (#8167)
* fix(windows): add windowsHide to all child process spawns (#8131) On Windows, child processes spawned without windowsHide: true cause transient conhost.exe/cmd console windows to flash open. Audited all spawn/exec/execFile/execSync/execFileSync call sites and added windowsHide: true where missing. Files patched: - src/mitm/manager.ts (MITM server spawn) - src/mitm/systemCommands.ts (sudo/system command spawn) - src/mitm/inspector/systemProxyConfig.ts (execFile wrapper) - src/shared/services/cliRuntime.ts (CLI spawn + npm execFileSync) - src/lib/plugins/loader.ts (plugin host spawn) - src/lib/providerModels/cursorAgent.ts (cursor binary spawn) - src/lib/cloudflaredTunnel.ts (cloudflared spawn) Unix-only call sites (shell: /bin/bash, which) are unaffected. electron/main.js already had windowsHide: true. * fix(windows): cover remaining spawn sites missed by #8131 windowsHide sweep Extends the #8131 windowsHide audit to the three call sites the original sweep missed: ServiceSupervisor.start() and processManager.startProcess() (both spawn() embedded-service child processes), and installers/utils.ts::buildNpmExecOptions() (the execFile() options runNpm() uses to install services). All three now always set windowsHide: true so no transient conhost.exe/cmd console window flashes open on Windows. The two spawn() options objects are factored into small, pure, exported builder functions (buildServiceSpawnOptions, buildCliproxyapiSpawnOptions) so the regression test can assert on the constructed options directly, since both call sites use a bare named `import { spawn } from "node:child_process"` that ESM live-binding semantics make unmockable without --experimental-test-module-mocks (not currently enabled repo-wide). Bumps config/quality/file-size-baseline.json for cloudflaredTunnel.ts 934->935 (the PR's own +1 windowsHide line at the existing spawn options object). Co-Authored-By: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local> Co-authored-by: Probe Test <probe@example.com> Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
e7f965c9cc |
fix(compression): persist Headroom minRows (set 5 and reload keeps 5) (#8058)
* fix(compression): persist Headroom minRows (set 5 and reload keeps 5) Fixes diegosouzapw/OmniRoute#8056. Headroom detail settings had a Save-looking form but EngineConfigPage only persisted aggressive/ultra via SETTINGS_SUBOBJECT, so minRows always reseeded to the schema default (8) after reload. - Add HeadroomConfig + DEFAULT_HEADROOM_CONFIG (minRows: 8) - Accept headroom in compressionSettingsUpdateSchema (minRows 2..10000) - Normalize/store headroom in get/updateCompressionSettings - Register headroom in EngineConfigPage SETTINGS_SUBOBJECT so Save works - Merge settings.headroom into stacked stepConfig for runtime apply - Thread minRows through preview API + EngineConfigPage preview payload - Tests: schema/DB round-trip, engine apply, stacked merge, UI Save→PUT 5 * chore(quality): rebaseline compression.ts + strategySelector.ts own-growth (#8056 headroom minRows) --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> |
||
|
|
7ae17168db |
fix(security): decouple request PII redaction from injection mode (#8102)
* fix(security): decouple request PII redaction from injection mode PII_REDACTION_ENABLED now rewrites request PII independently of INPUT_SANITIZER_MODE, so the enterprise recipe (MODE=block + PII on) actually redacts. Also cover Responses API string input/prompt shapes and correct docs that claimed MODE=redact strips injection text. Refs: #8092 #8093 #8094 #8096 #8097 * test(security): drop no-explicit-any in sanitizer unit tests Unblocks CI lint/quality ratchet on the PII redaction PR by typing chat-like payloads instead of casting to any. --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> |
||
|
|
e31c4d31e2 |
fix: classify Google quota exhaustion responses (#8071)
Recognize Google RESOURCE_EXHAUSTED responses that include a billing-period reset window while preserving transient rate-limit classification for generic exhaustion messages. Fixes #8060 |
||
|
|
b8ec0aa218 |
fix(providers): refresh Baidu ERNIE and Qianfan website URLs (#6271) (#8128)
Point dashboard provider cards at current Baidu developer landings instead of the deprecated yiyan nag page and the 301ing wenxinworkshop path. Co-authored-by: LandLord64 <ulofeuduokhai@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
dbdc7daade |
feat(compression): per-model/endpoint compression exclusion filter (#8034) (#8064)
* feat(compression): per-model/endpoint compression exclusion filter (#8034) * chore(quality): rebaseline compression.ts own-growth 845->850 (#8034 exclusions persistence) --------- Co-authored-by: Probe Test <probe@example.com> |
||
|
|
e392a39047 | feat: native Fish Audio TTS provider on /v1/audio/speech (#8099) (#8164) | ||
|
|
b954a3a60f | fix(oauth): warn instead of silently opening unreachable localhost redirect for LAN-IP Codex/xAI/Grok OAuth (#8046) (#8152) | ||
|
|
6302a78657 |
fix(db): register SIGHUP handler and stop force-killing server on win32 stop paths (#8045) (#8148)
Windows console-window close delivers CTRL_CLOSE_EVENT, which Node/libuv maps to a JS-visible SIGHUP event. initGracefulShutdown() only listened for SIGTERM/SIGINT, so closing the window never ran cleanup() (WAL checkpoint + closeDbInstance()), leaving storage.sqlite's WAL un-checkpointed for the next launch. Separately, process.kill(pid, "SIGTERM") on win32 unconditionally force-terminates the target process instead of delivering an interceptable signal. The CLI's own stop paths (ServerSupervisor.stop() and runStopCommand()) sent it immediately on every stop, racing and beating the child's own async graceful shutdown before the WAL checkpoint could run. Fix: - src/lib/gracefulShutdown.ts: register a SIGHUP handler alongside SIGTERM/SIGINT. - src/shared/platform/windowsProcess.ts (new): stopProcessGracefully() skips the immediate SIGTERM on win32 (letting the target's own CTRL_C/CTRL_CLOSE handling run) and polls before escalating to SIGKILL; unchanged immediate SIGTERM behavior on POSIX. - bin/cli/runtime/processSupervisor.mjs and bin/cli/commands/stop.mjs: use stopProcessGracefully() instead of an unconditional process.kill(SIGTERM). Regression tests: tests/unit/graceful-shutdown-sighup-8045.test.ts (reuses the RED probe from the triage analysis) and tests/unit/windows-process-stop-8045.test.ts. |
||
|
|
1c116e0501 |
fix(cli): merge node bin dir into CLI healthcheck PATH for codex detection (#8036) (#8156)
checkRunnable() built the healthcheck spawn's minimalEnv.PATH from the caller's PATH only, never merging in this Node's own bin dir the way locateCommand's known-path search already does. npm-installed CLIs like codex are `#!/usr/bin/env node` shebang scripts, so when the server is launched with a minimal PATH (systemd/docker/PM2/Electron) lacking node's dir, the healthcheck spawn fails even though the binary was correctly located, and the tool shows as undetected. Extracted the merge into a new buildHealthcheckPath() helper (cliRuntimeHealthcheckPath.ts) to keep cliRuntime.ts within its frozen file-size ceiling. |
||
|
|
813bea4184 |
feat(vnc-session): persistent noVNC browser login for web-cookie providers (#7892)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168) * feat(vnc-session): persistent noVNC browser login for web cookie/token providers ## Why (the headless-install problem) OmniRoute's web cookie/token providers (ChatGPT Web, Gemini Web, Claude Web, DeepSeek Web, …) need a live browser session, but the gateway normally runs **headless** — as a systemd service, inside Docker, or on a VPS with no display. There is no desktop for the operator to log into the provider in. Today the operator has to obtain the session cookie/token *out of band* (open a real browser elsewhere, export cookies, paste them into the connection row). That is fiddly, breaks on every provider UI change, and is a non-starter on a headless box where you can't open a browser at all. This PR adds an **on-demand interactive login**: OmniRoute boots a containerized browser that exposes a noVNC web UI at the host. The operator opens that URL in *their own* browser, logs in normally, and OmniRoute then harvests the resulting cookies / localStorage back into the provider's `provider_connections` row over the DevTools Protocol. No display required on the host — the headless server renders the login into a container and the human just drives it through a web page. ## How we ran into this - The shipped `dist/` bundle has **no App Router source**, so the only visible seam was `dist/server-ws.mjs`'s `http.createServer` monkeypatch. That seam is **dead**: Next's standalone `startServer` creates its own http server in a way that bypasses the override, so a route registered there never fires (debug logs confirmed: zero requests reached it). The real seam is the Next **App Router** (`src/app/api/...`), which lives in the dev tree, not `dist/`. - **Chromium ≥130 forces the remote-debugging port onto `127.0.0.1`** and ignores `--remote-debugging-address=0.0.0.0`. A plain published port can't reach it, so cookie harvest needs an in-container TCP bridge to republish the loopback CDP onto `0.0.0.0`. We shipped that bridge, but the cleaner default is **Firefox** (`jlesage/firefox`): its debugger binds `0.0.0.0` out of the box, so harvest works with no bridge at all. - The CDP harvester **hung forever** on the first tries: the message handler was defined but never attached to the socket, so every `send()` promise stayed pending. We replaced Playwright's `connectOverCDP` (which stalls through the bridge) with a **raw `ws` client** and wired the handler — now resolves. ## What New management API (scoped like the other admin endpoints via `requireManagementAuth`): | Method | Path | Purpose | | --- | --- | --- | | GET | `/api/vnc-session` | list active sessions + supported providers | | GET | `/api/vnc-session/:provider` | session state | | POST | `/api/vnc-session/:provider/start` | boot browser container → returns `vncUrl` | | POST | `/api/vnc-session/:provider/harvest` | persist cookies into the provider row | | POST | `/api/vnc-session/:provider/touch` | defer idle auto-stop | | DELETE | `/api/vnc-session/:provider` | stop + remove the container | ## Implementation - `src/lib/vncSession/manifest.ts` — provider → login URL + cookie/token map + config - `src/lib/vncSession/harvest.ts` — raw-CDP cookie/localStorage harvester (`ws`) - `src/lib/vncSession/service.ts` — docker lifecycle, port allocation, idle sweep, DB write - `src/app/api/vnc-session/**` — App Router routes - `src/lib/gracefulShutdown.ts` — tears down running login containers on exit ## Browser image choice Default is **`jlesage/firefox`** (0.0.0.0-friendly CDP, no bridge). The Chromium image + in-container bridge lives under `docker/vnc-browser/chromium`, selectable via `OMNIROUTE_VNC_IMAGE`. See `docker/vnc-browser/README.md`. ## Config (env) `OMNIROUTE_VNC_IMAGE`, `OMNIROUTE_VNC_CONTAINER_VNC_PORT`, `OMNIROUTE_VNC_CONTAINER_CDP_PORT`, `OMNIROUTE_VNC_PROFILE_DIR`, `OMNIROUTE_VNC_IDLE_MS`, `OMNIROUTE_VNC_MAX_MS`, `OMNIROUTE_VNC_MAX_SESSIONS`, `OMNIROUTE_DOCKER_BIN` — all documented in the docker README. ## Tests `tests/unit/vnc-session.test.ts` — manifest lookup + credential mapping (cookie / token / whole-jar). All passing via the Node test runner. ## Notes - Docker is the only external dependency; if the `docker` CLI is missing, `start` throws a clear error and shutdown is a no-op. - No secrets are returned by any endpoint — only session metadata + ports. Co-authored-by: Sora <138304505+Capslockb@users.noreply.github.com> Co-authored-by: Bernardo <138304505+Capslockb@users.noreply.github.com> * refactor(vnc-session): derive provider credentials from shared contract * fix(vnc-session): harden CDP harvesting and credential filtering * refactor(vnc-session): scope lifecycle to provider connections * fix(vnc-session): sanitize and scope management routes * fix(vnc-session): use canonical provider list in API * test(vnc-session): align coverage with canonical manifest * docs(vnc-session): align browser setup with current implementation * fix(security): loopback-gate /api/vnc-session (Hard Rule #15/#17) The new /api/vnc-session/* routes spawn Docker containers via child_process.spawn (src/lib/vncSession/service.ts) but were never registered in LOCAL_ONLY_API_PREFIXES or SPAWN_CAPABLE_PREFIXES, so they were reachable from non-loopback callers (any manage-scope API key or dashboard session over a tunnel) - the same CVE class (GHSA-fhh6-4qxv-rpqj) those constants exist to close. Register VNC_ROUTE_PREFIX (already exported but unused in manifest.ts) in both prefix lists, and add a regression test asserting isLocalOnlyPath()/isLocalOnlyBypassableByManageScope() correctly classify the new prefix. Co-authored-by: CAPSLOCKB <138304505+Capslockb@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> |
||
|
|
e86e5bcc51 |
feat(media): Adobe Firefly image + video generation provider (#8006)
* feat(media): Adobe Firefly image + video generation provider
Add unofficial adobe-firefly media provider with full OpenAI-compatible
image and video generation: Nano Banana / GPT Image families, Sora 2,
Veo 3.1 (standard/fast/reference), and Kling 3.0.
Supports browser session cookies (auto IMS token exchange) or direct
IMS access tokens, async submit-and-poll against Firefly 3P endpoints,
aspect-ratio and resolution controls, and multi-account web-session UX.
Chat completions are intentionally rejected (media-only surface).
Includes unit coverage for registry wiring, payload builders, auth
resolution, and mocked generate happy-paths.
* fix(adobe-firefly): clio auth, discovery fallback, credits balance
Root-cause 401 invalid token against live firefly.adobe.com captures:
generate/discovery use x-api-key + IMS client_id clio-playground-web
(not projectx_webapp). Align headers/origin, dual cookie to IMS exchange
(clio first, Express fallback), BKS poll rewrite for /jobs/result.
Models: parse POST /v2/models/discovery + static fallback catalog from
adobe/get_models.txt; expand image/video registries.
Limits: GET firefly.adobe.io/v1/credits/balance (SunbreakWebUI1) with
total/remaining + free/plan detail quotas. Clarify cookie vs JWT UX.
Unit tests: 27/27 pass.
* fix(adobe-firefly): reject guest tokens from page-only cookies
Live repro with firefly.adobe.com Cookie export: IMS check with
guest_allowed=true returns account_type=guest (no AdobeID). That token
fails generate (401 invalid token) and credits/balance (403
ErrMismatchOauthToken). guest_allowed=false needs adobelogin.com IMS
session cookies which are not present in a page-only Cookie paste.
- Detect/reject guest JWTs; clear error tells user to paste Bearer JWT
- Prefer user JWT from HAR/mixed paste; improve credential extraction
- Update web-cookie + credential UX to recommend Authorization Bearer
Unit tests 29/29.
* fix(adobe-firefly): production auth, Limits, and 408 load handling
Live validation against firefly.adobe.com + packaged VibeProxy:
Auth / credentials
- Prefer IMS user JWT (Bearer from firefly-3p); reject guest tokens from
page-only cookies with an actionable error
- Extract JWT from Bearer, access_token=, IMS sessionStorage tokenValue,
and mixed HAR pastes; prefer non-guest tokens
- Strip JWT from Cookie header (undici Headers.append crash on mixed paste)
- Keep sherlockToken → x-arp-session-id + sanitized Cookie for generate
Limits
- credits/balance → Record quotas (firefly_total / free / plan) so
providerLimits caches them (arrays were ignored)
- Allowlist adobe-firefly + firefly in USAGE_SUPPORTED + APIKEY limits
- Live: 10000 plan credits parsed end-to-end after refresh
Generate
- Browser-shaped gpt-image body (size auto, no extra top-level size)
- Exponential 408 "system under load" retries (8 attempts) with clear
client message that 408 is Adobe capacity, not invalid token
- Live: generate returns proper 408 under load; balance/models stay 200
Tests: adobe-firefly unit suite 33/33 pass.
* fix(adobe-firefly): match live capture headers; add gpt-image-2
- Do not send firefly.adobe.com Cookie to firefly-3p (wrong-origin; soft 408)
- Lift sherlockToken only into x-arp-session-id
- Poll headers match status_check.txt (Bearer + accept, no x-api-key)
- Catalog gpt-image-2 alias → upstream modelVersion "2" (GPT Image 2)
- Shorter 408 retry budget so clients fail fast with clear message
- Unit suite 34/34
* fix(adobe-firefly): always send x-arp-session-id on generate (fixes 408)
Root cause of Bearer JWT → HTTP 408 colligo "system under load":
submit only set x-arp-session-id when sherlockToken was present in a
cookie paste. JWT-only credentials never sent the header, and Adobe
soft-blocks those requests with instant 408 (x-colligo-timeout:0.0).
A/B against a real user IMS token:
- det nonce + synthetic ARP → 200
- random nonce + synthetic ARP → 200
- det nonce without ARP → 408
Match adobe2api / GPT2Image-Pro:
- buildAdobeSubmitNonce = sha256(user_id + prompt[:256])
- buildAdobeArpSessionId = base64({sid, ftr}) synthetic session
- buildAdobeSubmitHeaders always sets both headers
Live adobeFireflyGenerateImage end-to-end: submit + poll → S3 presigned URL.
* fix(adobe-firefly): drop literal cred fallbacks + type-clean tests
Addresses pre-merge review feedback on #8006:
- Removes the `|| "literal"` fallback after resolvePublicCred() in
adobeFireflyApiKey()/adobeFireflyExpressClientId()/adobeFireflyBalanceApiKey()
(open-sse/services/adobeFireflyClient.ts). resolvePublicCred() already
always returns the decoded embedded default, so the literal fallback
was dead code that reproduced the exact env-or-literal anti-pattern
docs/security/PUBLIC_CREDS.md documents as BAD (Hard Rule #11).
- Replaces the 11 `@typescript-eslint/no-explicit-any` casts in
tests/unit/adobe-firefly.test.ts with concrete types
(Record<string, unknown>, Headers, Error-narrowing on the
assert.rejects predicate), matching the pattern already used
elsewhere in this suite. `no-explicit-any` is a hard ESLint error
under tests/ in this repo.
- Freezes file-size baseline entries for the new
open-sse/services/adobeFireflyClient.ts (1958 LOC, new-file cap 800,
mirrors the qoderCli.ts precedent for a legitimately large new
provider client), open-sse/config/imageRegistry.ts (800->821, new
adobe-firefly registry entry) and the +3 LOC growth in
src/lib/usage/providerLimits.ts (1000->1003).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: artickc <artickc@users.noreply.github.com>
|
||
|
|
1b010f6c40 |
feat(sse): add HyperAgent (hyperagent.com) unofficial web provider (#7994)
* feat(sse): add HyperAgent (hyperagent.com) unofficial web provider
Reverse-engineered from live SPA captures (hyperagent/*.txt):
Chat:
- Cookie session auth (full Cookie header)
- New thread via GET /threads/new (or POST /api/threads)
- POST /api/threads/{id}/chat with SPA feature flags + content
- SSE parse of text/session_start/session_end/done events
- Multi-turn sticky threadId + sessionId cache (history prefix + last assistant)
Models:
- Hardcoded catalog from SPA pricing map
- Pretty display names (Claude Fable 5) while wire modelId stays fable etc.
- /v1/models exposes pretty name; chat uses modelId
Limits:
- GET /api/settings/billing/usage → creditBlocks initialUsd/remainingUsd/usedUsd
- USD Credits quota for Limits page
Tests: 15/15 unit/executor-hyperagent
* fix(sse): HyperAgent execution mode + fable-latest wire model (no plan mode)
* fix(sse): document HyperAgent env vars + regenerate golden snapshot
Addresses pre-merge review feedback on #7994:
- Documents HYPERAGENT_USAGE_URL in .env.example and ENVIRONMENT.md
(OMNIROUTE_DATA_DIR was already documented via the sibling PromptQL
provider) so check-env-doc-sync.test.ts passes.
- Regenerates the provider-translate-path golden snapshot to include
the new hyperagent/ha registry entries.
- Swaps the local toNumber() helper in usage/hyperagent.ts for the
canonical @/shared/utils/numeric import (#7879 no-restricted-syntax
rule landed on the release branch after this PR was opened).
- Freezes file-size baseline entries for the new
open-sse/executors/hyperagent.ts (937 LOC, new-file cap 800) and the
+3 LOC growth in src/lib/usage/providerLimits.ts (1000->1003), both
irreducible to this PR's own provider-registration wiring.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
|
||
|
|
79ec594c1f |
fix(embeddings): support secure multimodal inputs (#7978)
* fix(embeddings): support secure multimodal inputs Closes #7956 * fix(embeddings): translate multimodal inputs and harden URL/base64 bounds Reject oversize base64 before format validation to avoid Zod stack overflows, translate canonical items to Jina modality-keyed and Gemini embedContent contracts, and fetch HTTPS media server-side with DNS pinning before provider submission. Closes #7956 * fix(embeddings): pin DNS only for embedding media fetches Default remote-image fetch keeps the previous globalThis.fetch path so image-generation tests and callers stay mockable. Multimodal embeddings still opt into undici DNS pinning for URL media. * fix(embeddings): close 2 SSRF/DoS gaps in secure multimodal input (#7978) Closes two gaps in the multimodal embedding input hardening from #7956: 1. `createPinnedFetch()` (the connection-pinning mechanism that closes the DNS-rebinding TOCTOU window, GHSA-cmhj-wh2f-9cgx) had zero test coverage anywhere in the repo. Writing that test surfaced a real regression: its custom `connect.lookup` only implemented the single-address callback form `(err, address, family)`. Node's autoSelectFamily/Happy Eyeballs (on by default since Node 18) calls `lookup` with `{ all: true }` and requires the array form `(err, addresses[])` — the mismatch threw `ERR_INVALID_IP_ADDRESS` on every real pinned fetch, silently breaking all URL-sourced multimodal embedding requests in production. Fixed by branching on `options.all`. 2. The documented "16 MiB decoded per request" cap was enforced by the Zod schema only for base64-sourced items; URL-sourced items were excluded, and all up-to-32 items were fetched concurrently via `Promise.all` — allowing ~256 MiB in memory at once (16x the documented bound). Fixed by resolving items sequentially with a running byte budget shared across base64 and fetched-URL sources, rejecting once the aggregate is exhausted instead of after over-fetching. Adds tests/unit/remote-image-fetch-pin-dns-connection.test.ts (real loopback-server pinning tests) and a new aggregate-cap test in tests/unit/embeddings-multimodal-7956.test.ts; both were verified to fail against the pre-fix code before the corresponding fix was applied. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
9020ed53f9 |
fix(grok-cli): require full auth.json on OAuth paste import (#7610) (#8027)
* fix(grok-cli): require full auth.json on OAuth paste import (#7610) The Grok Build paste path told operators to paste only the JWT "key" field, which creates connections with refresh_token=null that can never auto-refresh. Require the full ~/.grok/auth.json object (with refresh_token) in OAuthModal, and reject bare JWT pastes with a clear error. * fix(grok-cli): add behavioral test coverage for auth.json paste-import (#7610) Replace the source-regex-only test for the OAuth paste-import path with a real behavioral suite (bare JWT rejected, auth.json missing refresh_token rejected, multi-entry auth.json accepted, valid auth.json POSTed) using the existing grok-device-oauth-modal.test.tsx jsdom harness. Extract parseGrokCliPasteToken() into its own src/lib/oauth/utils/grokCliAuthJson.ts module so it is directly unit-testable and to keep OAuthModal.tsx's frozen file-size gate from growing (bump 1080->1100, justified in file-size-baseline.json, mirroring the existing extraction precedent on this file). Also fixes two pre-existing "JWT Token" label assertions that this PR's own tab rename ("Import auth.json") had left stale. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
fb6ea295bf |
feat(compression): add Responses tool-output engine (#8010)
* Add Responses tool-output compression engine * fix: enable Codex Responses stacked steps * fix(compression): share Codex tokenizer and rebase UI * fix(compression): sync MCP engine selection * fix(compression): i18n parity for codex-responses mode + rebaseline The codex-responses compression engine already imports the shared countTextTokens/resolveTokenizerEncoding from tiktokenCounter.ts (no duplicate encoder) and CompressionSettingsTab.tsx already threads the new mode through the existing useTranslations()/labelKey pattern - both pre-existing on this branch tip after rebasing onto release/v3.8.49. What was missing after the rebase: the new compressionModeCodexResponses / compressionModeCodexResponsesDesc keys existed only in en.json. Filled en-fallback into all 42 locales via scripts/i18n/fill-missing-from-en.mjs and added real pt-BR/vi translations. Also rebaselined the three files whose own growth (new codex-responses mode wiring) crossed the frozen file-size caps: open-sse/mcp-server/schemas/tools.ts, open-sse/services/ compression/strategySelector.ts, and src/lib/db/compression.ts. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
f879a394f4 |
feat(routing): add prompt-cache affinity (#8008)
* Add prompt cache locality routing * fix: preserve weighted cache-affinity routing * feat(routing): add cache-optimized combos * fix(routing): preserve normal ordering on cache misses * fix(routing): bind cache affinity to concrete accounts * feat(routing): add prompt-cache affinity + align combo-auto-config test with new defaults Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: JxnLexn <JxnLexn@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |