mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
beb6ec857ba434edda63f258afa1bf750b024d20
2608 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
beb6ec857b |
feat(dashboard): agentic conversation tracking — v4, decoupled + storage-architecture concern resolved (#10263)
* feat(responses): virtualize previous_response_id continuation regardless of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (#10121). * feat(dashboard): agentic conversation tracking with live transcript view Every agentic chat request now gets a conversation id (X-ConversationId response header). OmniRoute detects when a follow-up request continues the same conversation via fingerprint + bounded prefix-hash matching, with a strict-growth invariant to prevent false merges between independent single-shot requests that happen to share identical opening content. Continuation detection excludes the system message from the identity anchor, since real coding-agent CLIs commonly regenerate it every request with live context (timestamp, cwd, git status) — without this, that volatility alone broke every continuation check against real traffic. - `/dashboard/logs`: new toggleable Conversation column. - `/dashboard/logs/timeline`: requests sharing a conversation id share a timeline lane, connected by an arrow, with a configurable lane-reuse window. - Request detail panel: new Full Conversation transcript above the raw SSE event stream — Markdown rendering, per-turn timestamps, turn-relative view, click-any-turn navigation, live auto-refresh building the transcript in real time from the in-flight SSE chunk buffer while a request is still streaming, auto-scroll-to-bottom as the live turn grows. - New `/dashboard/conversations` page listing conversations with 2+ turns, no-forking model (an edited/duplicated mid-history turn mints its own independent conversation instead of merging), pagination, duplicate- anchor fix. - Configurable auto-refresh intervals on both the timeline and conversations list pages. - Responses API tool-call gap fix: turnsFromOpenAiMessages only handled role-based Chat Completions messages, so bare {type:"function_call"} / {type:"function_call_output"} / {type:"reasoning"} items (real Responses API traffic) silently vanished from the Conversation Context panel. - truncateForLog now counts input[] (Responses API), not just messages[] (Chat Completions), so a truncated /v1/responses request still shows a placeholder instead of nothing. - RequestTimeline.tsx now reads the same debugEnabled/emailsVisible settings RequestLoggerV2.tsx already used, instead of hardcoding both false — the timeline view never showed SSE/stream-chunk events or respected email-masking, regardless of the actual setting. Migrations 147/148 (agentic_conversations, conversation_turn_nodes) — 135 and 136 are now taken upstream; 143-145 are documented KNOWN_GAPS, so this uses the next free slot past upstream's current highest. Test plan: - npm run typecheck:core — clean - npm run lint — clean - node --import tsx/esm scripts/check/check-migration-numbering.mjs — OK, 0 collisions - 109 unit tests across the conversation-tracking, migration-renumber, and dashboard-wiring surface — 0 failures * refactor(dashboard): reuse call-log artifacts for conversation transcript content conversation_turn_nodes no longer stores turn text/tool-call content (text_preview/block_kind/tool_name) -- it's identity-only now (id/parent/ content_hash), matching agentic_conversations' existing lightweight-index shape. Every node's originating request is already fully captured by the call-log pipeline artifact its last_correlation_id points at, so the /dashboard/conversations tree view resolves each node's actual display content on demand from there (open-sse/services/conversationTurnContent.ts), re-running the same extractCanonicalTurns/hashTurnContent the write path used and matching by content_hash, instead of duplicating conversation content into a second store under a separate retention/gating policy. This also drops the old 8000-char text_preview truncation entirely -- resolved content is always full and untruncated. The frontend contract is unchanged (tree API still returns {textPreview, blockKind, toolName} per node), so the dashboard UI itself (page.tsx, RequestLoggerDetail/RequestTimeline, sidebar, i18n) needed no changes. Renumbered the cherry-picked 147/148 migrations to 153/154 -- 147 now collides with 147_api_keys_model_access_mode.sql, which landed on release/v3.8.50 after this work was originally built. Also includes a standalone, unrelated fix carried along from this rebase: close isProviderModelHidden's missing function-body brace in modelSelectModalHelpers.ts (separately landed as #10206). Stacked on feat/responses-previous-response-id-virtualization (#3), which is itself stacked on feat/openai-responses-store-toggle (#10121). * fix(dashboard): resync conversation list on open so the live-text poll starts immediately openConversation() seeded activeConversation (and therefore activeCallLogId, which gates the live-partial-text poll effect) from whatever row snapshot the list's own fixed-interval poll last produced. A conversation opened right after a reply started streaming -- after that tick, before the next -- had activeCallLogId still null, so the live-text poll never started; only a subsequent background list-poll resync (already existed) picked it up, which is why closing and reopening the same conversation "just worked". loadConversations() is now a shared callback so openConversation can force one immediately on open instead of waiting on pollSeconds. Live-verified against omniroute-dev: opening a conversation mid-stream now shows live reasoning on the first open. * style: prettier formatting for conversationTurnContent.test.ts * fix(db): close migration numbering gap left by decoupling from #3/#10262 153/154 (originally 154/155) were chosen back when this branch stacked on top of the previous_response_id migration (153_call_logs_response_id.sql). Decoupling removed that migration from this branch's history, leaving an unused 153 slot that check-migration-numbering.test.ts correctly flags as a gap. * refactor(dashboard): split RequestTimeline/RequestLoggerDetail under the 1000-line file-size cap Both files exceeded check-file-size's new-file cap after this PR's own additions (RequestTimeline 1048, RequestLoggerDetail 1163). Extracted pure non-component logic (types, constants, allocateLanes and its helpers) out of RequestTimeline.tsx into RequestTimeline.utils.ts, and the two self-contained presentational sub-components (PayloadSection, ConversationContextSection + its private helper) out of RequestLoggerDetail.tsx into RequestLoggerDetail.sections.tsx. No behavior change; existing external imports (default exports, allocateLanes, TimelineLog, CONVERSATION_LANE_REUSE_STORAGE_KEY) still resolve from the original file paths. * fix(db): renumber agentic-conversation migrations to clear 153 collision + sync migration-count docs The refresh-merge of release/v3.8.50 exposed that the feature's three migrations collided at slot 153 with the base's radar_local_model_state (153) and its own call_logs_response_id. Migration runner enforces unique numeric prefixes -> every DB init threw, red-ing Vitest, all Unit shards and the DB-backed quality gates. Renumber the feature's pair to 155_agentic_conversations / 156_conversation_turn_nodes and move call_logs_response_id to 154 (keeps 153_radar base-owned, preserves agentic-before-turn_nodes ordering). Update SQL headers and the 154/156 references in feature code + tests. Migration count is now 151 (was 148 stale in README/AGENTS/llm.txt) — sync the doc counts to clear the docs-accuracy gate. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(ui): drop unused CONVERSATION_LANE_REUSE_STORAGE_KEY re-export from RequestTimeline Knip 6.32 (baseline 415) flags the public re-export of CONVERSATION_LANE_REUSE_STORAGE_KEY from RequestTimeline.tsx as dead: no external consumer imports it through that re-export (it is imported and used directly from RequestTimeline.utils.ts inside the component). Removed the unused re-export; the internal import stays. DEAD_TOTAL 416 -> 415, back to the frozen baseline. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(agentic-conversations): guard resolveConversationId, drop dead whole-chain export - Wrap resolveConversationId() in try/catch in chat.ts, matching the defensive pattern used by every other best-effort side call nearby, so a DB hiccup in conversation tracking can't turn a working chat request into a hard failure. - Remove getConversationTurnTree: knip's project scope excludes tests/**, so an export used only by tests can never register as used there. Swap its 8 test call sites to the paginated getConversationTurnPage (already the dashboard's canonical query) with a generous limit, collapsing to one query path instead of keeping a second whole-chain export alive solely for test convenience. - Regenerate i18n llm.txt mirrors from root (pre-existing drift on this branch, unrelated to the above, caught by the docs-sync pre-commit gate). Addresses PR review feedback. * fix(i18n): close requestLogger conversation-column gap, fix domain-modules count drift - fr.json, vi.json were missing requestLogger.columns.conversation (added in the conversation-tracking feature), failing i18n-vi-completeness.test.ts. - docs/i18n/*/llm.txt mirrors still said 117 domain-specific files after an earlier rebase fixed the migration count but missed this companion number, failing check-docs-sync.mjs across all 42 locales. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(docs): restore PROXY_LOG_INCLUDE_IPS env/doc entries (env-doc-sync red) .env.example and docs/reference/ENVIRONMENT.md were both missing the PROXY_LOG_INCLUDE_IPS entry that src/lib/proxyLogger.ts already reads (confirmed present at this branch's merge-base too, so this predates the conversation-tracking work and is unrelated to it) -- the entry was added on release/v3.8.50 after this branch's last sync and this branch never picked it up. That gap red-lines tests/unit/check-env-doc-sync.test.ts and tests/unit/issue-7793-env-doc-sync-repro.test.ts (Unit Tests fast-path 2/4 in CI). Restore both entries verbatim from the current release/v3.8.50 tip -- no feature-code change. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: hartmark <hartmark@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
d93b24e761 |
feat(api): add provider quota telemetry, adaptive routing, and status inventory (#10148)
* feat(api): add provider quota telemetry, adaptive routing, and status inventory Adds a read-only OmniRoute status/inventory surface plus supporting resilience and usage-tracking infrastructure: - src/lib/quota/providerQuotaTelemetry.ts, providerCapabilities.ts: provider quota state and capability signals, sourced from configured metadata rather than invented values; unknown stays unknown. - src/lib/resilience/adaptiveCircuit.ts, failureClassification.ts: circuit state with lazy recovery and explicit failure classification. - src/lib/usage/usageLedger.ts, budgetGuard.ts, modelPricingRegistry.ts: internal usage tracking and budget allow/warn/deny decisions, kept separate from upstream-reported quota (never conflated). - src/lib/routing/adaptiveRouting.ts: excludes exhausted-quota and open-circuit candidates from routing, penalizes approaching-limit. - src/lib/omnirouteStatus.ts + src/app/api/omniroute/status, route/preview: read-only status endpoint; never issues a live upstream model request (asserted via liveRequestExecuted: false). - src/lib/db/quotaPools.ts: adds ensurePool() for idempotent pool management by automation/CLI callers, following the existing group-demo default-group convention. - scripts/omniroute-verify.mjs (+ omniroute:verify script): local verification against the running gateway. 9 new unit tests, all passing. typecheck:core clean relative to base (release/v3.8.50) -- the 2 pre-existing gateways.ts errors are tracked separately in #9985 and untouched by this change. * test(cli): align cli-machine-token assertions with HMAC-SHA256 64-char format The quota-telemetry feature hardens cliToken to HMAC-SHA256(machineId, SALT) (64-char hex, pristine machine id). Update the regression test to the new format and mirror the production derivation in the different-machine-id check. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: desamours-hub <desamours-hub@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
6615a5445b |
feat: combo-lane awareness + activation UX + MCP visibility (Wave 2 of #9654) (#10039)
* feat(admission): per-target lane-aware probes for combo/fusion fan-out (#9654 Wave 2)
Combo and fusion fan out N targets without ever consulting the adaptive-admission
layer: the parent request holds one lease, but each fan-out target is dispatched
unconditionally. With virtual lanes enabled (OMNIROUTE_CHAT_VIRTUAL_LANES=1), a
connection whose lane queue is full now SKIPS additional fan-out targets instead
of piling more queued work onto an already-congested session.
Adds PerTargetAdmissionHook (admission/types.ts) + createPerTargetAdmissionHook
factory (chatAdmission.ts): strictly non-blocking (maxWaitMs 0 - skip, never
queue), a no-op when virtual lanes are off, keyed to the parent tenantKey, and
release-on-admit so the probe is a capacity gate, not a hold.
Threaded through every parallel fan-out path:
- priority/weighted executeTarget + round-robin skip chains (combo.ts)
- fusion panel before fan-out (fusion.ts), judge fallback prefers survivors
- chaos parallel panel (autoCombo/chaosEngine.ts)
- tryFusionDispatch / tryRuntimeUnitDispatch / buildBaseOptions (dispatchPrelude.ts)
- chat.ts primary + safety-net redirect call sites
Snapshot exposes virtualLanes so the no-op gate is cheap and honest.
Tests: tests/unit/combo-lane-awareness-9654.test.ts (10 tests) - factory
semantics, priority/RR skip, fusion panel drop + all-skipped 503, no-hook
backward-compat baseline.
* feat(flags): activation UX - env-wins adaptive virtual-lanes flag + env docs (#9654 Wave 2)
U7: make adaptive virtual admission lanes discoverable + activatable.
- New OMNIROUTE_CHAT_VIRTUAL_LANES feature flag (boolean/runtime/requiresRestart) in featureFlagDefinitions + en.json i18n key.
- lib/admissionVirtualLanes.ts: env-wins resolver (env > DB > default) + boot warm folding a DB-sourced override into the process-global runtime env via reloadAdaptiveAdmissionRuntime(options.env) - no process.env mutation, no open-sse changes. Env still wins; DB toggle gates at next boot.
- GET /api/settings/feature-flags special-cases the flag to report the gate true source (ccDiscoveryAliases precedent); flagPayload helper dedupes the payload shape.
- Wire the warm into instrumentation-node registerNodejs (non-fatal, DB-ready).
- Document the master switch in .env.example + ENVIRONMENT.md with the system-1/system-2 distinction; zero new env-doc-sync drift.
- 11 new tests (resolver precedence + warm); 60/60 across feature-flag suites; typecheck core clean; ESLint + doc gates green.
* feat(mcp): surface adaptive admission lane data in omniroute_get_health (#9654 Wave 2)
U8: make adaptive virtual-lane admission visible to agents via the MCP health tool. handleGetHealth now surfaces a curated adaptiveAdmission block from the health payload (which already carried the runtime snapshot but was dropping it): virtualLanes/pressure/utilization/laneCount/laneQueuedCount/laneQueuedCost, laneTenants capped at top-10 by queued cost, admitted/rejected/wouldReject counts, shutdown. Block omitted entirely when the health endpoint reports none.
isLaneFlagOn mirrors the runtime 1|true convention so a string serialization can never invert a boolean lane report. getHealthOutput schema extended with the matching optional shape; tool description updated.
4 new dispatch tests (full block, top-10 cap/order, omission, defensive coercion of string flags + malformed lane entries) - 22/22 in essentialTools.test.ts. README: Adaptive Admission Lane Data table + Skills & Tool Navigability audit (29/43 schema entries covered, 14 undocumented, tool_search keyword runtime discovery, full catalog in docs/frameworks/MCP-SERVER.md).
No new lint errors (4 pre-existing in server.ts), typecheck core clean, doc counts + fabricated-docs gates green.
* docs: add changelog entry for #9654 Wave 2 (#10039)
* fix(codeql): suppress js/insufficient-password-hash false positive in lane-key fingerprinting (#10039)
resolveSessionId sha256-hashes bearer/x-api-key/x-goog-api-key to derive a deterministic, non-reversible per-key lane-bucket ID for virtual admission lanes (#9654). This is not password storage or verification, so the rule is a false positive; suppress it inline (same house style as src/lib/sync/tokens.ts) to clear the codeqlAlerts ratchet (2 > baseline 1) that blocks #10039 and every PR against release/v3.8.50.
* docs(mcp): complete MCP server README tool reference (#10039)
The MCP server README covered only 29 of the 43 schema entries, listing the
remaining tools solely as a gap note with omniroute_tool_search as the runtime
fallback. Add tool-reference tables for the agent-skills trio, oneproxy trio,
web_fetch/web_search, tool_search, create_combo, set_routing_strategy,
pick_fastest_model, sync_pricing, and db_health_check so the README covers the
full schemas catalog, and fold the coverage note into the tool_search discovery
paragraph.
* fix(chat): drop unused correlationId from safety-net combo redirect (#10039)
handleComboChat's HandleComboChatOptions has no correlationId member and
the combo pipeline never consumes it; the property was copied from the
handleSingleModelChat options shape by accident and introduced a new
TS2353 under the open-sse workspace typecheck gate.
* fix(i18n): translate featureFlagChatVirtualLanesEnabledDescription into 42 locales (#10039)
en.json gained the flag description in this PR but the locale catalogs
were never mirrored, failing the pt-BR key-parity (#6695) and vi
completeness gates. Adds a real translation to every locale, keeping the
zh-CN/zh-TW glossary canonical terms (提供者/儀表板) and no ICU drift.
* chore(quality): ratchet open-sse-typecheck baseline down (#10039)
The Wave 2 admission refactor removed 66 baselined open-sse type errors;
re-freeze the baseline so the gate pins the new, tighter state.
* docs: resync provider reference to 341 and CLI tools to 34
The release branch gained an 11th no-auth provider (freeaiapikey registry
resync, #10233) and a 26th CLI Code tool without regenerating the
auto-generated docs, leaving every PR against release/v3.8.50 failing the
Docs Gates strict validator (code 341 vs doc 340, CLI 34 vs "33 tools").
Regenerate docs/reference/PROVIDER_REFERENCE.md and sync the provider/tool
counts across README.md, AGENTS.md, llm.txt plus 42 i18n mirrors,
package.json description, and the four diagram SVGs.
* fix(tests): align count expectations with live catalogs (pre-existing release drift)
Release/v3.8.50 currently fails five gates on its own tree; this PR inherits
them. Fix the stale expectations to match live code:
- feature-flags-settings: 48 -> 49 flags (Wave 2 adds OMNIROUTE_CHAT_VIRTUAL_LANES)
- cli-tools-schema / cli-catalog-counts: 33 -> 34 tools (zcode added; 26 code = 21 visible + 5 none)
- optional-transformers-dependency: onnxruntime-node ~1.24.3 -> ~1.27.0 (bump #10382)
- stryker.conf.json: register chatcore-header-drop-warn-dedupe-10315 test
- check-public-creds: freeze zcodeProtocol clientId false positive (client identifier, not a credential)
* fix(tests): follow release's onnxruntime-node revert to ~1.24.3
release/v3.8.50's #10543 pinned onnxruntime-node back to ~1.24.3 after
#10403's ~1.27.0 bump caused npm to nest a second native copy under
@huggingface/transformers and broke the Docker SONAME contract. This
PR's own drift-alignment commit (
|
||
|
|
8acd799af7 |
feat(routing): add exclusive managed session connection leases (#10362)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
fd76271515 |
fix(providers): make upstream model sync opt-in and preserve manual overrides (#10603)
* fix(providers): make upstream model sync opt-in and preserve manual overrides (cherry picked from commit 0a84f5496896a95856e834112b3d813fa1b87d38) * test(providers): cover upstream model sync controls * fix(providers): fix pre-existing tests broken by opt-in model sync + sync i18n keys The upstream model auto-fetch opt-in default flip made 3 pre-existing tests short-circuit before reaching the paths they exercise, because their connection fixtures never set providerSpecificData.autoFetchModels: true: - tests/unit/provider-models-route-lan-guard.test.ts (#6939 SSRF-guard tests) - tests/unit/openrouter-embeddings-catalog-6976.test.ts (live discovery merge/dedup) - tests/unit/provider-models-route.test.ts (Kimi Coding auth-header test — this was mislabeled as base/catalog drift during review, but is the same root cause: without autoFetchModels the mocked fetch is never reached and the route falls back to local catalog data instead) Also syncs the 11 new providers.autoFetchModels*/overridesUpstreamModel*/ resetToUpstreamDefaults* i18n keys from en.json/zh-CN.json to the remaining 40 locale files via a narrowly-scoped ad-hoc translation script (only these 11 keys — leaves each locale's pre-existing, unrelated missing-key backlog untouched). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): fix remaining pre-existing tests broken by opt-in model sync Rebase surfaced that the 'Kimi Coding' CI failure flagged as possible base drift during review was actually the same root cause as the lan-guard and openrouter-embeddings fixes: 29 pre-existing tests in tests/unit/provider-models-route.test.ts (of 59 total) short-circuit under the new autoFetchModels opt-in default because their connection fixtures never set providerSpecificData.autoFetchModels: true, so they never reach the live-fetch/validation paths they were written to exercise (fetch mocks never called, base-URL validation never reached, live models never merged). Adds providerSpecificData.autoFetchModels: true to each affected fixture. No production code or test assertions changed — same TEST-fixture-only pattern as the lan-guard and openrouter-embeddings fixes. All 59 tests in the file now pass (was 30/59). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
30265ef7f8 |
fix(i18n): add missing routing and compression messages (#10546)
* fix(i18n): add missing routing and compression messages * fix(i18n): align zh-TW provider terminology |
||
|
|
6b823aa441 |
fix(logging,sse): redact sensitive log fields and default SSE comments to disabled (#10539)
* fix(logging): redact client IPs and account prefixes by default ProxyEgress and AUTH logs exposed client IPs, egress IPs, and account prefixes at info level — a privacy leak in multi-tenant/shared-log environments. Now redacted by default, only shown when debugMode=true. Fixes #10348 * fix(sse): default SSE comment lines to disabled Strict SSE clients (WorkBuddy, etc.) JSON.parse every SSE line and crash on comment lines. Changed OMNIROUTE_SSE_COMMENTS default from enabled to disabled. Operators can opt in with OMNIROUTE_SSE_COMMENTS=on. Fixes #10524 * fix(logging): gate AUTH account-prefix redaction on a narrow flag, not debugMode The proxy-log redaction half of #10348 is superseded by an already-merged fix (PROXY_LOG_INCLUDE_IPS, decoupled from debugMode). The remaining gap was the chat.ts AUTH log line ("Using <provider> account: <prefix>..."), which this PR gated on the broad `debugMode` setting. `debugMode` is a general dashboard-visibility toggle unrelated to log privacy — coupling redaction to it means any future, unrelated change to debugMode's default silently changes whether account prefixes leak into logs. Add a dedicated AUTH_LOG_INCLUDE_ACCOUNT_ID feature flag (default off, security category) and gate the AUTH log line on it via isFeatureFlagEnabled(), which reads the DB override synchronously on every call (no stale in-memory cache to invalidate) and fails safe to redacted on any lookup error. Also update the SSE-comments tests/docs that still asserted the old enabled-by-default behavior (tests/unit/sseHeartbeat.test.ts, tests/unit/sse-comments-optout-9305.test.ts, docs/reference/ENVIRONMENT.md) to match the new default-off behavior from this PR's earlier commit. Refs #10348, #10524 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
6d99a46d4b |
fix(cli): guarantee non-empty [STARTUP] Fatal log on instrumentation-hook boot throw (#10447)
* fix(cli): guarantee non-empty [STARTUP] Fatal log on instrumentation-hook boot throw Refs #10171: on native Windows / WSL2 boots, an instrumentation-hook throw during module-load or registerNodejs() leaves the HTTP listener up while every DB-touching route 500s, with app.log staying completely empty. The #7773/#7828 guard in ensureDbReadyForBoot only logs one specific failure class (DB driver init). register() in src/instrumentation.ts now wraps the boot call in a try/catch at the outermost boundary and unconditionally logs a "[STARTUP] Fatal: instrumentation hook failed during boot:" line before rethrowing, so app.log/stdout is never silently empty on a failed boot regardless of platform or which step threw. This is a partial diagnostic hardening, not the full fix for #10171 — the platform-specific root cause on native Windows/WSL2 still needs the reporter's raw child stderr from a real host (tracked separately, see _tasks/pipeline/bugs/2-implementing/10171-instrumentation-hook-500-on-windows-wsl.plan.md). * fix(cli): normalize instrumentation boot errors Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(cli): reuse shared normalizeBootError helper in instrumentation.ts The outermost instrumentation-hook boot boundary (#10171) was inlining its own err-instanceof-Error normalization instead of reusing the existing normalizeBootError() helper already defined in instrumentation-node.ts for the same purpose (#6560/#7773). Extract it into a dependency-free src/lib/instrumentationBootError.ts so both instrumentation.ts (which also loads under the Edge runtime) and instrumentation-node.ts can import it statically without risking a second failing dynamic import of instrumentation-node.ts from within the catch block. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
c545855b26 |
fix(logging): capture early-keepalive bytes in the call-log artifact (#10331)
Diagnosed while chasing the reused-output-index incident (see 705ac7335 / OpenClaw issue #123342): every call-log artifact showed a wire-clean response, even for requests that actually failed, because withEarlyStreamKeepalive injects its startup/keepalive/error frames directly into the outer response stream, entirely outside the request handler's own reqLogger. reqLogger.appendConvertedChunk (which populates pipeline.streamChunks.client) never sees those bytes — only what chatCore.ts's own SSE writer produced. The persisted artifact was answering "what did the handler generate," not "what did the client actually receive," which is the wrong question when diagnosing a client-visible stream defect. withEarlyStreamKeepalive wraps the handler's Promise from OUTSIDE its call tree; the reqLogger it needs to feed is created deep inside chatCore.ts, after routing/model/provider resolution, and doesn't exist yet when the keepalive frames are written. The two sides share no reference — only an identifier, if one is deliberately threaded through both. Fix: responses/route.ts now generates a correlationId before calling handleChat, passes it as handleChat's existing (already-supported, previously-unused-here) 4th positional arg — which chatCore.ts already threads into trackPendingRequest's metadata as entry.correlationId, zero changes needed there — and also into withEarlyStreamKeepalive's options. The wrapper buffers every direct- to-client write (startup frame, periodic ticks, in-band error frames) via the new earlyKeepaliveByteBuffer module, keyed by that same id. chatCore/attemptLogging.ts, which already has correlationId in scope right where it assembles the final pipeline payload before saveCallLog, takes the buffered bytes and prepends them into streamChunks.client in send order. The verbatim-forwarded real response body is deliberately NOT re-recorded here — the handler's own reqLogger already captures that; recording it twice would duplicate it in the artifact. The buffer is consumed exactly once per correlationId and swept on a 10-minute TTL so a request that never reaches the persist call (aborted, detailed logging disabled, a route that doesn't opt in) cannot leak entries forever. Scoped to /v1/responses only, where the incident actually happened. /v1/chat/completions and /v1/messages call withEarlyStreamKeepalive the same way and would need the identical two-line route change to opt in; left as a follow-up rather than bundled in sight-unseen. Test plan: - tests/unit/early-keepalive-byte-buffer.test.ts (new): record/take ordering, single-consumption, per-id isolation, empty-input no-ops, unbounded-growth cap - tests/unit/early-stream-keepalive.test.ts: two new tests — a correlationId records the startup frame and keepalive ticks but NOT the forwarded body; omitting correlationId is a true no-op - tests/unit/attempt-logging-early-keepalive-merge.test.ts (new): real temp-DB end-to-end proof against the actual persisted call-log row — early bytes prepended in send order, consumed exactly once, no-op without a correlationId, gated by detailedLoggingEnabled matching the existing streamChunks capture gate - tests/unit/chatcore-attempt-logging.test.ts (existing): unchanged, still passing — confirms the merge addition doesn't disturb existing persistence behavior - 44 passed total across the above plus earlyStreamKeepalive.test.ts, 2 pre-existing skips unrelated to this change - tsgo --noEmit: clean on all touched files |
||
|
|
671aa3d80d |
fix(oauth): treat Kiro social poll status as alias of error for pending states (#10620)
Kiro's device poll endpoint reports progress in a `status` field (e.g. `authorization_pending`), but `classifyKiroSocialPoll()` only inspected `data.error`. This caused every pre-authorization poll to fall through to the terminal `invalid_token_response` error, making social login impossible for Kiro AI and Amazon Q via Google/GitHub. Changes: - Add `status` field to `KiroSocialPollData` type - Update `classifyKiroSocialPoll()` to check `data.status` as fallback for `data.error` when detecting pending states - Add tests covering `status`-based pending detection and precedence Closes #10618 |
||
|
|
5a44c46b1d |
feat(resilience): scope auto-disable banned accounts to subscriptions (#10617)
* feat(resilience): scope auto-disable banned accounts to subscriptions Prepaid API keys should stay in the routing pool after a permanent-ban signal; subscription/OAuth accounts can still be deactivated. Default scope remains all so existing installs do not change. * docs(security): document auto-disable scope and log skipped prepaid keys Keep the operator ban-detection page aligned with the new setting and reuse the shared scope enum in the settings schema and dashboard radios. * chore(changelog): name the auto-disable scope fragment for #10617 * docs(settings): treat free login seats as auto-disable targets The first-cut scope is still all vs login-style auth. Copy now states that paid subscriptions and free accounts both disable, while prepaid API keys stay in the pool until per-account overrides exist. * i18n: backfill autoDisableBannedScope keys across all locales npm run i18n:sync-ui — the 6 new autoDisableBannedScope* keys landed in en.json and vi.json but not the other 40 locales (including pt-BR), tripping the pt-BR no-drift regression test (#6695). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
d6242b7267 |
fix(auth): add missing state parameter to OIDC authorization URL (#10614)
* fix(auth): add missing state parameter to OIDC authorization URL
The OIDC login route generates a state UUID and stores it in the
oidc_state cookie, but never includes it in the authorization URL.
This causes the OIDC callback to receive state=null, failing with
'oidc_error=missing_code' because the provider has no state to echo.
Add url.searchParams.set('state', state) after setting scope, so the
state parameter is sent to the OIDC provider and returned in the
callback for proper CSRF protection.
* test(auth): add regression coverage for OIDC login state parameter
Adds a TDD regression test proving the fix in this PR: the OIDC login
route now includes the state query parameter in the authorization
redirect URL, and it matches the oidc_state cookie value set on the
same response. Modeled on tests/unit/oidc-callback.test.ts.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(auth): use originEarly for OIDC err redirects (#10224)
* test(auth): verify OIDC err redirects use proxy origin (#10224)
---------
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
6003612000 |
fix(audio): fall back nested STT models when the prefix provider has no credentials (#10584)
* fix(audio): fall back nested STT models when the prefix provider has no credentials Bare ids such as deepgram/nova-3 prefix-match the native provider and 400 when that key is missing, even if OpenRouter lists the same model. Retry the gateway and mention qualified catalog ids in the error. Closes #10583 * test(audio): scope whisper-1 fallback test to a 2-provider registry nanogpt was added to AUDIO_TRANSCRIPTION_PROVIDERS (already merged, unrelated to this fix) with a bare "whisper-1" model id, which now intercepts findAlternateAudioProvider's first candidate before the qualified-alias branch this test exists to cover. Scope the test to a local {openai, openrouter} registry subset so it deterministically exercises the qualified `${provider}/${model}` fallback regardless of future providers that also list a bare "whisper-1" id. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
3d0ffb49a4 |
feat(providers): complete Jina + Gemini Embedding 2 multimodal via OmniRoute (#10581)
* feat(providers): complete Jina AI via OmniRoute including Omni multimodal
Dashboard and env keys share one Jina credential pool, native v5 Omni
{text}/{image}/{content} docs pass through /v1/embeddings intact, and
classify/segment/search are proxied without a third unused Jina card.
* chore(changelog): name Jina complete-provider fragment for #10581
* feat(providers): make Gemini Embedding 2 multimodal work via OmniRoute
Route gemini-embedding-2 through embedContent/batchEmbedContents so N
OpenAI input items become N vectors, pass through native multimodal
parts, and use dashboard Gemini keys (GEMINI_API_KEY only as fallback).
* fix(providers): resolve rebase fallout for Jina/Gemini embeddings
- narrow the two new no-explicit-any violations introduced by this PR
(validateJinaFoundationProvider's params + catch, search.ts's
normalizeJinaSearchResponse data param)
- cast credentials to Record<string, unknown> at the two quota-preflight
call sites in src/sse/services/auth.ts so the new JinaEnvCredentials /
GeminiEnvCredentials union members type-check without loosening the
allRateLimited narrowing used elsewhere in the same function
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
c767494ae4 |
feat(api): alias /v1/multimodal-embeddings to /v1/embeddings (#10568)
Jina-compatible clients POST /v1/multimodal-embeddings and currently get HTTP 404 unknown_route from the catch-all. Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
134ab8cabb |
fix(api): name working OpenRouter ids when Gemini embed creds are missing (#10565)
Native gemini-embedding-2 400s with a dead-end credentials error even though openrouter/google/gemini-embedding-2 already serves 3072-d vectors. Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
458ab1aac0 |
fix(vision): preserve high detail for inline images (#10554)
* fix(vision): preserve high detail for inline images * fix(vision): scope high-detail image default to OpenCode clients defaultImageDetail() was applied at prepareUpstreamBody, the shared upstream-body prep path for every provider and format, not just the OpenCode path the fix targets. Gate it on isOpencodeClient (the existing User-Agent/x-opencode-* header signal already used for bypassDefaultToolLimit at this call site) so non-OpenCode callers keep the provider's own image detail default. Adds a regression test covering a non-OpenCode caller against the same opencode-zen provider. * fix(vision): document and test the global vs OpenCode-only detail scope The OpenCode-only high-detail default in chatCore/upstreamBody.ts (defaultImageDetail, gated on isOpencodeClient) forwards the caller's own image_url.detail and was already correctly scoped in a prior commit on this branch. The internal vision-bridge describe self-loop (visionBridgeHelpers.ts) is architecturally global: VisionBridgeGuardrail runs for every caller/provider whenever the target model lacks vision support, and there is no client-identity signal at that layer to gate on. Its describe prompt explicitly asks the vision model to transcribe visible text, so requesting "high" detail unconditionally is justified on its own merits (OCR accuracy), independent of the OpenCode motivation. Adds a compatibility assertion proving the Anthropic wire-format branch of the same describe self-loop carries no `detail` field (it has no such concept) and is therefore unaffected by this default, and documents the split (OpenCode-only forwarding vs. global describe default) in docs/security/GUARDRAILS.md. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: rinseaid <rinseaid@rinseaid.net> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
0431dd84e7 | fix(db): preserve native runtime drivers in standalone bundles (#10552) | ||
|
|
20af3988cf |
fix(a2a): use a constant-time bearer compare in /api/a2a/tasks (#10544)
* fix(a2a): use a constant-time bearer compare in /api/a2a/tasks * fix(a2a): drop new Function from tasks-auth test in favor of dynamic import The regression test for the constant-time bearer compare loaded tokensMatch and authenticateA2A by regex-extracting their source and eval'ing it via new Function, which trips the repo's no-new-func/no-implied-eval ESLint rules (error-level everywhere, including tests). Export both helpers as a test seam from the route module (mirrors the existing bridgeSecretMatches/authRouteInternals pattern) and import them directly in the test instead. Also drops the now-unused eslint-disable directives. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
b9cd5ed138 |
feat(providers): optional AI Horde API key and live image catalog (#10542)
* feat(providers): optional AI Horde API key and live image catalog Allow a registered Horde key on the no-auth connection and send it for chat and image jobs. List only image models that currently have workers, and generate through Horde's native async API. # Conflicts: # open-sse/config/imageRegistry.ts # src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx # src/shared/constants/providers.ts # src/sse/services/auth.ts * fix(providers): validate AI Horde keys against find_user The OpenAI-compatible /v1/models probe returns 200 for any Bearer token on oai.aihorde.net, so Check always succeeded. Use Horde's /v2/find_user lookup instead; an empty key still counts as the optional anonymous path. * chore(changelog): name the AI Horde fragment for #10542 * fix(images): harden AI Horde optional-key selection and outbound fetches - Optional-key selection now honors connection health (rate-limit cooldown and terminal/unavailable test status) before handing a stored key back, rotating to the next healthy key or falling back to the anonymous no-auth path instead of using an unhealthy stored key. - Route the Horde submit/check/status/cancel and catalog calls through the repository's bounded outbound-fetch helper (timeout, no more bare fetch()) and route R2 image downloads through the established bounded remote-image fetch (SSRF host guard, DNS-rebinding pin, streaming byte cap, redirect limit) instead of an unbounded fetch(). - Extend the generation deadline to cover the full request lifecycle (catalog freshness check, submit, polling, and image download), and add a regression test proving that exceeding the deadline issues a DELETE cancel to Horde's API rather than only timing out locally. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: pqr <pqr@soraka.ititti.es> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
276b3dffa3 |
fix(sse): clear quota_exhausted cooldown when real window recovers (#10534)
* fix(sse): clear quota_exhausted cooldown when real window recovers The claude-token-fallback combo was not auto-returning to Sonnet/Opus after a subscription 429 recovered. maybeClearRecoveredQuotaState() was honoring the synthetic 1h cooldown (SUBSCRIPTION_QUOTA_COOLDOWN_MS, persisted when no upstream reset was parseable) instead of the REAL per-window resetAt returned by the scheduled quota poller, so the connection stayed locked long past the actual quota reset. Add windowStillExhaustedAfterRealReset() and use it to decide recovery per-quota-window: a quota_exhausted connection now clears as soon as no governing window is still exhausted with a future-or-unknown real reset, instead of waiting out the synthetic cooldown. Falls back to the previous synthetic-cooldown guard when the fetch has no quota object at all (degraded/failed shape) so existing behavior is unchanged there. Preserves the existing kimi-coding partial-refresh semantics: an exhausted window with no parseable resetAt still blocks recovery. * fix(sse): preserve Claude extra-usage block from general quota recovery maybeClearRecoveredQuotaState()'s new per-window recovery check (added in this branch) only inspected usage.quotas, so a Claude connection blocked by the extra-usage guard (lastErrorSource: "extra_usage") could be released just because the session/weekly quota windows looked recovered, even while extraUsage.queued was still true. Extra-usage blocking is orthogonal to quota-window exhaustion and must only be released by syncClaudeExtraUsageStateIfNeeded (buildClaudeExtraUsageConnectionUpdate). Add a guard that keeps the connection locked when lastErrorSource is "extra_usage", the blockExtraUsage policy is still enabled, and the fresh usage snapshot still reports extraUsage.queued === true. Add an integration test walking the real fetchLiveProviderLimitsWithOptions -> syncClaudeExtraUsageStateIfNeeded -> maybeClearRecoveredQuotaState call chain with recovered quota windows but extraUsage.queued=true, asserting the connection stays unavailable with lastErrorSource still "extra_usage". Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
9a433775e7 |
fix(models): correct Codex context and combo limit resolution (#10533)
* fix(models): honor Codex combo context overrides * test(codex): align discovery context expectation * test(models): align Codex route limits * test(models): align remaining Codex route limits |
||
|
|
ebf0bf913a |
fix(settings,auth): default debugMode to false and skip account rotation on model-unsupported 400 (#10525)
* fix(settings,auth): default debugMode to false and skip account rotation on model-unsupported 400 * fix(auth): disambiguate model-unsupported from auth-credential 400 The model-unsupported guard used MODEL_ACCESS_DENIED_PATTERNS directly, which also matches auth-credential errors like 'invalid api key for model X'. Add the AUTH_CREDENTIAL_ERROR_PATTERNS exclusion (same as checkFallbackError) and use provider_model_unsupported log reason. Addresses maintainer feedback on PR #10525 Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(auth): narrow model-unsupported guard to avoid misclassifying account-scoped entitlement 400s The #10460 guard reused MODEL_ACCESS_DENIED_PATTERNS directly, which also matches ambiguous "access"/"permission" phrasing (e.g. "does not have permission to access this model") that commonly signals an ACCOUNT-scoped entitlement gap (PRO vs free tier) rather than a genuinely provider-wide unsupported model — a different account of the same provider may still have access, so those must keep rotating normally instead of being short-circuited. Extract isProviderModelUnsupported400() in accountFallback.ts: reuses the same AUTH_CREDENTIAL_ERROR_PATTERNS exclusion checkFallbackError's 400 branch already applies, narrowed to a strict subset of unambiguous "provider does not serve this model at all" phrasings. auth.ts now calls this shared helper instead of testing the broader patterns in isolation, and exposes the sanitized reason ("provider_model_unsupported") on the returned result, not just in the log line. Also fix DATA_DIR test-isolation ordering in account-fallback-service.test.ts: it was assigned after the first dynamic import of accountFallback.ts, which transitively imports src/lib/db/core.ts (DATA_DIR is captured once at module-load time), so the intended isolated test directory was silently never used. Move the assignment before any transitive DB import, and add regression tests for the 3-account rotation contract: exactly one upstream call for an unambiguous provider-wide 400 with the combo advancing to the next target, continued rotation for account-scoped 401/403/429 and for the permission/entitlement 400 case that motivated this narrowing. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
9392b30575 | fix(compliance): redact extra provider API keys (#10521) | ||
|
|
daae6e6fb5 |
fix(providers): test token-backed web sessions (#10519)
* fix(providers): test token-backed web sessions * fix(providers): restrict token-web-session test dispatch to validated providers Narrow shouldUseApiKeyConnectionTest to the token-kind web-session providers that actually have a token-aware connection validator (deepseek-web, kimi-web, tinycms-web, copilot-m365-web, copilot-web, zai-web). WEB_SESSION_CREDENTIAL_REQUIREMENTS marks more providers as kind: "token" than have a matching validator in SPECIALTY_VALIDATORS (hailuo-web, microsoft-designer-web, t3-chat-web, promptql) — those were falling through to the generic cookie-based validateWebCookieProvider probe, which sends the stored credential as a Cookie header and treats most non-401/403 responses as valid, so an invalid token could be reported as a healthy connection. Add regression coverage for hailuo-web and promptql (plus microsoft-designer-web and t3-chat-web) proving they stay off the API-key test path, and for every currently validated token-kind provider proving they still use it. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
83c1d3c659 |
fix(dashboard): count live usage_history rows in Free Tier 'used this month' (#10381) (#10509)
* fix(dashboard): count live usage_history rows in Free Tier 'used this month' (#10381) * fix(dashboard): use an indexable UTC month-range predicate for used-this-month (#10381) sumUsageTokensThisMonth() filtered usage_history with substr(timestamp, 1, 7) = strftime('%Y-%m', 'now') — a substr() expression SQLite cannot use a range index on, and fragile against any timestamp that isn't exactly ISO-shaped. Replace with an indexable inclusive-start/ exclusive-end UTC range: timestamp >= <month start> AND timestamp < <next month start>, matching the ISO 8601 format saveRequestUsage() already writes. Adds a boundary regression test: the first instant of the current month is included, the last instant of the previous month is excluded, and a next-month row is excluded too (covers the upper bound substr() could never express). --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
9500adb013 |
fix(combo): surface context-overflow before compression so oversized requests fail fast with a clear error (#10225) (#10503)
* fix(combo): surface context-overflow before compression so oversized requests fail fast with a clear error (#10225) * fix(combo): make context-overflow deferral target-aware for native Codex passthrough (#10225) The deferral added by the prior commit checked only operator-named compression exclusions when deciding whether at least one target "can compress" — it never accounted for native Codex Responses passthrough targets, which chatCore.ts unconditionally excludes from compression (compressionExcluded = nativeCodexPassthrough || ...). Deferring on such a target's account let an oversized request skip both the combo preflight AND compression, reaching fetch() uncompressed. Thread the same request-shape facts chatCore.ts uses (shouldUseNativeCodexPassthrough: provider/sourceFormat/endpointPath/body/ headers) down into getKnownContextOverflow so the deferral decision can never drift from chatCore's own — a native-codex-passthrough target now never counts as "compressible", so a pool made only of such targets keeps the fast local 400 instead of a wasted round trip. Adds regression coverage: the pure getKnownContextOverflow target-aware check, an end-to-end handleComboChat proof that a native-codex-only pool fails fast with zero dispatches, and two real handleChatCore-path tests proving compression actually reduces the dispatched body when eligible, and that a still-too-large-after-compression request is rejected locally without an upstream call. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
da42ed6d2e |
fix(providers): fall back to public Code Suggestions endpoint on GitLab Duo direct_access 401 (#10365) (#10499)
* fix(providers): fall back to public Code Suggestions endpoint on GitLab Duo direct_access 401 (#10365) * fix(providers): extend GitLab Duo 401 fallback to the connection-test path (#10365) The chat-completion path (open-sse/executors/gitlab.ts) already falls back to the public Code Suggestions completions endpoint when the direct_access exchange is rejected with 401, but testOAuthConnection() / the dashboard Retest button still reported the connection unhealthy on the same 401 — even though a real chat request through that connection would have succeeded via the fallback. Apply the identical fallback contract to the connection-test path (first attempt and the post-refresh retry), sharing the predicate with the executor via shouldFallbackToPublicCodeSuggestions. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
497dd6f357 |
fix(memory): auto-check Qdrant health on mount and stop false-red badge (#10489)
* fix(memory): auto-check Qdrant health on mount and stop false-red badge The Qdrant engine card on /dashboard/memory?tab=engine showed a red "Error" badge after every page refresh even when Qdrant was healthy: the badge derives its state from a health check, but the mount effect only fetched settings + embedding models — health started as null and the render treated `health?.ok` (undefined) as a failure. Clicking "Test connection" (which runs the same server-side /readyz check) immediately turned it green, proving the connection was fine. Two changes: - Auto-run the health check on mount once settings load and Qdrant is enabled, so a refreshed page reflects the real state (verified live: /api/settings/qdrant/health returns ok:true in ~2ms on a healthy compose deployment). - While health has not been checked yet (null), render a neutral gray "Testing..." state instead of red — red is now reserved for an actual failed health check. Regression test added (fails on the old code): with enabled settings and a healthy mock, the card must hit /api/settings/qdrant/health on mount and show statusActive, never statusError. * chore(changelog): fragment for #10489 * Merge branch 'release/v3.8.50' into fix/qdrant-health-badge * test(fix): refresh expired alibaba quota sample validity and onnxruntime pin for v3.8.50 base - alibaba-free-tier-quota-fetcher.test.ts: sample quotaValidityPeriod (2026-08-16 16:00 UTC) is in the past, making every quota entry classify as expired/not_capable; bump to 2028-01-01 UTC so the text/merge classification tests exercise the intended path again. - optional-transformers-dependency.test.ts: onnxruntime-node pin assertion updated from ~1.24.3 to ~1.27.0 to match package.json (bumped by #10403); the regular-not-optional intent is unchanged. * test(fix): align optional-transformers-dependency with onnxruntime ~1.24.3 pin (base #10543) * docs(fix): sync 150-migration count and document PROXY_LOG_INCLUDE_IPS (base drift #10348/#10507) * fix(memory): re-check Qdrant health after saving settings save() optimistically flipped enabled and started the PUT while the mount effect could immediately GET /api/settings/qdrant/health against the OLD persisted settings. If that GET won, it returned not_configured/failed and - because health was non-null - the effect never retried after the PUT succeeded, leaving a healthy Qdrant red until a manual Test connection. Invalidate health (generation counter + setHealth(null)) at save start and after a successful PUT, then explicitly schedule a fresh check: setting health to null alone is not enough, React bails on the no-op when health is already null (the exact GET-wins ordering). Stale responses are dropped via the sequence guard so an in-flight pre-save check can never overwrite the post-save result. Adds a regression test covering enable ordering. Addresses PR #10489 review finding (issuecomment-5312271806). * fix: narrow omniglyph transform result union (merge base |
||
|
|
e667ab12d1 |
fix(usage): wire agentrouter balance quota into dashboard Quota UI (#10078) (#10472)
* fix(usage): wire agentrouter balance quota into dashboard Quota UI (#10078) * fix(usage): render AgentRouter wallet balance as USD in the Quota UI (#10078) The prior fix wired AgentRouter's balance into getUsageForProvider() and USAGE_SUPPORTED_PROVIDERS, but the actual dollar figure never reached the Dashboard Quota UI: quotas.balance.remaining carried a synthetic two-state percent (100/0) instead of the real dollarBalance, and the Provider Limits renderer only formats a row as "$X.XX" when isCredits/currency/creditCount are set, which the generic quota-parsing path never sets. A configured balance rendered as a bare "100% left" percentage, not USD. Shape quotas.balance.remaining as the real USD amount (clamped to 0) and add an agentrouter branch to quotaParsing.ts that builds a credits-style row (same buildCreditsQuota() pattern as DeepSeek/Claude extra-usage), so a configured balance shows a currency-formatted dollar amount and an exhausted balance always renders as exactly $0.00. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
09680013de |
fix(providers): resolve combo names on /v1/audio/speech and /v1/videos/generations (#10471)
* fix(providers): resolve combo names on /v1/audio/speech and /v1/videos/generations `GET /v1/models` advertises combos with `owned_by: combo`, and chat, embeddings, transcriptions (#9134) and images (#8986, #9239) all resolve those names. Speech and video did not: both rejected a combo name at model validation, before any resolution could happen. POST /v1/audio/speech {"model":"my-combo","input":"hi"} -> 400 Invalid speech model: my-combo. Use format: provider/model POST /v1/videos/generations {"model":"my-combo","prompt":"a cube"} -> 400 Invalid video model: my-combo. Use format: provider/model A client picking a model out of /v1/models therefore could not tell which entries the catalogue would actually accept, and callers ended up hardcoding vendor ids for these two routes while using combo names everywhere else. Both routes now mirror the images route: detect a combo name before the provider lookup and divert to a strategy executor. The two new executors follow imageCombo — expand targets with resolveComboTargets(), filter to targets the route can actually serve, walk them in priority order, and return the first success or the last failure, with 400/401/403 treated as terminal. Two details differ from the image strategy: Speech filters at model level rather than provider level. parseSpeechModel() resolves a provider prefix without checking that the model behind it can speak, so `openai/gpt-4o` would otherwise be accepted as a target and fail only once dispatched. The filter now checks the provider's own model list, and keeps targets from dynamic provider nodes that do not enumerate models. Speech also returns the handler's Response untouched instead of building a JSON body, because that route streams audio; only the ADD-only meta headers are attached, exactly as the direct path does. The failure branch is the only place the body is read. successfulMediaGenerationResponse() gains optional `strategy` and `fallbackAttempts` so the video strategy can report them the way imageCombo does, rather than duplicating the cost calculation. Both are omitted on the direct single-model path, where neither is meaningful. Tests mirror tests/unit/combo/image-combo.test.ts for both routes: combo not found, no capable targets, empty combo, and targets present with no provider connection. 16/16 pass across the three combo test files. * fix(providers): preserve local overrides, custom models and per-target prompt rules through video combo dispatch executeVideoCombo() diverged from the direct /v1/videos/generations route in three ways: it dropped the ComfyUI-style local-override credential lookup for authType:"none" targets, its capability filter only matched the built-in video registry (skipping custom OpenAI-compatible provider nodes tagged with the "videos" endpoint), and the route validated the prompt against the unresolved combo name before combo targets were expanded — rejecting prompt-optional I2V targets that never got the chance to opt out. Extracts the shared resolution rules (resolveVideoModelTarget, isVideoPromptOptional, resolveLocalOverrideCredentials) into src/app/api/v1/_shared/videoModelResolution.ts so the direct route and the combo executor apply identical rules, moves the combo-name diversion ahead of the prompt-required check so validation runs against the real resolved target, and adds per-target prompt validation inside the combo loop so a missing prompt only rules out that target instead of the whole combo. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
a55daacc49 |
fix(dashboard): send periodic WS heartbeat pings to stop live-dashboard reconnect churn (#10452)
* fix(dashboard): send periodic WS heartbeat pings to stop live-dashboard reconnect churn
The live-dashboard WS client (src/hooks/useLiveDashboard.ts) only sent a
subscribe frame on open and never emitted the protocol's { type: "ping" }
heartbeat. The server (src/server/ws/liveServer.ts) refreshes client
liveness only from inbound messages and terminates any client idle past
HEARTBEAT_TIMEOUT_MS (35s), so a healthy, connected-but-idle dashboard
client was force-terminated roughly every 35-45s, causing constant
reconnect churn (#10319).
Fix (both directions, per the analyzed plan):
- Client: start a 15s ping interval on open, cleared on close/unmount/
reconnect, so the connection stays inside the server's liveness window.
- Server (defense in depth): the outbound heartbeat pong now also bumps
client.lastActivity, so even a third-party client that never pings is
not dropped for being idle.
Regression coverage:
- tests/unit/useLiveDashboard-heartbeat.test.tsx: fast fake-timer check
that the hook emits periodic ping frames and cleans up the interval on
close/unmount (no leaked timers).
- tests/integration/live-ws-heartbeat-keepalive.test.ts: real WS-server
integration test asserting a silent-but-subscribed client stays
connected past the 35s heartbeat timeout (~50s window), converted from
the plan file's TDD RED repro.
Closes #10319
* fix(dashboard): stop renewing stale LiveWS sockets
Keep application-level heartbeat responses from refreshing server liveness, and add a regression covering silent stale sockets alongside clients that answer protocol heartbeats.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
|
||
|
|
8dc797fecd |
fix(dashboard): make provider card warning indicators expose the interaction they advertise (#10448)
* fix(dashboard): make provider card warning indicators expose the interaction they advertise The usage-risk indicator (subscriptionRisk) promised "click for details" in its tooltip but was a bare <span> with no onClick/role/dialog. The connection warning-count badge exposed neither a title tooltip (reasons) nor any click affordance, even though the reasons already exist in providerSpecificData.apiKeyHealth[]. Turn the risk indicator into a real <button role/aria-haspopup="dialog"> that opens an accessible Modal reusing the existing riskNotice copy, and wrap the warning badge in a keyboard- and pointer-interactive control that surfaces a sanitized reasons summary (max failure count + relative last-failure time, never raw upstream error text) and navigates to the connection detail/health view on activation. Both indicators are now visually distinct (bare icon vs. pill Badge). Closes #10261 * i18n(providers): sync riskNotice.detailsTitle + warningNotice keys to all 42 locales (#10261) Real Vietnamese translations (vi.json has a strict no-__MISSING__-marker gate); other 41 locales carry the sync-ui __MISSING__ placeholder pending the normal translation pass. * test(dashboard): relocate provider warning regression test Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
d49ccdaaf1 |
fix(sse): gate structural chat admission shedding on real heap pressure (#10437)
* fix(sse): gate structural chat admission shedding on real heap pressure Closes #10183, Closes #10268 3.8.49 (#9654/#9940) replaced the 3.8.48 heap-ratio shed (heapUsed/heapLimit >= 0.75) in chatBodyAdmission.ts with an unconditional CHAT_MAX_HEAVY_IN_FLIGHT=1 structural lease. A second concurrent "structurally heavy" chat request (>=200 messages, >=64 tools, or >=32k estimated tokens — routine for coding-agent fan-out like Hermes/Cursor/Claude Code) was hard-rejected with a retryable HTTP 503 chat_admission_busy/structure_limit regardless of actual heap pressure, even on a host with ample free RAM. Restore the heap-conditional gate as an ADDITIONAL check layered on top of (not a replacement for) the #9654 bounded-concurrency / per-connection-lane protection: when heavyweight capacity is busy, only enter the bounded-wait/shed path when a live heap-pressure probe (heapUsed / v8 heap_size_limit >= OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO, default 0.75) confirms real pressure. A healthy heap now admits the second heavy request immediately via a no-op lease instead of parking or shedding it. The probe is injectable via admitChatStructure({ heapPressureCheck }) for deterministic tests. Regression tests: - tests/unit/bug-10183-admission-heavy-healthy-heap.test.ts (new, permanent): healthy-heap 2nd heavy request now admitted (was RED); genuinely pressured heap still sheds it. - tests/unit/probe-10268-structural-503.test.ts (promoted to permanent): the exact reported 503 chat_admission_busy shape is still produced under real heap pressure, and the same fan-out is admitted on a healthy heap. - tests/unit/chat-body-admission.test.ts, tests/unit/chat-body-admission-queue.test.ts, tests/unit/per-connection-admission-9654.test.ts updated to inject heapPressureCheck: () => true where they exercise the busy/shed path, preserving #9654/#4380 coverage. Gates run: npm run typecheck:core (clean), eslint --suppressions-location config/quality/eslint-suppressions.json on changed files (clean), scripts/check/check-file-size.mjs (OK), scripts/check/check-test-discovery.mjs (OK), focused admission suite (68/68 passing) and npm run test:unit (in progress at commit time under heavy shared-devbox contention from a 13-way parallel session fan-out; no admission-related failures observed through 1873 lines of output, the sole failure seen was a pre-existing unrelated proxy/search timeout consistent with known load-induced flakiness, not a regression from this change). ⚠️ base-red inherited: #9985 — ESLint errors (2) from #10250 * docs(env): document OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO (#10183, #10268) * fix(sse): bound the healthy-heap admission fast path (#10437) The #10183/#10268 fix admitted a busy heavyweight request immediately whenever the heap was healthy, via an unconditional no-op lease with no bound of its own -- an unlimited number of "healthy heap" requests could pile in ahead of the heap-pressure shed path, defeating the point of admission control. Adds an independent, bounded healthy-heap headroom budget (CHAT_ADMISSION_HEALTHY_HEADROOM, tryAcquireHealthyHeadroom()) that the healthy-heap fast path draws from; once exhausted, requests fall through to the same bounded-wait/shed path used under real heap pressure, which is otherwise unchanged. Also fixes a pre-existing gap in per-connection-admission-9654.test.ts's shared-budget test, which needed an explicit heapPressureCheck override to keep exercising the #10110 invariant now that a healthy heap gets bounded headroom instead of an outright reject. * docs(env): document OMNIROUTE_CHAT_ADMISSION_HEALTHY_HEADROOM in .env.example Documented in docs/reference/ENVIRONMENT.md but missing from .env.example, caught by the env-doc-sync gate when combined with other PRs in the release merge-train. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
a4d6ad7da4 |
fix(sse): bridge generic compatible-provider type id to concrete node id in credential lookup (#10434)
* fix(sse): bridge generic compatible-provider type id to concrete node id in credential lookup getProviderSearchPool only bridged a provider string to a node id via the node's prefix, never via the generic derived type id (openai-compatible-chat / openai-compatible-responses / anthropic-compatible) that resolveProviderNodeForConnection already accepts at connection-creation time (#4421). A connection persisted under the generic type id was therefore unreachable when the chat path resolved the concrete uuid node id, surfacing "No active credentials for provider: openai-compatible-chat-<uuid>" even though the key and model catalog were valid. Closes #10085 * fix(sse): register #10085 mutation-coverage test file in stryker.conf.json check:mutation-test-coverage --strict flagged tests/unit/10085-compatible-generic-vs-uuid-credential.test.ts as a covering test for src/sse/services/auth.ts that was missing from stryker.conf.json's tap.testFiles, per the CI Fast Quality Gates run on PR #10434. * fix(sse): disambiguate compatible provider credential lookup Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(sse): require unambiguous type in both credential-lookup bridge directions (#10434) getProviderSearchPool()'s generic-type<->concrete-node-id bridge (#4421, #10085) only applied the "exactly one node of this derived type" ambiguity guard to the concrete-id -> generic-type direction. The generic-type -> concrete-id direction added every node sharing a derived type to the search pool unconditionally, so a bare generic-type lookup could resolve to a connection scoped to one specific node's baseUrl/headers even when a second node shares the same derived type -- leaking that node's credentials/upstream URL into an unrelated node's request. Both directions now share the same typeIsUnambiguous gate, mirroring the rule already enforced by selectProviderNodeForConnection() for connection creation (src/lib/db/providerNodeSelect.ts, #4421). --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
0f448d64e2 |
fix(dashboard): remap Kimi Code API-key save to admitted managed id (#10096) (#10417)
* fix(dashboard): remap Kimi Code API-key save to admitted managed id (#10096) The unified Kimi Code card's API-key branch posted provider: "kimi-coding" to POST /api/providers. "kimi-coding" is an OAuth-primary managed id, not an admitted API-key/dual-auth connection id, so the backend correctly rejected it with 400 "Invalid provider" even though key validation passed. Add resolveApiKeySaveProviderId() in useApiKeySave.ts to remap the posted provider id to the dedicated, admitted managed API-key id "kimi-coding-apikey" for the API-key save flow only. The OAuth flow (handleOAuthSuccess in ProviderDetailPageClient.tsx) never calls this hook and keeps posting "kimi-coding" unchanged. Regression test: tests/unit/bug-10096-kimi-coding-apikey-save.test.ts * fix(dashboard): remap Kimi Code bulk API-key save Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
514573b1f6 |
fix(proxy-subscriptions): allow local/loopback proxy-subscription fetch URLs (#10416)
* fix(proxy-subscriptions): allow local/loopback proxy-subscription fetch URLs
The subscription fetch guard (fetchGuard.ts) unconditionally blocked all
loopback/private IP ranges as SSRF protection, but the same feature already
permits loopback for the routing half (coreEndpoint.ts's
ALLOWED_LOCAL_CORE_HOSTS) — so an operator could route traffic through a
loopback core but could not fetch a proxy list from a loopback HTTP server.
Make the fetch guard local-first by reusing the existing
areLocalProviderUrlsAllowed() policy (default ON) from
outboundUrlGuardPolicy.ts: loopback/private hosts are now allowed as fetch
targets by default, while cloud-metadata/link-local (169.254.0.0/16, incl.
169.254.169.254 IMDS) and the unspecified address stay blocked
unconditionally, mirroring the provider-validation guard's "block-metadata"
mode. Callers that want the old strict behavior can pass
{ allowLocal: false }.
Closes #10158.
* fix(proxy-subscriptions): unwrap IPv4-mapped IPv6 + full fe80::/10 range (#10416)
The #10158 SSRF guard left two gaps on the IPv6 side: an IPv4-mapped IPv6
literal (::ffff:a.b.c.d) skipped IPv4 range checking entirely, and the
link-local check only matched strings literally prefixed with "fe80"
instead of the full fe80::/10 range (fe80:: - febf:ffff::), so fe90::,
febf:ffff::, etc. were wrongly allowed through.
isIpv6Blocked() now unwraps mapped IPv4 addresses (both the dotted-quad
and WHATWG-normalized hex-group forms) and re-checks them against the
IPv4 rules, and link-local detection parses the first hex group's numeric
value against the 0xfe80-0xfebf range instead of a string prefix.
---------
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
|
||
|
|
548316a2c4 |
fix(translator): Normalize tool call names from lowercase to PascalCase when translating upstream responses to Claude Messages API format (#10392)
* fix(translator): Normalize tool call names from lowercase to PascalCase (#1) * Fix: Map lowercase tool names from Antigravity (Gemini format) to Claude Code expected PascalCase * Fix: toolNameMap in fun restoreClaudePassthroughToolUseName * fix(translator): Normalize tool call names from lowercase to PascalCase when translating upstream responses (OpenAI, Gemini, Antigravity) to Claude Messages API format This resolves `Error: No such tool available: read`/`bash`/`write` errors when using Claude Code CLI with third-party providers that emit lowercase tool names. The fix adds case-insensitive tool name lookups in `openai-to-claude.ts`, `gemini-to-claude.ts`, and related translators, ensuring tool names like `read`/`bash` are mapped to `Read`/`Bash` before being sent to Claude Code. Includes unit tests and comprehensive changelog notes ([#10250](https://github.com/diegosouzapw/OmniRoute/pull/10250)) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(translator): Parse <tool_call> JSON and TOOL_CALL text formats fr… (#2) * fix(translator): Parse <tool_call> JSON and TOOL_CALL text formats from model output Some models (DeepSeek, Qwen) emit tool calls as text instead of proper tool_calls JSON: either <tool_call>{...}</tool_call> or TOOL_CALL Name: {...}. Extend extractXmlInvokeBlocks to handle all 3 formats in a single scan pass, picking whichever pattern appears first. Includes unit tests for all formats. * fix(translator): Parse text-format tool calls in gemini-to-claude translator Extend the Gemini->Claude translator to detect <invoke>, <tool_call> JSON, and TOOL_CALL text formats emitted inline in text parts (Antigravity/Gemini models), converting them to proper tool_use content blocks instead of leaking raw text to Claude Code. * docs(changelog): Add changelog entry for text tool call parsing fix * fix(translator): consolidate tool name casing normalization and restore thought-signature persistence (#3) * fix(translator): sanitize tool_use.id and tool_result.tool_use_id to match Anthropic schema (#4) Ensure tool IDs from OpenAI-compatible upstreams (which may contain dots, colons, or special characters) are sanitized to ^[a-zA-Z0-9_-]+$ in response translators and passthrough requests before reaching Claude endpoints. * fix(responses): preserve native tools for openai-compatible Responses targets (#5) A Responses-shaped request to a custom openai-compatible connection whose outbound protocol is Responses took a Responses -> Chat -> Responses round trip, so Codex custom tools lost their grammar (`exec`), namespace groups were flattened (`collaboration`), and tool invocations failed upstream. Gate a native Responses passthrough on the connection's configured protocol (`apiType: "responses"` / `_omnirouteForceResponsesUpstream`) so the original tool definitions reach a Responses-capable upstream unchanged. Chat-only connections keep the existing downgrade. Closes #10374 * fix(translator): add support for 'applypatch' tool name in tool call checks * test(translator): add unit test for apply_patch and applypatch tool name remapping * fix(translator): remove no-explicit-any lint errors in tool-use-id-sanitization test Type the openaiToClaudeResponse/translateNonStreamingResponse return values with narrow local shapes instead of `any`, satisfying the repo's no-explicit-any = error rule for tests/. No behavior change — the same 3 assertions still pass. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test: update 9568 casing regression to match #10392's consolidated fix restoreClaudeToolName's static casing map now normalizes known lowercase tool names to canonical PascalCase unconditionally on the gemini-to-claude and openai-to-claude Claude Messages API paths (not gated behind toolNameMap), superseding the earlier per-map-only fix that the original #9568 regression test locked in as "expected" (it was previously labeled a known bug case). The gemini-to-openai passthrough path is unaffected by #10392 and keeps its original pass-through assertion. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
45ff8d4de0 |
fix(services): use CLIProxy executable on Windows (#10371)
* fix(services): use CLIProxy executable on Windows * fix(services): align Windows CLIProxy artifact path --------- Co-authored-by: tkgo11 <7.1800574e+07+tkgo11@users.noreply.github.com> |
||
|
|
ceced68817 |
feat(oauth): add gemini-3.7-flash models for antigravity and agy providers (#10305)
* feat(oauth): add gemini-3.7-flash models with reasoning tiers for antigravity Support gemini-3.7-flash and its thinking tiers (low/medium/high) for antigravity and agy providers. - Define public models, pricing, modelSpecs, and CLI tool definitions - Map tiers to live upstream id gemini-3.7-flash-tiered - Configure defaultThinkingBudget (low: 1024, medium: 8192, high: 32768) - Allow executor fallback on upstream 404 and 5xx errors - Add unit tests in antigravity-model-aliases.test.ts * fix(oauth): expose gemini-3.7-flash as one callable antigravity/agy model Upstream (fetchAvailableModels on daily-cloudcode-pa) only accepts the single upstream id gemini-3.7-flash-tiered; the high/medium/low suffixed tier ids 404. Registering all four as distinct public model ids violates the base #3696 uniqueness invariant (no two ANTIGRAVITY_PUBLIC_MODELS entries may resolve to the same upstream id). Collapse to the single live gemini-3.7-flash public model (aliased to gemini-3.7-flash-tiered) and drop the tiered specs, pricing, free-catalog and CLI entries accordingly, keeping the leading public model order (Gemini 3.6 tiers first) intact. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: Chewji9875 <Chewji9875@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
ac2439b8af |
fix(api): scale pool usage snapshot limits by pool member count (summed budget) (#10253)
* fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190) Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13 (with monaco-editor scoped override). Closes Dependabot #189, #190. Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge — awaiting Dependabot re-scan. npm audit → 0 vulnerabilities. * fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) _tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential _tasks symlink can slip in via git add -A and, once pulled, checkout materializes it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks ignores the symlink too, preventing re-capture. * Hide health-check excluded models from /v1/models catalog (#10026) Mirror the request-time exclusion rule (provider_specific_data.excludedModels) in the unified catalog builder: a model is hidden when its provider has connections but none of them is eligible for it. Applied across the PROVIDER_MODELS, synced, custom, alias-backed, and managed-fallback loops so ghost models no longer appear as available. Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com> * fix(models): memoize getModelsDevPricing (event loop / healthz) (#10055) * fix(models): memoize getModelsDevPricing for /v1/models catalog resolveCatalogPricing called getModelsDevPricing once per model while building GET /v1/models. Each call re-scanned models_dev_pricing and JSON.parsed every row (~10k SQL scans + multi-GB parse work), pegging the event loop so even /healthz timed out (#9685, #10052). Memoize the parsed map until saveModelsDevPricing / clearModelsDevPricing and add a unit test for invalidation. Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> * fix(db): invalidate modelsDevPricing cache on DB reset (#10055) Copilot review fixes: 1. Register invalidateModelsDevPricingCache() with DB state reset system so resetDbInstance() clears the process-local memo, preventing stale pricing data from surviving across DB reset/restore operations. 2. Add test assertion verifying DB reset bypasses the memo (Copilot #10055). The process-local memo at modelsDevSync.ts:204 caches getModelsDevPricing() results until saveModelsDevPricing()/clearModelsDevPricing() to avoid re-scanning all pricing rows on every /v1/models request. Without this hook, backup restore and test DB resets would serve stale cached data from the previous connection. Tests: npm run test:unit:serial -- tests/unit/modelsDevSync-extended.test.ts --------- Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> * fix(api): scale pool usage snapshot limits by member count (summed budget) --------- Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: ritheshcn25 <rithesh.chandran@snb.ca> Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com> Co-authored-by: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
1089c24bc8 |
Remove/mimocode sunset provider (#10186)
* remove: drop sunset MiMoCode provider from model catalog * remove: drop sunset MiMoCode provider from model catalog (shared.ts) Remove unused imports, types, and comments from shared.ts. * remove: MiMoCode provider (Xiaomi sunset) — executor, registry, no-auth config, icon, tests * refactor(providers): finish MiMoCode removal — sweep remaining no-auth references Drop the leftover mimocode entries from the no-auth provider controls, the translate-path snapshot, the eslint suppressions, and the #3061 auth-loop test. Re-point the fingerprint-pin (#6696) and proxy-noauth (#6272) tests at opencode, which exercises the same fingerprint path, so the removal does not break runtime behavior. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(providers): reconcile provider/executor counts after MiMoCode sunset The base's parallel doc-count sync (#10433) pinned 340 providers / 101 executors. With mimocode removed, live code has 339 providers and 100 executors; refresh the user-facing counts (package.json description, llm.txt, README/AGENTS, i18n llm.txt, provider reference, diagrams) so the check-docs-counts STRICT gate stays green. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * test(providers): fix orphaned mimocode references after MiMoCode sunset The sunset removed mimocode/mcode from the free-onboarding candidates and from FINGERPRINT_PROVIDERS, but two tests still referenced them: - free-provider-onboarding-setup: the mimocode->theoldllm substitution introduced duplicate 'opencode' rows (impossible given the request-set dedupe) and the wrong display name; align expectations with the actual {opencode, theoldllm} dedupe behavior and 'The Old LLM (Free)' name. - combo-system-prompt-templates-5501: resolveTargetFingerprint tested with provider 'mcode', which is no longer a fingerprint provider; point it at the remaining fingerprint provider 'opencode'. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Tushar49 <Tushar49@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> |
||
|
|
2d50ec0789 |
feat(routing): add quota-aware provider scheduling — Phase 2 (#10126)
* feat(quota): Phase 2 adapters, reset timers, analytics, and dashboard API
* feat(routing): add quota-aware provider scheduling (opt-in)
* fix(db): rename migration to 148_provider_quota_state.sql
* fix(quota): harden quota state route, isolate phase2 tests, slim env diff
- route: requireManagementAuth + Zod body validation + buildErrorBody
sanitization (Hard Rule #12); fix clearProviderQuotaState -> clearProviderQuota
- .env.example/ENVIRONMENT.md: drop ~20 foreign vars, keep only
OMNIROUTE_QUOTA_AWARE_ROUTING (migration 148)
- tests/unit/quota-phase2.test.ts: DATA_DIR mkdtemp + resetDbInstance teardown
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* chore(ci): fix docs-sync + eslint-suppression drift for quota branch
CI gates flagged on PR #10126 head
|
||
|
|
100c9dd3fa |
perf(logging): offload call-log artifacts to a worker (#10123)
* perf(logging): offload call-log artifacts to a worker * test(call-log): raise drain wait timeout for cold worker spawn The first cold spawn of the worker_threads artifact worker can take ~2.4s before queued artifact writes start draining, so a 2s wait in call-log-save-drain.test.ts flakes on cold runs. Raise it to 10s. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: xz-dev <xz-dev@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
0a74bfbdea |
feat(cli): relay-like CLI closure — target manifest, Codex TOML, Gemini launcher, guards
- canonical executable manifest (bin/cli/cli-manifest.mjs): run/configure/completion derive targets, aliases and --model wiring from one table; drift test cross-checks manifest x cliRuntime x UI catalog (tests/unit/cli/cli-manifest-drift.test.ts) - dashboard Codex generator converged to ~/.codex/config.toml (modern Codex v0.137+, verified against codex-cli 0.147.0): conservative merge, env_key auth (key never written), refuses invalid TOML, reports legacy config.yaml as migration note - omniroute run gemini: launcher over OmniRoute's /v1beta surface via GOOGLE_GEMINI_BASE_URL + isolated GEMINI_CLI_HOME forcing gemini-api-key auth (contract proven against @google/gemini-cli 0.50.0); ACP registration kept distinct - opt-in real smoke harness for upstream CLIs (RUN_CLI_SMOKE=1, credential by env NAME, redacted output): tests/integration/upstream-cli-smoke.int.test.ts - container-guard homologation for POST /api/cli-tools/apply (422 in container, dry-run preview allowed, host write passes) + docs; guard untouched - typecheck: omniglyphAdapter union narrowing, usageTracking typed signatures (UsageLike, no any), models.ts isValidModel params — typecheck:core and typecheck:noimplicit:core now clean - relay core (prior session of this effort): omniroute run for 6 CLIs, configure picker with per-context favorites/recents, contexts with optional keychain + 0600 fallback, provider CRUD with recursive redaction, completion updates, docs |
||
|
|
a5b32557d6 | refactor(video): use the structured pino logger in the broker extract route | ||
|
|
e7858d7165 |
feat(video): cap the drill-down cache with a global byte budget
The per-session drill-down cache now tracks decoded bytes per entry and evicts least-recently-used entries until an aggregate maxTotalBytes budget fits (route sets 256 MiB); an entry larger than the whole budget is rejected. Prevents the previous worst case of 64 x 32 MiB (~2 GiB) pinned in memory. |
||
|
|
533e5c6ec7 |
feat(video): surface audio/video fusion telemetry and degrade invalid audio to partial
The fusion result's availability, partial and failure fields now reach DescribedVideo.fusion, the guardrail meta (audioFusionRuns/Partials/ FailureCodes), the result-cache metadata and bridge stats. Audio transcript validation moved inside the fusion's audio branch, so an invalid audioTranscript records failures.audio and keeps the visual description instead of failing the whole video. |
||
|
|
ffb0cbc10b | fix(video): include audioTranscript and focus window in the result cache key | ||
|
|
68b3fe715a | feat(video): add timestamped contact sheets |