From 19d91d82e2257b9475724109f2cca9d869af5907 Mon Sep 17 00:00:00 2001
From: Diego Rodrigues de Sa e Souza
<8016841+diegosouzapw@users.noreply.github.com>
Date: Tue, 23 Jun 2026 03:08:29 -0300
Subject: [PATCH] Release v3.8.34 (#4614)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* chore(release): open v3.8.34 development cycle
* chore(quality): release-green pre-flight validator + nightly signal (C+D) (#4622)
C — scripts/quality/validate-release-green.mjs (npm run check:release-green):
reproduces the release-equivalent validation (typecheck, eslint, db-rules,
public-creds, full unit, vitest, ratchets, optional --with-build package-artifact)
against the current working tree and classifies each red as HARD (real defect,
exit 1) vs DRIFT (ratchet — reported, never affects exit / never blocks). Pure
helpers exported + orchestration behind a direct-run guard; unit-tested.
D — .github/workflows/nightly-release-green.yml: runs C on the active release
branch nightly (and on workflow_dispatch) and opens/updates a single tracking
issue on HARD failures. Never a required check, never touches a contributor PR.
Closes the gap where the full gate (ci.yml) only ran on the release PR, so reds
accrued silently on release/** and surfaced in 40-min layers at release time.
Non-blocking by construction; drift is the maintainer's to rebaseline at release.
Co-authored-by: Diego Rodrigues de Sa e Souza
* fix(providers): show revealed connection API keys (#4583)
Integrated into release/v3.8.34
* fix(resilience): respect upstream retry hint toggle (#4585)
Integrated into release/v3.8.34
* feat(settings): expose stream recovery feature flags (#4586)
Integrated into release/v3.8.34
* fix(logs): make active request stale sweep configurable (#4599)
Integrated into release/v3.8.34
* fix(plugin): auto-prefix providerId with 'opencode-' for OC 1.17.8+ native gate (#4527)
Integrated into release/v3.8.34 (supersedes #4445)
* fix(models): treat unknown output caps as unset (#4584)
Integrated into release/v3.8.34
* fix(executors): strip temperature for GitHub Copilot gpt-5.4 family (#4564)
Integrated into release/v3.8.34 (rebuilt onto tip)
* fix(oauth): update Qwen OAuth URLs from chat.qwen.ai to qwen.ai (#4561)
Integrated into release/v3.8.34 (rebuilt onto tip)
* fix(api/settings): prevent cached /api/settings responses (port from 9router#951) (#4566)
Integrated into release/v3.8.34 (rebuilt onto tip)
* feat(audio): MiniMax T2A v2 TTS dispatch in audioSpeech (port #1043) (#4553)
Integrated into release/v3.8.34 (rebuilt onto tip)
* fix(dashboard): surface manual config CTA when Open Claw CLI auto-detect fails (#4562)
Integrated into release/v3.8.34 (rebuilt onto tip)
* feat(providers): optional model ID for custom API-key validation (#4555)
Integrated into release/v3.8.34 (rebuilt onto tip)
* fix(cli): align data dir and env loading with runtime (#4607)
Integrated into release/v3.8.34 (rebuilt onto tip)
* fix(quota): expose Bailian quota windows (#4610)
Integrated into release/v3.8.34 (rebuilt onto tip)
* fix: retain provider cooldowns for configured max window (#4588)
Integrated into release/v3.8.34 (rebuilt — bundled commits stripped)
* fix: reject invalid provider cooldown bounds (#4589)
Integrated into release/v3.8.34 (rebuilt — bundled commits stripped)
* fix: preserve production combo metrics on shadow eviction (#4590)
Integrated into release/v3.8.34 (rebuilt — bundled commits stripped)
* fix(stream): estimate input tokens when upstream reports prompt_tokens=0 (#4615)
Integrated into release/v3.8.34 (rebuilt onto tip)
* fix(catalog): shorten no-thinking gateway prefix to no-think/ (#4525)
Integrated into release/v3.8.34 (rebuilt — kept only the prefix rename, dropped stale-base reverts)
* fix(relay): apply IP rate limit to bifrost sidecar (#4593)
Integrated into release/v3.8.34 (rebuilt onto tip; merge before #4612)
* fix(bifrost): finalize SSE relay usage after stream (#4612)
Integrated into release/v3.8.34 (rebuilt + reconciled with #4593)
* feat(compression): per-request `x-omniroute-compression` header (Phase 3) (#4645)
* docs(compression): Phase 3 per-request header design spec
Approved brainstorming output for the x-omniroute-compression header:
header-first precedence, name-first combo matching (Decision A), explicit
value bypasses auto-trigger (Decision B), DerivedPlan.source, and the
X-OmniRoute-Compression response header.
Co-Authored-By: Claude Opus 4.8 (1M context)
* docs(compression): Phase 3 per-request header implementation plan
4-task TDD plan (resolver header-first + source, parser, chatCore wiring +
response header, docs/file-size) with full code and exact commands.
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(compression): header-first resolver + plan source (Phase 3 core)
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(compression): resolveCompressionHeader parser (Phase 3)
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(compression): wire x-omniroute-compression header + response header (Phase 3)
Co-Authored-By: Claude Opus 4.8 (1M context)
* refactor(compression): extract plan-resolution leaf (planResolution.ts) under size cap (Phase 3)
Co-Authored-By: Claude Opus 4.8 (1M context)
* docs(compression): document x-omniroute-compression header (Phase 3)
Co-Authored-By: Claude Opus 4.8 (1M context)
* fix(compression): harden named-combo map + trim engine: header id (Phase 3 review)
Addresses gemini-code-assist review on #4645:
- Extract buildNamedComboLookup (pure) so a blank/whitespace/null combo name
contributes only its id key (no '' key, no throw that disables all combos).
- Trim the engine: header value so 'engine: rtk' resolves.
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Diego Rodrigues de Sa e Souza
Co-authored-by: Claude Opus 4.8 (1M context)
Co-authored-by: Diego Rodrigues de Sa e Souza
* fix: exclude exhausted connections from auto scoring (#4592)
Integrated into release/v3.8.34 (rebuilt + opt-in gate fix)
* fix(dashboard): memoize compatible provider groups (#4613)
Integrated into release/v3.8.34 (rebuilt + test added)
* fix(dashboard): isolate quota widget refresh clock (#4611)
Integrated into release/v3.8.34 (rebuilt + jsdom test)
* fix(dashboard): gate topology side effects behind widget visibility (#4606)
Integrated into release/v3.8.34 (rebuilt + jsdom test)
* fix(dashboard): keep play_arrow spinning on provider Test All buttons (#4563)
Integrated into release/v3.8.34 (rebuilt onto tip; UI-cosmetic per owner)
* fix(db): schedule retention cleanup + fix cleanup table/column names (extracted from #4428) (#4691)
Integrated into release/v3.8.34 (cleanup core extracted from #4428, credit @oyi77)
* fix(telemetry): back off live-WS event forwarding when the sidecar is unreachable (#4604) (#4687)
Co-authored-by: Diego Rodrigues de Sa e Souza
* fix(api): serve GET /v1/models/{model} as JSON, not the HTML dashboard (#4674) (#4677)
Co-authored-by: Diego Rodrigues de Sa e Souza
* feat(opencode): add go deepseek reasoning variants (#4647)
Integrated into release/v3.8.34
* fix(executors): robust deepseek-web tool-call parsing and agentic context retention (#4644)
Integrated into release/v3.8.34
* fix(cli): authenticate `omniroute logs` and honor active context (#4638)
Integrated into release/v3.8.34 (authored by Rahul Sharma, AI co-author trailer stripped per project policy)
* fix(proxy): apply pipelining:0 + connections cap to the direct dispatcher (#4580) (#4684)
Co-authored-by: Diego Rodrigues de Sa e Souza
* fix(executors): Firecrawl web_fetch 500 with include_metadata=true (#4692)
Integrated into release/v3.8.34
* fix(routing): include all noAuth models in auto-combos + add reka-flash + best-free template (#4621)
Integrated into release/v3.8.34 (dead getFirstRegistryModelId dropped, rebuilt onto tip)
* fix(dashboard): gate home topology live-WS networking (#4596) (#4618)
Integrated into release/v3.8.34 (adapted onto #4606's extracted topology section: default-hidden flip + enabled gate on useLiveDashboard)
* fix(cli): align `omniroute` env loading with the runtime data dir (#4597) (#4619)
Integrated into release/v3.8.34 (data-dir.mjs refactor reconciled with #4607; loadEnvFile aligned to getDefaultDataDir)
* chore(quality): reconcile file-size baseline for #4644 (deepseek-web.ts 1117->1125) (#4695)
file-size reconcile for #4644
* Support quota scraping for OpenCode Go and Ollama Cloud (#4642)
Integrated into release/v3.8.34 (Ollama Cloud + OpenCode Go dashboard quota scraping; rebuilt onto tip, gates green: typecheck/public-creds/file-size/lint/docs-sync + 31 tests)
* feat(executors): land M365 Copilot pure framing + connection helpers (#4042) (#4696)
Land M365 pure modules ahead of draft #4400
* deps: bump production + development groups; migrate js-yaml to v5 ESM (#4697)
Incorporates Dependabot #4667 + #4668 + js-yaml v5 ESM migration into release/v3.8.34
* fix: noAuth provider validation + kimi executor routing (#4699)
Integrated into release/v3.8.34 (noAuth in NOAUTH_PROVIDERS dynamic check + remove misrouted kimi web alias; 9 tests)
* refactor(imageGeneration): extract 8 provider families to co-located files (#4609)
Integrated into release/v3.8.34 (extraction completed: added missing imports/exports per module, main imports handlers locally; 145 image-gen tests pass, typecheck/cycles/file-size green)
* chore(release): v3.8.34 — finalize changelog, rebaseline drift, fix release-green reds
- Finalize CHANGELOG [3.8.34] (43 bullets, full contributor attribution) + seed i18n mirrors
- Rebaseline inherited cycle drift surfaced by release-green pre-flight: eslint warnings
3900->3907, cognitive-complexity 797->801 (release-finalize touches no prod code; all
drift is from this cycle's contributor merges)
- fix(providers): keep reka-flash-3 as the Reka provider default. #4621 inserted reka-flash
at the head of the model list, silently changing the default from reka-flash-3 (the
free-tier model) to reka-flash; reorder so reka-flash-3 stays default, reka-flash retained.
- test: align provider-models-config / provider-models-route / web-cookie-providers-new with
#4621 (reka-flash now in the Reka catalog) and #4699 (the `kimi` API-key provider correctly
falls through to DefaultExecutor instead of KimiWebExecutor)
- chore(quality): allowlist the COMPRESSION_GUIDE doc name in check-fabricated-docs
(false-positive env-var match; docs/compression/COMPRESSION_GUIDE.md exists)
* fix(release-green): resolve release-PR full-CI reds for v3.8.34
Surfaced only on the release PR (these gates don't run on PR->release fast-gates):
- fix(quota): complete HTML-comment sanitization in opencodeOllamaUsage SSR reset-time
parsing — strip any generically instead of the two literal React hydration
markers, so no partial "|$)` arm consumes a trailing "", so no
partial " form because an unclosed comment could still leave " / hydration markers). The `(?:-->|$)` arm consumes a
+ // trailing "" too, so no partial "|$)/g, "").replace(/Resets?\s*in\s*/i, "")
+ );
+ if (resetInSec === null || !Number.isFinite(resetInSec)) continue;
+ const window = {
+ usagePercent,
+ resetAt: safeToIsoString(Date.now() + Math.max(0, resetInSec) * 1000),
+ };
+ if (label.includes("rolling")) usage.session = window;
+ else if (label.includes("weekly")) usage.weekly = window;
+ else if (label.includes("monthly")) usage.mcp_monthly = window;
+ }
+ return usage.session || usage.weekly || usage.mcp_monthly ? usage : null;
+}
+
+async function fetchOpenCodeGoDashboardUsage(
+ config: Extract
+) {
+ const url = `${OPENCODE_GO_DASHBOARD_BASE_URL.replace(/\/+$/, "")}/${encodeURIComponent(
+ config.workspaceId
+ )}/go`;
+ const response = await fetch(url, {
+ headers: {
+ Accept: "text/html",
+ Cookie: `auth=${normalizeOpenCodeGoAuthCookie(config.authCookie)}`,
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0",
+ },
+ signal: AbortSignal.timeout(10_000),
+ });
+ if (!response.ok)
+ return { usage: null, message: `OpenCode Go dashboard error (${response.status}).` };
+ const usage = parseOpenCodeGoDashboardHtml(await response.text());
+ return {
+ usage,
+ message: usage ? undefined : "OpenCode Go dashboard response did not contain quota windows.",
+ };
+}
+
+export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?: JsonRecord) {
+ const dashboardConfig = resolveOpenCodeGoDashboardConfig(providerSpecificData);
+ if (dashboardConfig.state === "incomplete") {
+ return {
+ message: `OpenCode Go dashboard quota config is incomplete. Missing ${dashboardConfig.missing}.`,
+ };
+ }
+ if (dashboardConfig.state === "configured") {
+ try {
+ const dashboard = await fetchOpenCodeGoDashboardUsage(dashboardConfig);
+ if (!dashboard.usage) {
+ return { message: dashboard.message || "OpenCode Go dashboard quota data unavailable." };
+ }
+ const quotas: Record = {};
+ for (const quotaName of OPENCODE_GO_QUOTA_ORDER) {
+ const usage = dashboard.usage[quotaName];
+ if (usage) {
+ quotas[quotaName] = buildOpenCodeGoDollarQuota(
+ quotaName,
+ usage.usagePercent,
+ usage.resetAt
+ );
+ }
+ }
+ return { plan: "OpenCode Go", quotas: orderOpenCodeGoQuotas(quotas) };
+ } catch (error) {
+ return { message: `OpenCode Go dashboard quota error: ${sanitizeErrorMessage(error)}` };
+ }
+ }
+
+ const token = apiKey.trim().replace(/^Bearer\s+/i, "");
+ if (!token) {
+ return {
+ message:
+ "OpenCode Go quota requires OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE. " +
+ "The API key can be used for chat/models, but OpenCode Go does not expose quota via API key.",
+ };
+ }
+
+ try {
+ const res = await fetch(OPENCODE_GO_QUOTA_URL, {
+ headers: {
+ Authorization: buildBearerAuthorization(token),
+ "Accept-Language": "en-US,en",
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ });
+ if (!res.ok) {
+ if (res.status === 401 || res.status === 403) {
+ return {
+ message:
+ "OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
+ "Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.",
+ };
+ }
+ return {
+ message:
+ `OpenCode Go quota API error (${res.status}). ` +
+ "Set OMNIROUTE_OPENCODE_GO_QUOTA_URL to a working endpoint, or follow " +
+ "https://github.com/anomalyco/opencode/issues/16017 for upstream status.",
+ };
+ }
+
+ let json: unknown;
+ try {
+ json = await res.json();
+ } catch {
+ return { message: "OpenCode Go quota response parsing failed." };
+ }
+ const root = toRecord(json);
+ if (
+ toNumber(root.code, 200) === 401 ||
+ toNumber(root.code, 200) === 403 ||
+ root.success === false
+ ) {
+ return {
+ message:
+ "OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
+ "Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.",
+ };
+ }
+
+ const data = toRecord(root.data);
+ const quotas: Record = {};
+ for (const limit of Array.isArray(data.limits) ? data.limits : []) {
+ const src = toRecord(limit);
+ const type = String(src.type || "").toUpperCase();
+ const resetAt = parseResetTime(src.nextResetTime);
+ if (type === "TOKENS_LIMIT" || type === "TOKEN_LIMIT") {
+ const quotaName = getOpenCodeGoTokenQuotaName(src, quotas);
+ quotas[quotaName] = buildOpenCodeGoDollarQuota(
+ quotaName,
+ src.percentage,
+ resetAt,
+ undefined,
+ Array.isArray(src.models)
+ ? src.models.map((model) => {
+ const modelInfo = toRecord(model);
+ return {
+ name: String(modelInfo.model || modelInfo.modelCode || "usage"),
+ used: toNumber(modelInfo.percentage, 0),
+ };
+ })
+ : undefined
+ );
+ } else if (type === "TIME_LIMIT" || type === "TIME_USAGE_LIMIT") {
+ quotas.mcp_monthly = buildOpenCodeGoDollarQuota(
+ "mcp_monthly",
+ src.percentage,
+ resetAt,
+ src.currentValue,
+ Array.isArray(src.usageDetails)
+ ? src.usageDetails.map((item) => {
+ const detail = toRecord(item);
+ return {
+ name: String(detail.modelCode || detail.name || "usage"),
+ used: toNumber(detail.usage, 0),
+ };
+ })
+ : undefined
+ );
+ }
+ }
+
+ const levelRaw =
+ typeof data.planName === "string"
+ ? data.planName
+ : typeof data.level === "string"
+ ? data.level
+ : "";
+ const planLabel = toTitleCase(levelRaw.replace(/\s*plan$/i, ""));
+ return {
+ plan: planLabel
+ ? /^opencode\s+go\b/i.test(planLabel)
+ ? planLabel
+ : `OpenCode Go ${planLabel}`
+ : null,
+ quotas: orderOpenCodeGoQuotas(quotas),
+ };
+ } catch (error) {
+ return { message: `OpenCode Go quota API error: ${sanitizeErrorMessage(error)}` };
+ }
+}
+
+function resolveOllamaCloudConfig(providerSpecificData?: JsonRecord): OllamaCloudConfig {
+ const cookie =
+ process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE?.trim() ||
+ process.env.OLLAMA_USAGE_COOKIE?.trim() ||
+ process.env.OLLAMA_CLOUD_USAGE_COOKIE?.trim() ||
+ getProviderSpecificString(providerSpecificData, [
+ "ollamaUsageCookie",
+ "ollamaCloudUsageCookie",
+ "ollamaCloudCookie",
+ "usageCookie",
+ "cookie",
+ ]);
+ if (!cookie) return { state: "none" };
+ if (cookie.includes("\r") || cookie.includes("\n")) {
+ return { state: "invalid", error: "Ollama Cloud cookie contains invalid CRLF characters." };
+ }
+ return { state: "configured", cookie };
+}
+
+function normalizeOllamaCloudCookie(value: string): string {
+ const trimmed = value.trim();
+ return trimmed.toLowerCase().startsWith(`${OLLAMA_CLOUD_SESSION_COOKIE.toLowerCase()}=`)
+ ? trimmed.slice(OLLAMA_CLOUD_SESSION_COOKIE.length + 1).trim()
+ : trimmed;
+}
+
+function extractOllamaUsagePercent(trackHtml: string): number | null {
+ const tagHeader = trackHtml.match(/^[^>]*/)?.[0] ?? "";
+ const ariaMatch = tagHeader.match(/(\d+(?:\.\d+)?)%\s*used/);
+ if (ariaMatch) {
+ const pct = toNumber(ariaMatch[1], Number.NaN);
+ if (Number.isFinite(pct) && pct >= 0 && pct <= 100) return pct;
+ }
+ const style = tagHeader.match(/style="([^"]*)"/)?.[1] ?? "";
+ const pct = toNumber(style.match(/(?:^|;)\s*width\s*:\s*([0-9.]+)%/)?.[1], Number.NaN);
+ return Number.isFinite(pct) && pct >= 0 && pct <= 100 ? pct : null;
+}
+
+function parseOllamaCloudSettingsHtml(html: string): OllamaCloudUsage | null {
+ const parts = html.split(/\bdata-usage-track\b/);
+ if (parts.length < 2) return null;
+ const extractTime = (text: string): string | null => {
+ const match = text.match(/class="[^"]*local-time[^"]*"[^>]*data-time="([^"]*)"/);
+ return match?.[1] || null;
+ };
+ const sessionPercent = extractOllamaUsagePercent(parts[1]);
+ const weeklyPercent = parts[2] ? extractOllamaUsagePercent(parts[2]) : null;
+ if (sessionPercent === null && weeklyPercent === null) return null;
+ return {
+ ...(sessionPercent !== null
+ ? { session: { usagePercent: sessionPercent, resetAt: extractTime(parts[1]) } }
+ : {}),
+ ...(weeklyPercent !== null
+ ? { weekly: { usagePercent: weeklyPercent, resetAt: extractTime(parts[2]) } }
+ : {}),
+ planTier: html.match(/class="[^"]*capitalize[^"]*"[^>]*>([^<]*))?.[1]?.trim() || null,
+ };
+}
+
+async function fetchOllamaCloudUsageFromSettings(
+ config: Extract
+) {
+ const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
+ redirect: "manual",
+ headers: {
+ Accept: "text/html",
+ Cookie: `${OLLAMA_CLOUD_SESSION_COOKIE}=${normalizeOllamaCloudCookie(config.cookie)}`,
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0",
+ },
+ signal: AbortSignal.timeout(10_000),
+ });
+ if (response.status >= 300 && response.status < 400) {
+ return { usage: null, message: "Ollama Cloud authentication expired. Refresh the cookie." };
+ }
+ if (!response.ok)
+ return { usage: null, message: `Ollama Cloud settings error (${response.status}).` };
+ const usage = parseOllamaCloudSettingsHtml(await response.text());
+ return {
+ usage,
+ message: usage ? undefined : "Ollama Cloud settings page did not contain usage quota tracks.",
+ };
+}
+
+export async function getOllamaCloudUsage(providerSpecificData?: JsonRecord) {
+ const config = resolveOllamaCloudConfig(providerSpecificData);
+ if (config.state === "none") {
+ return {
+ message:
+ "Ollama Cloud quota requires OLLAMA_USAGE_COOKIE. Copy the __Secure-session cookie from ollama.com/settings.",
+ };
+ }
+ if (config.state === "invalid") return { message: config.error };
+
+ try {
+ const result = await fetchOllamaCloudUsageFromSettings(config);
+ if (!result.usage) return { message: result.message || "Ollama Cloud quota data unavailable." };
+ const quotas: Record = {};
+ for (const key of ["session", "weekly"] as const) {
+ const quota = result.usage[key];
+ if (!quota) continue;
+ const pct = toPercentage(quota.usagePercent);
+ quotas[key] = {
+ used: pct,
+ total: 100,
+ remaining: Math.max(0, 100 - pct),
+ remainingPercentage: Math.max(0, 100 - pct),
+ resetAt: quota.resetAt,
+ unlimited: false,
+ displayName: key === "session" ? "Session" : "Weekly",
+ };
+ }
+ return {
+ plan: result.usage.planTier ? `Ollama Cloud ${result.usage.planTier}` : "Ollama Cloud",
+ quotas,
+ };
+ } catch (error) {
+ return { message: `Ollama Cloud quota error: ${sanitizeErrorMessage(error)}` };
+ }
+}
diff --git a/open-sse/services/providerCooldownTracker.ts b/open-sse/services/providerCooldownTracker.ts
index afb5a9303f..f5a14d2006 100644
--- a/open-sse/services/providerCooldownTracker.ts
+++ b/open-sse/services/providerCooldownTracker.ts
@@ -17,13 +17,16 @@ interface CooldownEntry {
lastFailureAt: number;
/** Number of consecutive failures (resets on success) */
failureCount: number;
+ /** How long this entry must be retained for cleanup purposes */
+ retentionMs: number;
}
// Global cooldown state: keyed by "provider:connectionId" or "provider"
const cooldownMap = new Map();
-// Evict entries older than this to prevent unbounded memory growth
-const MAX_ENTRY_AGE_MS = 30 * 60 * 1000; // 30 minutes
+// Evict entries older than their configured retention horizon to prevent
+// unbounded memory growth without shortening operator-configured cooldowns.
+const DEFAULT_ENTRY_RETENTION_MS = 30 * 60 * 1000; // 30 minutes
const CLEANUP_INTERVAL_MS = 60 * 1000; // Cleanup every 60s
let cleanupTimer: ReturnType | null = null;
@@ -31,17 +34,32 @@ let cleanupTimer: ReturnType | null = null;
function startCleanupIfNeeded(): void {
if (cleanupTimer) return;
cleanupTimer = setInterval(() => {
- const now = Date.now();
- for (const [key, entry] of cooldownMap) {
- if (now - entry.lastFailureAt > MAX_ENTRY_AGE_MS) {
- cooldownMap.delete(key);
- }
- }
+ cleanupExpiredCooldownEntries();
}, CLEANUP_INTERVAL_MS);
// Allow Node.js to exit even if the timer is running
if (cleanupTimer.unref) cleanupTimer.unref();
}
+function getEntryRetentionMs(settings?: ResilienceSettings): number {
+ const maxRetryCooldownMs =
+ settings?.providerCooldown?.maxRetryCooldownMs ??
+ DEFAULT_RESILIENCE_SETTINGS.providerCooldown.maxRetryCooldownMs;
+ return Math.max(DEFAULT_ENTRY_RETENTION_MS, maxRetryCooldownMs);
+}
+
+/**
+ * Remove expired cooldown entries using each entry's configured retention
+ * horizon. Exported for diagnostics and focused tests; normal runtime cleanup is
+ * still performed by the unref'd interval started on first cooldown record.
+ */
+export function cleanupExpiredCooldownEntries(now = Date.now()): void {
+ for (const [key, entry] of cooldownMap) {
+ if (now - entry.lastFailureAt > entry.retentionMs) {
+ cooldownMap.delete(key);
+ }
+ }
+}
+
/**
* Build a cooldown key from provider and optional connectionId.
*/
@@ -66,12 +84,14 @@ export function recordProviderCooldown(
const key = cooldownKey(provider, connectionId);
const existing = cooldownMap.get(key);
const now = Date.now();
+ const retentionMs = getEntryRetentionMs(settings);
if (existing) {
existing.lastFailureAt = now;
existing.failureCount++;
+ existing.retentionMs = Math.max(existing.retentionMs, retentionMs);
} else {
- cooldownMap.set(key, { lastFailureAt: now, failureCount: 1 });
+ cooldownMap.set(key, { lastFailureAt: now, failureCount: 1, retentionMs });
}
startCleanupIfNeeded();
diff --git a/open-sse/services/reasoningTokenBuffer.ts b/open-sse/services/reasoningTokenBuffer.ts
index 23290de713..65d021e7f5 100644
--- a/open-sse/services/reasoningTokenBuffer.ts
+++ b/open-sse/services/reasoningTokenBuffer.ts
@@ -1,7 +1,4 @@
import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts";
-import { MODEL_SPECS } from "../../src/shared/constants/modelSpecs.ts";
-
-const DEFAULT_MAX_OUTPUT_TOKENS = MODEL_SPECS.__default__.maxOutputTokens;
export function toPositiveInteger(value: unknown): number | null {
const numericValue =
@@ -29,7 +26,7 @@ export function resolveReasoningBufferedMaxTokens(
if (capabilities.supportsThinking !== true) return null;
const maxOutputTokens = toPositiveInteger(capabilities.maxOutputTokens);
- if (maxOutputTokens === null || maxOutputTokens === DEFAULT_MAX_OUTPUT_TOKENS) return null;
+ if (maxOutputTokens === null) return null;
if (current > maxOutputTokens) return maxOutputTokens;
if (current === maxOutputTokens) return current;
diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts
index 6db2f04ea3..edd14b8a09 100644
--- a/open-sse/services/usage.ts
+++ b/open-sse/services/usage.ts
@@ -20,6 +20,7 @@ import { getDbInstance } from "@/lib/db/core";
import { fetchBailianQuota, type BailianTripleWindowQuota } from "./bailianQuotaFetcher.ts";
import { fetchDeepseekQuota, type DeepseekQuota } from "./deepseekQuotaFetcher.ts";
import { fetchOpencodeQuota, type OpencodeTripleWindowQuota } from "./opencodeQuotaFetcher.ts";
+import { getOllamaCloudUsage, getOpenCodeGoUsage } from "./opencodeOllamaUsage.ts";
import {
applyAntigravityClientProfileHeaders,
getAntigravityBootstrapHeaders,
@@ -91,19 +92,6 @@ const NANOGPT_CONFIG = {
usageUrl: "https://nano-gpt.com/api/subscription/v1/usage",
};
-const OPENCODE_GO_QUOTA_URL =
- // Note: api.z.ai rejects opencode-go keys with {"code":401}. This default is a
- // known broken placeholder (see issues #10448, #16017). The env-var override lets
- // operators point at a working endpoint once OpenCode ships one.
- process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL ?? "https://api.z.ai/api/monitor/usage/quota/limit";
-const OPENCODE_GO_QUOTA_TOTALS = {
- session: 12,
- weekly: 30,
- mcp_monthly: 60,
-} as const;
-const OPENCODE_GO_QUOTA_ORDER = ["session", "weekly", "mcp_monthly"] as const;
-type OpenCodeGoQuotaName = (typeof OPENCODE_GO_QUOTA_ORDER)[number];
-
// Cursor dashboard usage API config
// The endpoint that powers https://cursor.com/dashboard/spending. Validates the WorkOS
// session via the WorkosCursorSessionToken cookie (format: `${userId}::${jwt}`) and
@@ -214,77 +202,6 @@ function getGlmQuotaDisplayName(quotaName: string): string {
if (quotaName === "weekly") return "Weekly Quota";
return quotaName;
}
-
-function getOpenCodeGoTokenQuotaName(
- limit: JsonRecord,
- existingQuotas: Record
-): "session" | "weekly" {
- const unit = toNumber(limit.unit, 0);
- const number = toNumber(limit.number, 0);
-
- if (unit === 3 && number === 5) return "session";
- if (unit === 6 && number === 1) return "weekly";
- if ((unit === 4 && number === 7) || (unit === 3 && number >= 24 * 7)) return "weekly";
-
- return existingQuotas.session ? "weekly" : "session";
-}
-
-function getOpenCodeGoQuotaDisplayName(quotaName: OpenCodeGoQuotaName): string {
- if (quotaName === "session") return "5-hour rolling";
- if (quotaName === "weekly") return "Weekly";
- return "Monthly";
-}
-
-function normalizeOpenCodeGoQuotaToken(apiKey: string): string {
- return apiKey.trim().replace(/^Bearer\s+/i, "");
-}
-
-function buildOpenCodeGoDollarQuota(
- quotaName: OpenCodeGoQuotaName,
- percentage: unknown,
- resetAt: string | null,
- usedOverride?: unknown,
- details?: UsageQuota["details"]
-): UsageQuota {
- const total = OPENCODE_GO_QUOTA_TOTALS[quotaName];
- const percentUsed = toPercentage(percentage);
- const rawUsed = toNumber(usedOverride, Number.NaN);
- const used = roundCurrency(
- Number.isFinite(rawUsed) ? Math.max(0, Math.min(total, rawUsed)) : (total * percentUsed) / 100
- );
- const remaining = roundCurrency(Math.max(0, total - used));
- const remainingPercentage =
- total > 0
- ? clampPercentage(Math.round((remaining / total) * 100))
- : clampPercentage(100 - percentUsed);
-
- return {
- used,
- total,
- remaining,
- remainingPercentage,
- resetAt,
- unlimited: false,
- displayName: getOpenCodeGoQuotaDisplayName(quotaName),
- currency: "USD",
- details,
- };
-}
-
-function orderOpenCodeGoQuotas(quotas: Record): Record {
- const ordered: Record = {};
-
- for (const key of OPENCODE_GO_QUOTA_ORDER) {
- if (quotas[key]) ordered[key] = quotas[key];
- }
-
- for (const [key, quota] of Object.entries(quotas)) {
- if (!ordered[key]) ordered[key] = quota;
- }
-
- return ordered;
-}
-
function getFieldValue(source: unknown, snakeKey: string, camelKey: string): unknown {
const obj = toRecord(source);
return obj[snakeKey] ?? obj[camelKey] ?? null;
@@ -966,121 +883,6 @@ async function getGlmUsage(apiKey: string, providerSpecificData?: Record).code, 200);
- if (code === 401 || code === 403 || (json as Record).success === false) {
- return {
- message:
- "OpenCode Go does not expose a public quota API. Chat requests still work. " +
- "Set OMNIROUTE_OPENCODE_GO_QUOTA_URL to a working endpoint, or follow " +
- "https://github.com/anomalyco/opencode/issues/16017 for upstream status.",
- };
- }
-
- const data = toRecord((json as Record).data);
- const limits: unknown[] = Array.isArray(data.limits) ? data.limits : [];
- const quotas: Record = {};
-
- for (const limit of limits) {
- const src = toRecord(limit);
- const type = String(src.type || "").toUpperCase();
- const resetAt = parseResetTime(src.nextResetTime);
-
- if (type === "TOKENS_LIMIT" || type === "TOKEN_LIMIT") {
- const quotaName = getOpenCodeGoTokenQuotaName(src, quotas);
-
- quotas[quotaName] = buildOpenCodeGoDollarQuota(
- quotaName,
- src.percentage,
- resetAt,
- undefined,
- Array.isArray(src.models)
- ? (src.models as unknown[]).map((model) => {
- const modelInfo = toRecord(model);
- return {
- name: String(modelInfo.model || modelInfo.modelCode || "usage"),
- used: toNumber(modelInfo.percentage, 0),
- };
- })
- : undefined
- );
- continue;
- }
-
- if (type === "TIME_LIMIT" || type === "TIME_USAGE_LIMIT") {
- quotas.mcp_monthly = buildOpenCodeGoDollarQuota(
- "mcp_monthly",
- src.percentage,
- resetAt,
- src.currentValue,
- Array.isArray(src.usageDetails)
- ? src.usageDetails.map((item) => {
- const detail = toRecord(item);
- return {
- name: String(detail.modelCode || detail.name || "usage"),
- used: toNumber(detail.usage, 0),
- };
- })
- : undefined
- );
- }
- }
-
- const levelRaw =
- typeof data.planName === "string"
- ? data.planName
- : typeof data.level === "string"
- ? data.level
- : "";
- const planLabel = toTitleCase(levelRaw.replace(/\s*plan$/i, ""));
- const plan = planLabel
- ? /^opencode\s+go\b/i.test(planLabel)
- ? planLabel
- : `OpenCode Go ${planLabel}`
- : null;
-
- return { plan, quotas: orderOpenCodeGoQuotas(quotas) };
-}
-
/**
* Bailian (Alibaba Coding Plan) Usage
* Fetches triple-window quota (5h, weekly, monthly) and returns worst-case.
@@ -1589,7 +1391,9 @@ export async function getUsageForProvider(
...(provider === "glm-cn" ? { apiRegion: "china" } : {}),
});
case "opencode-go":
- return await getOpenCodeGoUsage(apiKey || "");
+ return await getOpenCodeGoUsage(apiKey || "", providerSpecificData);
+ case "ollama-cloud":
+ return await getOllamaCloudUsage(providerSpecificData);
case "minimax":
case "minimax-cn":
return await getMiniMaxUsage(apiKey || "", provider);
@@ -3118,7 +2922,10 @@ async function getKiroUsage(accessToken?: string, providerSpecificData?: JsonRec
// "auth expired, chat may still work" message instead of a generic upstream-error blob
// so the quota card matches what users with legacy social-auth accounts already see.
// Inspired by https://github.com/decolua/9router/pull/620.
- if ((response.status === 401 || response.status === 403) && isSocialAuthKiroAccount(providerSpecificData)) {
+ if (
+ (response.status === 401 || response.status === 403) &&
+ isSocialAuthKiroAccount(providerSpecificData)
+ ) {
return {
message: "Kiro quota API authentication expired. Chat may still work.",
quotas: {},
diff --git a/open-sse/translator/deepseekWebTools.ts b/open-sse/translator/deepseekWebTools.ts
new file mode 100644
index 0000000000..34723839f2
--- /dev/null
+++ b/open-sse/translator/deepseekWebTools.ts
@@ -0,0 +1,441 @@
+// DeepSeek-web-specific tool-call translation.
+//
+// chat.deepseek.com has no native function calling, so OmniRoute serializes the OpenAI
+// `tools[]` into a prompt contract and parses the model's text reply back into OpenAI
+// `tool_calls`. The canonical `webTools.ts` parser handles the well-behaved
+// `{json}` / bare-JSON shapes used by most web-cookie providers, and it MUST
+// stay untouched (it works for the others).
+//
+// DeepSeek, however, emits a much wider zoo of ad-hoc shapes:
+// {json} name in the tag suffix, body is the arguments
+// {id,type,params} alternate key names (type → name, params → arguments)
+// {json} name in an attribute
+// {json} tool name in the id attribute
+// {json} doubled / nested wrappers
+// x{json} XML children
+// parameter style
+//
+// A single regex cannot robustly cover all of these (nesting + attributes + XML children),
+// so this parser tokenizes the tool tags and walks them with a stack instead. It reuses the
+// proven JSON-normalization / fuzzy-name-matching / range-stripping helpers from webTools.ts
+// rather than duplicating them.
+
+import {
+ parseToolCallsFromText,
+ parseLooseJsonObject,
+ getRequestedToolNames,
+ resolveRequestedToolName,
+ toArgumentsString,
+ stripRanges,
+ type OpenAIToolCall,
+ type RequestedToolName,
+} from "./webTools.ts";
+
+interface OpenAIToolDef {
+ type?: string;
+ function?: { name?: string; description?: string; parameters?: unknown };
+}
+
+// ── Stricter, compact tool-use prompt ───────────────────────────────────────
+
+/**
+ * Serialize an OpenAI `tools` array into a DeepSeek-specific system-prompt block.
+ *
+ * It is deliberately stricter than the generic `serializeToolsToPrompt`: DeepSeek tends to
+ * (a) invent its own wrappers and (b) merely *describe* a plan instead of emitting a call.
+ * The wording forces the single canonical `{json}` shape and forbids the
+ * alternatives, while staying short to avoid wasting tokens.
+ */
+export function serializeDeepSeekToolPrompt(tools: unknown): string {
+ if (!Array.isArray(tools) || tools.length === 0) return "";
+
+ const lines: string[] = [];
+ for (const t of tools as OpenAIToolDef[]) {
+ const fn = t?.function;
+ if (!fn?.name) continue;
+ const desc = typeof fn.description === "string" && fn.description ? fn.description : "";
+ let params = "";
+ try {
+ params = fn.parameters ? JSON.stringify(fn.parameters) : "";
+ } catch {
+ params = "";
+ }
+ lines.push(
+ `- ${fn.name}${desc ? `: ${desc}` : ""}${params ? `\n parameters: ${params}` : ""}`
+ );
+ }
+ if (lines.length === 0) return "";
+
+ return [
+ "You can call tools. To call a tool, output ONLY this exact block (no markdown fence):",
+ '{"name": "", "arguments": { ... }}',
+ "Rules:",
+ "- Use exactly .... Do NOT use , , , , id=/name= attributes, or code fences.",
+ '- "name" must be one of the tools below; "arguments" must be a JSON object.',
+ "- When a tool is needed, emit the block instead of only describing the plan.",
+ "- Emit one block per call; you may put several blocks back to back.",
+ "- If no tool is needed, just answer normally without any block.",
+ "",
+ "Available tools:",
+ ...lines,
+ ].join("\n");
+}
+
+// ── Tool-aware conversation prompt ───────────────────────────────────────────
+
+interface ChatMessage {
+ role: string;
+ content?: unknown;
+ tool_calls?: Array<{ id?: string; function?: { name?: string; arguments?: unknown } }>;
+ tool_call_id?: string;
+ name?: string;
+}
+
+function extractText(content: unknown): string {
+ if (Array.isArray(content)) {
+ return (content as Array<{ type?: string; text?: string }>)
+ .filter((item) => item?.type === "text")
+ .map((item) => item?.text ?? "")
+ .join("\n");
+ }
+ return content == null ? "" : String(content);
+}
+
+/**
+ * Build the single `prompt` string for an agentic (tool-using) DeepSeek-web turn.
+ *
+ * The web endpoint takes a flat prompt with no `messages[]`, so the legacy `messagesToPrompt`
+ * only forwarded the last user message — which makes an agent loop amnesiac: on every turn the
+ * follow-up messages carry no new *user* text, so DeepSeek only ever saw the original task and
+ * kept restarting (re-creating todos, re-listing files…). This builder instead replays the WHOLE
+ * trajectory — including the assistant's prior `` calls and each `role:"tool"` result — so
+ * the model continues from where it left off instead of starting over.
+ */
+export function buildToolConversationPrompt(
+ messages: ChatMessage[],
+ toolSystemPrompt: string
+): string {
+ const systemParts: string[] = [];
+ if (toolSystemPrompt) systemParts.push(toolSystemPrompt);
+
+ const lines: string[] = [];
+ const callNameById = new Map();
+ let sawToolActivity = false;
+
+ for (const m of messages) {
+ if (m.role === "system") {
+ const t = extractText(m.content).trim();
+ if (t) systemParts.push(t);
+ } else if (m.role === "user") {
+ const t = extractText(m.content).trim();
+ if (t) lines.push(`User: ${t}`);
+ } else if (m.role === "assistant") {
+ const t = extractText(m.content).trim();
+ const calls = Array.isArray(m.tool_calls) ? m.tool_calls : [];
+ const parts: string[] = [];
+ if (t) parts.push(t);
+ for (const c of calls) {
+ const name = typeof c?.function?.name === "string" ? c.function.name : "";
+ const rawArgs = c?.function?.arguments;
+ const args =
+ typeof rawArgs === "string" && rawArgs ? rawArgs : JSON.stringify(rawArgs ?? {});
+ if (c?.id) callNameById.set(c.id, name);
+ parts.push(`{"name": ${JSON.stringify(name)}, "arguments": ${args}}`);
+ sawToolActivity = true;
+ }
+ if (parts.length) lines.push(`Assistant: ${parts.join("\n")}`);
+ } else if (m.role === "tool") {
+ const t = extractText(m.content).trim();
+ const name = (m.tool_call_id && callNameById.get(m.tool_call_id)) || m.name || "tool";
+ lines.push(`Tool result (${name}): ${t || "(no output)"}`);
+ sawToolActivity = true;
+ }
+ }
+
+ const parts: string[] = [];
+ if (systemParts.length) parts.push(systemParts.join("\n\n"));
+ if (lines.length) parts.push(lines.join("\n\n"));
+ if (sawToolActivity) {
+ // Anchor the model to the work already done so it advances instead of repeating it.
+ parts.push(
+ "Continue the task using the tool results above. Do NOT repeat tool calls that already " +
+ "succeeded; perform the next step or give the final answer."
+ );
+ }
+
+ return parts.join("\n\n").replace(/!\[.*?\]\(.*?\)/g, "");
+}
+
+// ── Tag tokenizer ────────────────────────────────────────────────────────────
+
+interface TagToken {
+ start: number;
+ end: number;
+ closing: boolean;
+ suffix: string; // tool name after ':' in the tag (e.g. `` → "bash")
+ attrs: string; // raw attribute text inside the tag
+}
+
+// Matches an opening/closing or tag, optionally with a `:name`
+// suffix and an attribute list. `tool_call` is listed first so it wins the alternation.
+const TAG_TOKEN_RE = /<(\/?)(?:tool_call|tool)(:[A-Za-z0-9_.+-]+)?((?:\s[^>]*)?)\/?>/g;
+
+function tokenizeToolTags(text: string): TagToken[] {
+ const tokens: TagToken[] = [];
+ let m: RegExpExecArray | null;
+ TAG_TOKEN_RE.lastIndex = 0;
+ while ((m = TAG_TOKEN_RE.exec(text)) !== null) {
+ tokens.push({
+ start: m.index,
+ end: TAG_TOKEN_RE.lastIndex,
+ closing: m[1] === "/",
+ suffix: m[2] ? m[2].slice(1) : "",
+ attrs: m[3] || "",
+ });
+ }
+ return tokens;
+}
+
+interface ToolBlock {
+ open: TagToken;
+ close: TagToken;
+ innerStart: number;
+ innerEnd: number;
+}
+
+// Pair tags with a stack: every closing tag pairs with the nearest unmatched open. An open
+// left unmatched at the end (e.g. the stray outer `` of a doubled wrapper, or a
+// never-closed `` followed by ``) gets a synthetic close at the end
+// of the text so its body is still parsed; the doubled-wrapper outer is then dropped by the
+// leaf filter.
+function pairToolBlocks(tokens: TagToken[], textLen: number): ToolBlock[] {
+ const blocks: ToolBlock[] = [];
+ const stack: TagToken[] = [];
+ for (const tok of tokens) {
+ if (!tok.closing) {
+ stack.push(tok);
+ continue;
+ }
+ const open = stack.pop();
+ if (!open) continue;
+ blocks.push({ open, close: tok, innerStart: open.end, innerEnd: tok.start });
+ }
+ for (const open of stack) {
+ const synthetic: TagToken = {
+ start: textLen,
+ end: textLen,
+ closing: true,
+ suffix: "",
+ attrs: "",
+ };
+ blocks.push({ open, close: synthetic, innerStart: open.end, innerEnd: textLen });
+ }
+ return blocks;
+}
+
+// ── Attribute / XML-child helpers ────────────────────────────────────────────
+
+/** Read an attribute value, tolerating backslash-escaped quotes inside the value. */
+function getAttr(attrs: string, name: string): string | null {
+ const re = new RegExp(`\\b${name}\\s*=\\s*("|')`);
+ const m = re.exec(attrs);
+ if (!m) return null;
+ const quote = m[1];
+ let j = m.index + m[0].length;
+ let out = "";
+ while (j < attrs.length) {
+ const ch = attrs[j];
+ if (ch === "\\") {
+ out += attrs[j + 1] ?? "";
+ j += 2;
+ continue;
+ }
+ if (ch === quote) break;
+ out += ch;
+ j += 1;
+ }
+ return out;
+}
+
+function getXmlChild(inner: string, tag: string): string | null {
+ const m = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i").exec(inner);
+ return m ? m[1].trim() : null;
+}
+
+// The body group is a tempered greedy token: `(?:(?!` (no closing tag) cannot let the body matcher swallow a
+// following `...` and drop that parameter.
+const PARAM_TAG_RE = /]*?)\/?>(?:((?:(?!)?/gi;
+
+/** Collect `` / `y` into an object. */
+function buildArgsFromParameters(inner: string): Record | null {
+ const out: Record = {};
+ let found = false;
+ let m: RegExpExecArray | null;
+ PARAM_TAG_RE.lastIndex = 0;
+ while ((m = PARAM_TAG_RE.exec(inner)) !== null) {
+ const attrs = m[1] || "";
+ const body = m[2];
+ const name = getAttr(attrs, "name");
+ if (!name) continue;
+ const value = getAttr(attrs, "content") ?? (typeof body === "string" ? body.trim() : "");
+ out[name] = value;
+ found = true;
+ }
+ return found ? out : null;
+}
+
+// ── Single-block extraction ──────────────────────────────────────────────────
+
+interface ExtractedCall {
+ name: string;
+ arguments: string;
+}
+
+function asString(value: unknown): string | null {
+ return typeof value === "string" && value.length > 0 ? value : null;
+}
+
+/**
+ * Turn one tool block (tag name + inner text) into a name + JSON-string arguments.
+ * Returns null when no plausible tool name can be recovered.
+ */
+function extractCall(
+ tagName: string,
+ innerRaw: string,
+ requested: RequestedToolName[]
+): ExtractedCall | null {
+ const inner = innerRaw.trim();
+
+ const nameChild = getXmlChild(inner, "name");
+ const argsChild = getXmlChild(inner, "arguments") ?? getXmlChild(inner, "parameters");
+ const paramObj = argsChild ? null : buildArgsFromParameters(inner);
+ const hasXmlChildren = !!nameChild || !!argsChild || !!paramObj;
+
+ const json = hasXmlChildren ? null : parseLooseJsonObject(inner);
+ const jsonName = json ? (asString(json.name) ?? asString(json.type)) : null;
+
+ const childResolved = nameChild ? resolveRequestedToolName(nameChild, requested) : null;
+ const jsonResolved = jsonName ? resolveRequestedToolName(jsonName, requested) : null;
+ const tagResolved = tagName ? resolveRequestedToolName(tagName, requested) : null;
+
+ // Prefer a name that maps to a requested tool. The JSON body wins over the tag attribute
+ // because DeepSeek sometimes emits a bogus tag name (e.g. name="skill", #3260).
+ let name: string | null = null;
+ let nameFromTag = false;
+ const pick = (val: string | null, fromTag: boolean) => {
+ if (!name && val) {
+ name = val;
+ nameFromTag = fromTag;
+ }
+ };
+ pick(childResolved, false);
+ pick(jsonResolved, false);
+ pick(tagResolved, true);
+ pick(nameChild, false);
+ pick(jsonName, false);
+ pick(tagName, true);
+
+ // Shell-style `{ "command": "..." }` with no tag name: treat command as the tool name only
+ // if it actually resolves to a requested tool (the value is otherwise the command itself).
+ if (!name && !tagName && json) {
+ const command = asString(json.command);
+ const resolved = command ? resolveRequestedToolName(command, requested) : null;
+ if (resolved) {
+ name = resolved;
+ nameFromTag = false;
+ }
+ }
+ if (!name) return null;
+
+ let argsValue: unknown;
+ if (argsChild) {
+ argsValue = parseLooseJsonObject(argsChild) ?? argsChild;
+ } else if (paramObj) {
+ argsValue = paramObj;
+ } else if (json) {
+ if (json.arguments !== undefined) argsValue = json.arguments;
+ else if (json.params !== undefined) argsValue = json.params;
+ else if (nameFromTag) {
+ // `{"command": ...}` — the whole JSON object is the arguments payload.
+ argsValue = json;
+ } else {
+ // Name came from the JSON body — the remaining keys are the arguments.
+ const { name: _n, type: _t, id: _i, command: _c, arguments: _a, params: _p, ...rest } = json;
+ argsValue = rest;
+ }
+ } else {
+ argsValue = {};
+ }
+
+ return { name, arguments: toArgumentsString(argsValue) };
+}
+
+// ── Public parser ─────────────────────────────────────────────────────────────
+
+/**
+ * Parse a DeepSeek-web text reply into OpenAI `tool_calls`. Returns the surrounding text with the recognized blocks stripped (so it can
+ * still be streamed to the client) plus the parsed calls, or `null` when none are present.
+ *
+ * Falls back to the canonical `webTools.parseToolCallsFromText` for tag-free replies so that
+ * bare-JSON and plain `` behavior stays identical to the shared implementation.
+ */
+export function parseDeepSeekToolCalls(
+ text: string,
+ idSeed = "call",
+ requestedTools?: unknown
+): { content: string; toolCalls: OpenAIToolCall[] | null } {
+ if (typeof text !== "string" || text.length === 0) {
+ return { content: text ?? "", toolCalls: null };
+ }
+
+ const tokens = tokenizeToolTags(text);
+ if (tokens.length === 0) {
+ // No DeepSeek-specific tags — defer to the proven canonical parser (bare JSON, etc.).
+ return parseToolCallsFromText(text, idSeed, requestedTools);
+ }
+
+ const requested = getRequestedToolNames(requestedTools);
+ const blocks = pairToolBlocks(tokens, text.length);
+
+ // Only extract from leaf blocks (no other block nested inside), so a doubled
+ // `...` wrapper yields a single call from the inner block.
+ const isLeaf = (b: ToolBlock) =>
+ !blocks.some((o) => o !== b && o.open.start >= b.innerStart && o.close.end <= b.innerEnd);
+
+ const toolCalls: OpenAIToolCall[] = [];
+ const acceptedRanges: Array<{ start: number; end: number }> = [];
+
+ for (const block of blocks.filter(isLeaf).sort((a, b) => a.open.start - b.open.start)) {
+ const tagName =
+ block.open.suffix ||
+ getAttr(block.open.attrs, "name") ||
+ getAttr(block.open.attrs, "id") ||
+ "";
+ const inner = text.slice(block.innerStart, block.innerEnd);
+ const call = extractCall(tagName, inner, requested);
+ if (!call) continue;
+ toolCalls.push({
+ id: `${idSeed}_${toolCalls.length}`,
+ type: "function",
+ function: { name: call.name, arguments: call.arguments },
+ });
+ acceptedRanges.push({ start: block.open.start, end: block.close.end });
+ }
+
+ if (toolCalls.length === 0) {
+ // Tags were present but none parsed (e.g. malformed) — try the canonical bare-JSON path.
+ return parseToolCallsFromText(text, idSeed, requestedTools);
+ }
+
+ // Strip the accepted blocks plus any stray tool tags left outside them (the unmatched outer
+ // `` of a doubled wrapper, leftover `` of a non-leaf wrapper, etc.).
+ const within = (tok: TagToken) =>
+ acceptedRanges.some((r) => tok.start >= r.start && tok.end <= r.end);
+ const ranges = [
+ ...acceptedRanges,
+ ...tokens.filter((t) => !within(t)).map((t) => ({ start: t.start, end: t.end })),
+ ];
+
+ return { content: stripRanges(text, ranges), toolCalls };
+}
diff --git a/open-sse/translator/request/claude-to-gemini.ts b/open-sse/translator/request/claude-to-gemini.ts
index 92b825f46e..fadc428731 100644
--- a/open-sse/translator/request/claude-to-gemini.ts
+++ b/open-sse/translator/request/claude-to-gemini.ts
@@ -55,7 +55,10 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
result.generationConfig.topK = body.top_k;
}
if (body.max_tokens !== undefined) {
- result.generationConfig.maxOutputTokens = capMaxOutputTokens(model, body.max_tokens);
+ const maxOutputTokens = capMaxOutputTokens(model, body.max_tokens);
+ if (maxOutputTokens !== null) {
+ result.generationConfig.maxOutputTokens = maxOutputTokens;
+ }
}
// ── System instruction ─────────────────────────────────────────
diff --git a/open-sse/translator/request/openai-to-claude.ts b/open-sse/translator/request/openai-to-claude.ts
index 4ec6236f75..55a1f28433 100644
--- a/open-sse/translator/request/openai-to-claude.ts
+++ b/open-sse/translator/request/openai-to-claude.ts
@@ -41,14 +41,13 @@ function applyCopilotSummarizedThinkingDisplay(
// - max_tokens must be <= model output cap (e.g. 128000 for Opus 4.7)
const MIN_CLAUDE_THINKING_BUDGET = 1024;
const MIN_RESPONSE_ROOM = 1024;
-const FALLBACK_OUTPUT_CAP = 128000;
-function safeCapMaxOutputTokens(model: string): number {
+function safeCapMaxOutputTokens(model: string): number | null {
try {
const cap = capMaxOutputTokens(model);
- return typeof cap === "number" && cap > 0 ? cap : FALLBACK_OUTPUT_CAP;
+ return typeof cap === "number" && cap > 0 ? cap : null;
} catch {
- return FALLBACK_OUTPUT_CAP;
+ return null;
}
}
@@ -86,26 +85,35 @@ export function fitThinkingToMaxTokens(
// No budgeted thinking — just cap max_tokens to the model output ceiling.
if (!thinking || requestedBudget <= 0) {
return {
- maxTokens: Math.min(Math.max(callerMaxTokens, 1), modelCap),
+ maxTokens:
+ modelCap === null
+ ? Math.max(callerMaxTokens, 1)
+ : Math.min(Math.max(callerMaxTokens, 1), modelCap),
thinking,
};
}
let responseRoom = Math.max(callerMaxTokens, MIN_RESPONSE_ROOM);
- let target = Math.min(responseRoom + requestedBudget, modelCap);
+ let target =
+ modelCap === null
+ ? responseRoom + requestedBudget
+ : Math.min(responseRoom + requestedBudget, modelCap);
let fittedBudget = target - responseRoom;
// If the cap squeezed thinking below Anthropic's floor, try shrinking
// response room to MIN_RESPONSE_ROOM to recover budget.
if (fittedBudget < MIN_CLAUDE_THINKING_BUDGET && responseRoom > MIN_RESPONSE_ROOM) {
responseRoom = MIN_RESPONSE_ROOM;
- target = Math.min(responseRoom + requestedBudget, modelCap);
+ target =
+ modelCap === null
+ ? responseRoom + requestedBudget
+ : Math.min(responseRoom + requestedBudget, modelCap);
fittedBudget = target - responseRoom;
}
// Cap too tight for any thinking — disable rather than send an invalid request.
if (fittedBudget < MIN_CLAUDE_THINKING_BUDGET) {
- return { maxTokens: modelCap, thinking: undefined };
+ return { maxTokens: modelCap ?? Math.max(callerMaxTokens, 1), thinking: undefined };
}
const adjustedThinking: Record = { ...thinking };
diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts
index 39e2016da6..bc9e6651e6 100644
--- a/open-sse/translator/request/openai-to-gemini.ts
+++ b/open-sse/translator/request/openai-to-gemini.ts
@@ -32,9 +32,8 @@ import {
import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiToolsSanitizer.ts";
// Observed Antigravity wrapper output cap, not an underlying model capability.
-// Keep this bridge-local: capMaxOutputTokens() falls back to OmniRoute's generic
-// 8192 default for unknown Claude-family IDs, while Antigravity currently caps
-// visible output around 16K. See: https://github.com/keisksw/antigravity-output-analysis
+// Keep this bridge-local: Antigravity currently caps visible output around 16K.
+// See: https://github.com/keisksw/antigravity-output-analysis
const ANTIGRAVITY_CLAUDE_MAX_OUTPUT_TOKENS = 16_384;
type GeminiPart = Record;
@@ -274,13 +273,12 @@ function openaiToGeminiBase(
if (body.stop !== undefined) {
result.generationConfig.stopSequences = Array.isArray(body.stop) ? body.stop : [body.stop];
}
- const requestedMaxOutputTokens = (body.max_tokens ?? body.max_completion_tokens) as
- | number
- | undefined;
- if (requestedMaxOutputTokens !== undefined) {
- result.generationConfig.maxOutputTokens = capMaxOutputTokens(model, requestedMaxOutputTokens);
- } else {
- result.generationConfig.maxOutputTokens = capMaxOutputTokens(model);
+ const maxOutputTokens = capMaxOutputTokens(
+ model,
+ (body.max_tokens ?? body.max_completion_tokens) as number | undefined
+ );
+ if (maxOutputTokens !== null) {
+ result.generationConfig.maxOutputTokens = maxOutputTokens;
}
// Thinking / Reasoning support (Google Gemini 2.0+ Thinking models)
diff --git a/open-sse/translator/webTools.ts b/open-sse/translator/webTools.ts
index 28a94219d9..a3fc8f1766 100644
--- a/open-sse/translator/webTools.ts
+++ b/open-sse/translator/webTools.ts
@@ -34,7 +34,7 @@ interface ToolParseCandidate {
requireRequestedTool: boolean;
}
-interface RequestedToolName {
+export interface RequestedToolName {
original: string;
normalized: string;
}
@@ -45,7 +45,7 @@ function toRecord(value: unknown): Record | null {
: null;
}
-function getRequestedToolNames(tools: unknown): RequestedToolName[] {
+export function getRequestedToolNames(tools: unknown): RequestedToolName[] {
if (!Array.isArray(tools)) return [];
const names: RequestedToolName[] = [];
const seen = new Set();
@@ -102,7 +102,10 @@ function scoreToolName(emitted: string, requested: RequestedToolName): number {
return similarity >= 0.72 ? similarity : 0;
}
-function resolveRequestedToolName(emitted: string, requestedTools: RequestedToolName[]): string | null {
+export function resolveRequestedToolName(
+ emitted: string,
+ requestedTools: RequestedToolName[]
+): string | null {
if (requestedTools.length === 0) return emitted;
let best: { name: string; score: number } | null = null;
@@ -227,7 +230,7 @@ function normalizeLooseJson(value: string): string {
.replace(/,\s*([}\]])/g, "$1");
}
-function parseLooseJsonObject(raw: string): Record | null {
+export function parseLooseJsonObject(raw: string): Record | null {
const trimmed = stripCodeFence(raw);
for (const candidate of [trimmed, normalizeLooseJson(trimmed)]) {
try {
@@ -282,7 +285,10 @@ function findBareJsonCandidates(text: string): ToolParseCandidate[] {
depth -= 1;
if (depth === 0 && start >= 0) {
const raw = text.slice(start, i + 1);
- if (/[{,]\s*["']?(name|command)["']?\s*:/i.test(raw) && /[{,]\s*["']?arguments["']?\s*:/i.test(raw)) {
+ if (
+ /[{,]\s*["']?(name|command)["']?\s*:/i.test(raw) &&
+ /[{,]\s*["']?arguments["']?\s*:/i.test(raw)
+ ) {
candidates.push({ raw, start, end: i + 1, requireRequestedTool: true });
}
start = -1;
@@ -293,11 +299,14 @@ function findBareJsonCandidates(text: string): ToolParseCandidate[] {
return candidates;
}
-function rangesOverlap(a: { start: number; end: number }, b: { start: number; end: number }): boolean {
+function rangesOverlap(
+ a: { start: number; end: number },
+ b: { start: number; end: number }
+): boolean {
return a.start < b.end && b.start < a.end;
}
-function stripRanges(text: string, ranges: Array<{ start: number; end: number }>): string {
+export function stripRanges(text: string, ranges: Array<{ start: number; end: number }>): string {
let content = text;
const sorted = [...ranges].sort((a, b) => b.start - a.start);
for (const range of sorted) {
@@ -308,13 +317,18 @@ function stripRanges(text: string, ranges: Array<{ start: number; end: number }>
const afterOnLine = content.slice(range.end, lineEnd);
const removeWholeLine = beforeOnLine.trim() === "" && afterOnLine.trim() === "";
const start = removeWholeLine ? lineStart : range.start;
- const end = removeWholeLine && nextLineBreak !== -1 ? nextLineBreak + 1 : removeWholeLine ? lineEnd : range.end;
+ const end =
+ removeWholeLine && nextLineBreak !== -1
+ ? nextLineBreak + 1
+ : removeWholeLine
+ ? lineEnd
+ : range.end;
content = `${content.slice(0, start)}${content.slice(end)}`;
}
return content.replace(/\n{3,}/g, "\n\n").trim();
}
-function toArgumentsString(value: unknown): string {
+export function toArgumentsString(value: unknown): string {
if (value === undefined) return "{}";
if (typeof value === "string") {
const parsed = parseLooseJsonObject(value);
@@ -346,7 +360,9 @@ export function serializeToolsToPrompt(tools: unknown): string {
} catch {
params = "";
}
- lines.push(`- ${fn.name}${desc ? `: ${desc}` : ""}${params ? `\n parameters: ${params}` : ""}`);
+ lines.push(
+ `- ${fn.name}${desc ? `: ${desc}` : ""}${params ? `\n parameters: ${params}` : ""}`
+ );
}
if (lines.length === 0) return "";
@@ -471,7 +487,7 @@ interface ToolPrepResult {
*/
export function prepareToolMessages(
bodyObj: Record,
- messages: Array<{ role: string; content: unknown }>,
+ messages: Array<{ role: string; content: unknown }>
): ToolPrepResult {
const requestedTools = bodyObj.tools;
const hasTools = Array.isArray(requestedTools) && requestedTools.length > 0;
@@ -500,9 +516,13 @@ interface ToolCompletionResult {
export function buildToolAwareResult(
rawContent: string,
requestedTools: unknown,
- idSeed = "call",
+ idSeed = "call"
): ToolCompletionResult {
- const { content, toolCalls } = parseToolCallsFromText(rawContent, `${idSeed}-${Date.now()}`, requestedTools);
+ const { content, toolCalls } = parseToolCallsFromText(
+ rawContent,
+ `${idSeed}-${Date.now()}`,
+ requestedTools
+ );
return {
content,
toolCalls,
diff --git a/open-sse/utils/noThinkingAlias.ts b/open-sse/utils/noThinkingAlias.ts
index 9acbb2b6c8..c0dfcfbb6b 100644
--- a/open-sse/utils/noThinkingAlias.ts
+++ b/open-sse/utils/noThinkingAlias.ts
@@ -6,7 +6,7 @@
* thinking-capable model into a no-thinking mode purely by *model selection*, the
* gateway exposes a synthetic catalog id:
*
- * claude-3-omniroute-no-thinking//
+ * no-think//
*
* When such an id arrives on a request we strip the prefix back to the real
* `/` and suppress reasoning (`thinking:{type:"disabled"}` for the
@@ -21,7 +21,7 @@
*/
import { getModelSpec } from "@/shared/constants/modelSpecs";
-export const NO_THINKING_PREFIX = "claude-3-omniroute-no-thinking/";
+export const NO_THINKING_PREFIX = "no-think/";
/** True when `modelId` carries the no-thinking gateway prefix. */
export function isNoThinkingAlias(modelId: unknown): modelId is string {
diff --git a/open-sse/utils/proxyDispatcher.ts b/open-sse/utils/proxyDispatcher.ts
index aab0050a66..df120d11a6 100644
--- a/open-sse/utils/proxyDispatcher.ts
+++ b/open-sse/utils/proxyDispatcher.ts
@@ -108,10 +108,42 @@ function getProxyDispatcherOptions(env: Record = pro
};
}
+export function getDefaultDispatcherConnectionLimit(
+ env: Record = process.env
+): number {
+ const raw = env.OMNIROUTE_DIRECT_DISPATCHER_CONNECTIONS;
+ if (raw == null || raw.trim() === "") return DEFAULT_PROXY_DISPATCHER_CONNECTIONS;
+
+ const parsed = Number(raw);
+ if (!Number.isFinite(parsed) || parsed < 1) {
+ console.warn(
+ `[ProxyDispatcher] Invalid OMNIROUTE_DIRECT_DISPATCHER_CONNECTIONS="${raw}". Using default ${DEFAULT_PROXY_DISPATCHER_CONNECTIONS}.`
+ );
+ return DEFAULT_PROXY_DISPATCHER_CONNECTIONS;
+ }
+
+ return Math.min(Math.floor(parsed), MAX_PROXY_DISPATCHER_CONNECTIONS);
+}
+
+function getDefaultDispatcherOptions(env: Record = process.env) {
+ const options = getDispatcherOptions();
+ // #4580 — On the direct egress path, undici's default pipelining (1) lets a long
+ // SSE stream monopolize the single pooled socket per origin, serializing every
+ // other concurrent request to that same provider. Mirror the proxy fix (#4288):
+ // disable pipelining and keep several connections available. Unlike the proxy
+ // path we KEEP keep-alive — the 1ms TTL there is a cheap-proxy-socket workaround,
+ // not needed (and harmful to perf) for direct connections.
+ return {
+ ...options,
+ connections: getDefaultDispatcherConnectionLimit(env),
+ pipelining: 0,
+ };
+}
+
export function getDefaultDispatcher(): Dispatcher {
const globalWithCache = globalThis as GlobalWithDispatcherCache;
if (!globalWithCache[DEFAULT_DISPATCHER_KEY]) {
- globalWithCache[DEFAULT_DISPATCHER_KEY] = new Agent(getDispatcherOptions());
+ globalWithCache[DEFAULT_DISPATCHER_KEY] = new Agent(getDefaultDispatcherOptions());
}
return globalWithCache[DEFAULT_DISPATCHER_KEY];
}
@@ -362,6 +394,12 @@ export function __getProxyDispatcherOptionsForTest(
return getProxyDispatcherOptions(env);
}
+export function __getDefaultDispatcherOptionsForTest(
+ env: Record = process.env
+) {
+ return getDefaultDispatcherOptions(env);
+}
+
export function createProxyDispatcher(proxyUrl: string): Dispatcher {
const normalizedUrl = normalizeProxyUrl(proxyUrl, "proxy dispatcher");
const dispatcherCache = getDispatcherCache();
diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts
index 3e51ea4e91..6d54886840 100644
--- a/open-sse/utils/stream.ts
+++ b/open-sse/utils/stream.ts
@@ -1545,6 +1545,26 @@ export function createSSEStream(options: StreamOptions = {}) {
if (Array.isArray(parsed.choices) && parsed.choices.length === 0) {
const emptyChoicesUsage = extractUsage(parsed) ?? parsed.usage;
if (hasValidUsage(emptyChoicesUsage)) {
+ // Some upstreams (e.g. Ollama Cloud) emit prompt_tokens: 0
+ // even when input was sent — they simply don't count input
+ // tokens. When we have a non-zero output but zero input,
+ // estimate the real input token count from the request body.
+ if (
+ emptyChoicesUsage &&
+ typeof emptyChoicesUsage === "object" &&
+ !Array.isArray(emptyChoicesUsage) &&
+ emptyChoicesUsage.completion_tokens > 0
+ ) {
+ const pt = emptyChoicesUsage.prompt_tokens ?? 0;
+ if (pt === 0) {
+ const estimated = estimateUsage(body, totalContentLength, FORMATS.OPENAI);
+ if (estimated?.prompt_tokens > 0) {
+ emptyChoicesUsage.prompt_tokens = estimated.prompt_tokens;
+ emptyChoicesUsage.total_tokens =
+ (emptyChoicesUsage.total_tokens ?? 0) + estimated.prompt_tokens;
+ }
+ }
+ }
usage = emptyChoicesUsage;
output = `data: ${JSON.stringify(parsed)}\n\n`;
injectedUsage = true;
diff --git a/package-lock.json b/package-lock.json
index c390e17096..8a1767faf8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "omniroute",
- "version": "3.8.33",
+ "version": "3.8.34",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
- "version": "3.8.33",
+ "version": "3.8.34",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
"open-sse"
],
"dependencies": {
- "@aws-sdk/client-bedrock-runtime": "^3.1045.0",
+ "@aws-sdk/client-bedrock-runtime": "^3.1073.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -24,7 +24,7 @@
"@ngrok/ngrok": "^1.7.0",
"@swc/helpers": "0.5.23",
"@types/mdx": "^2.0.13",
- "@xyflow/react": "^12.10.2",
+ "@xyflow/react": "^12.11.1",
"axios": "^1.16.1",
"bcryptjs": "^3.0.3",
"bottleneck": "^2.19.5",
@@ -34,9 +34,9 @@
"express": "^5.2.1",
"fetch-socks": "^1.3.3",
"fflate": "^0.8.3",
- "fumadocs-core": "^16.9.0",
+ "fumadocs-core": "^16.10.5",
"fumadocs-mdx": "^15.0.7",
- "fumadocs-ui": "^16.9.0",
+ "fumadocs-ui": "^16.10.5",
"http-proxy-middleware": "^4.0.0",
"https-proxy-agent": "^9.0.0",
"ink": "^7.0.3",
@@ -44,13 +44,13 @@
"ink-text-input": "^6.0.0",
"ioredis": "^5.10.1",
"jose": "^6.2.3",
- "js-yaml": "^4.1.1",
+ "js-yaml": "^5.0.0",
"jsonc-parser": "^3.3.1",
"lowdb": "^7.0.1",
- "lucide-react": "^1.16.0",
+ "lucide-react": "^1.21.0",
"marked": "^18.0.4",
"marked-terminal": "^7.3.0",
- "material-symbols": "^0.45.1",
+ "material-symbols": "^0.45.2",
"mermaid": "^11.15.0",
"monaco-editor": "^0.55.1",
"next": "^16.2.6",
@@ -58,7 +58,7 @@
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
"open": "^11.0.0",
- "ora": "^9.4.0",
+ "ora": "^9.4.1",
"parse5": "^8.0.1",
"pino": "^10.3.1",
"pino-abstract-transport": "^3.0.0",
@@ -103,7 +103,7 @@
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "latest",
"@types/keytar": "^4.4.2",
- "@types/node": "^25.9.1",
+ "@types/node": "^26.0.0",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.18.0",
@@ -114,15 +114,15 @@
"dpdm": "^4.2.0",
"eslint": "^9.39.4",
"eslint-config-next": "16.2.9",
- "eslint-plugin-sonarjs": "^4.0.3",
+ "eslint-plugin-sonarjs": "^4.1.0",
"fast-check": "^4.8.0",
"glob": "^13.0.6",
"husky": "^9.1.7",
"jscpd": "^4.2.5",
"jsdom": "^29.1.1",
- "knip": "^6.16.1",
+ "knip": "^6.18.0",
"license-checker-rseidelsohn": "^5.0.1",
- "lint-staged": "^17.0.5",
+ "lint-staged": "^17.0.8",
"lockfile-lint": "^5.0.0",
"node-loader": "^2.1.0",
"prettier": "^3.8.3",
@@ -355,9 +355,9 @@
}
},
"node_modules/@aws-sdk/client-bedrock-runtime": {
- "version": "3.1071.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1071.0.tgz",
- "integrity": "sha512-1Sa7UTC98Wy+M+zYbbXEqhcL5WUE3VnAmO1Y2RikmYjWx/8nCHudjSj1bVovk6Obfrrsir9Q1AbrmKbgxwI90g==",
+ "version": "3.1073.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1073.0.tgz",
+ "integrity": "sha512-Vecj8r9/KIh/Nu9T7CRoCw5EBqnmAa9Q+Iwi5J5Mr0IEBMH6KUoOgAjayfyEZjvvZTllLJ2dOAx5cYeIz8QD6A==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -367,7 +367,7 @@
"@aws-sdk/eventstream-handler-node": "^3.972.22",
"@aws-sdk/middleware-eventstream": "^3.972.18",
"@aws-sdk/middleware-websocket": "^3.972.30",
- "@aws-sdk/token-providers": "3.1071.0",
+ "@aws-sdk/token-providers": "3.1073.0",
"@aws-sdk/types": "^3.973.13",
"@smithy/core": "^3.24.6",
"@smithy/fetch-http-handler": "^5.4.6",
@@ -379,6 +379,23 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/token-providers": {
+ "version": "3.1073.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1073.0.tgz",
+ "integrity": "sha512-Tolawuc3I9Q6pElcqoBQMLCiCOfKn3eqG4oNIRci4BurhsrJmzXkhF3N+6LRXJrWYFtJKfTkBuLbYCLr8+pwig==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.22",
+ "@aws-sdk/nested-clients": "^3.997.22",
+ "@aws-sdk/types": "^3.973.13",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/core": {
"version": "3.974.22",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.22.tgz",
@@ -2291,6 +2308,29 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -4901,9 +4941,9 @@
}
},
"node_modules/@oxc-parser/binding-android-arm-eabi": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.135.0.tgz",
- "integrity": "sha512-sHeZItACNcA5WRAWqF6ixriR4GkZDyY10gVgnZU7pXku1DjHFATSqnwZM809jl0gXPHxb6fKzYQCK7bNK5cACQ==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.137.0.tgz",
+ "integrity": "sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==",
"cpu": [
"arm"
],
@@ -4918,9 +4958,9 @@
}
},
"node_modules/@oxc-parser/binding-android-arm64": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.135.0.tgz",
- "integrity": "sha512-wPte+SzgzWWFgMSF8YZDNM+tBXtJg0AXBi7+tU3yS2z1f2Af9kRLZLKuJojADmuD/cZexmnMHHC3SDItTW77Iw==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.137.0.tgz",
+ "integrity": "sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==",
"cpu": [
"arm64"
],
@@ -4935,9 +4975,9 @@
}
},
"node_modules/@oxc-parser/binding-darwin-arm64": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.135.0.tgz",
- "integrity": "sha512-BmKz3lHIsqVos+9aPcdYCT9MG3APoUyM43KlEFhJMWNVDOGG8FKyiFz81Bc+mGz2o0hpuQ3PfXLfVWJrKXjo2g==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.137.0.tgz",
+ "integrity": "sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==",
"cpu": [
"arm64"
],
@@ -4952,9 +4992,9 @@
}
},
"node_modules/@oxc-parser/binding-darwin-x64": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.135.0.tgz",
- "integrity": "sha512-dM8BS+8+Br1fNvmh2QZbGiHaYttwLebRa6J4Uz9vuFzMNmvsdRYwf7993ptOaV0JTrR63AaoVLjX7nhWbijxjQ==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.137.0.tgz",
+ "integrity": "sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==",
"cpu": [
"x64"
],
@@ -4969,9 +5009,9 @@
}
},
"node_modules/@oxc-parser/binding-freebsd-x64": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.135.0.tgz",
- "integrity": "sha512-xlZnvvJdR9bGu2pOhvR5hMuKPHCE6Sa9owK5A484mzjHdm75VRV5nCs5w/jkmGODMMTFc+KN7EnZqEieM813kw==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.137.0.tgz",
+ "integrity": "sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==",
"cpu": [
"x64"
],
@@ -4986,9 +5026,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.135.0.tgz",
- "integrity": "sha512-PSR8LmBK/H/PQRiN8g7RebQgZX/ntVCrdT/JBfNxE5ezdHG1s2i4rbazsRJYD83TTI1MmgTpC0MGL42PLtskQQ==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.137.0.tgz",
+ "integrity": "sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==",
"cpu": [
"arm"
],
@@ -5003,9 +5043,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.135.0.tgz",
- "integrity": "sha512-I85GJXzfUsigkkk7Ngdz95C217M4FdUi1Z2HrX5UyPmURobwQZ7m2bbUvwFkz4VGZd+lymFGKHvDZ3RQC9qOzA==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.137.0.tgz",
+ "integrity": "sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==",
"cpu": [
"arm"
],
@@ -5020,9 +5060,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-arm64-gnu": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.135.0.tgz",
- "integrity": "sha512-zqEY0npz0g0aGZj/8a5BclunjVDytsBQHYtIC10Gd26HcrLwbVF6YDbqRQjunMGYdSo97u6xOBl05aTDI2diDQ==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.137.0.tgz",
+ "integrity": "sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==",
"cpu": [
"arm64"
],
@@ -5040,9 +5080,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-arm64-musl": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.135.0.tgz",
- "integrity": "sha512-mWAfprP819gQ2qYst1RxgTI8b/z0b29OpoKfRflIXLHde2dZLihQD4g47Onuvtpo5GPIkMYPRlX9QoeZfs/GnQ==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.137.0.tgz",
+ "integrity": "sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==",
"cpu": [
"arm64"
],
@@ -5060,9 +5100,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.135.0.tgz",
- "integrity": "sha512-gri8c2AOmJKJwOux2KTHFBfUaXoJURuVMKhmKEi/2hTF55cQteTDV2XNfTiE5oCC+Tnem1Y4/MWzcyDadtsSag==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.137.0.tgz",
+ "integrity": "sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==",
"cpu": [
"ppc64"
],
@@ -5080,9 +5120,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.135.0.tgz",
- "integrity": "sha512-Y2tkupCG5wo0SxH2rMLG4d4Kmv6DaM3sBp+GuM5lox0S8Za6VxKgQrY2Mut088QQxKkEE89n/4CCCgmw2o0e3Q==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.137.0.tgz",
+ "integrity": "sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==",
"cpu": [
"riscv64"
],
@@ -5100,9 +5140,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-riscv64-musl": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.135.0.tgz",
- "integrity": "sha512-xDRJq6i6WTynjeP+ISbDpyH4p9BaJ0wuQcL0lCSDkt9qOXC9dmwpOu1VG/TlwmPI3KpYntmO9nJCuc3TMTsNBA==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.137.0.tgz",
+ "integrity": "sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==",
"cpu": [
"riscv64"
],
@@ -5120,9 +5160,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-s390x-gnu": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.135.0.tgz",
- "integrity": "sha512-V4MoUuiCRNvihxhIufRxvK+ka013V4joTSK0FAGA1KEjLuNprfH6N/Qw2uxQEVIFuNYMhD/hV6xJ/ptbzlKdHg==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.137.0.tgz",
+ "integrity": "sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==",
"cpu": [
"s390x"
],
@@ -5140,9 +5180,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-x64-gnu": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.135.0.tgz",
- "integrity": "sha512-JCFZ7zM7KXOKoPAbK/ZB4wY0M1jxRECiem2UQuiXLjzGqS9+hno7mtX+qyK2F7HWK2xPhyJb+frpcOtk5DKOtg==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.137.0.tgz",
+ "integrity": "sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==",
"cpu": [
"x64"
],
@@ -5160,9 +5200,9 @@
}
},
"node_modules/@oxc-parser/binding-linux-x64-musl": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.135.0.tgz",
- "integrity": "sha512-9jSVS1b3hOV7sdKH4aA2DFfnTz0RgQd0v2BefR+LYbH8yIlmSM22JJZbAAjVeVXmFgUAk3zJQ1tpE/Nd+Vi2YQ==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.137.0.tgz",
+ "integrity": "sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==",
"cpu": [
"x64"
],
@@ -5180,9 +5220,9 @@
}
},
"node_modules/@oxc-parser/binding-openharmony-arm64": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.135.0.tgz",
- "integrity": "sha512-M857ZLBSdn1Uy/SJJz5zh0qGu67B4P9omCgXGBU2LLqTzraX6ZjVNaKq5yW1PDw/LgJXDXR/dbZfgmB310f11Q==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.137.0.tgz",
+ "integrity": "sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==",
"cpu": [
"arm64"
],
@@ -5197,9 +5237,9 @@
}
},
"node_modules/@oxc-parser/binding-wasm32-wasi": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.135.0.tgz",
- "integrity": "sha512-2w6DVcntQZX9U5RhXtgiWb3FLWFB5EcwI1U8yr3htOCJUJjagN4BFUHz/Y/d9ZsumndZ6ByxxWEtbUZNE1bfFw==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.137.0.tgz",
+ "integrity": "sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==",
"cpu": [
"wasm32"
],
@@ -5207,18 +5247,71 @@
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/core": "1.10.0",
- "@emnapi/runtime": "1.10.0",
- "@napi-rs/wasm-runtime": "^1.1.4"
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.5"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
+ "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
"node_modules/@oxc-parser/binding-win32-arm64-msvc": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.135.0.tgz",
- "integrity": "sha512-rX1U8+IH2Z37EJjDXKa1iifvUQAdba+vZ4Ewj1iaG5eA/QaSybzclCOwtWa0/5BuUQnnK/T2JHUEFrwhL6Ck2Q==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz",
+ "integrity": "sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==",
"cpu": [
"arm64"
],
@@ -5233,9 +5326,9 @@
}
},
"node_modules/@oxc-parser/binding-win32-ia32-msvc": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.135.0.tgz",
- "integrity": "sha512-9FAisBbH1QICGAjlJobiuKGd/jOuVmyqniWdQMwTa5SkCl6hhuotBCJf1n46B0flYbSOR5TzfV9HZCWSyb3c/Q==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.137.0.tgz",
+ "integrity": "sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==",
"cpu": [
"ia32"
],
@@ -5250,9 +5343,9 @@
}
},
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.135.0.tgz",
- "integrity": "sha512-wYF+A2AzJ2n7ul6q+Z2G/ia0S2+8cUp0AgWZzoFvF4WmUcl1P7p+o6se1Gdr5wGnWuF0iAMIkGddrjCarNr2yA==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.137.0.tgz",
+ "integrity": "sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==",
"cpu": [
"x64"
],
@@ -5277,9 +5370,9 @@
}
},
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.20.0.tgz",
- "integrity": "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz",
+ "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==",
"cpu": [
"arm"
],
@@ -5291,9 +5384,9 @@
]
},
"node_modules/@oxc-resolver/binding-android-arm64": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.20.0.tgz",
- "integrity": "sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz",
+ "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==",
"cpu": [
"arm64"
],
@@ -5305,9 +5398,9 @@
]
},
"node_modules/@oxc-resolver/binding-darwin-arm64": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.20.0.tgz",
- "integrity": "sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz",
+ "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==",
"cpu": [
"arm64"
],
@@ -5319,9 +5412,9 @@
]
},
"node_modules/@oxc-resolver/binding-darwin-x64": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.20.0.tgz",
- "integrity": "sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz",
+ "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==",
"cpu": [
"x64"
],
@@ -5333,9 +5426,9 @@
]
},
"node_modules/@oxc-resolver/binding-freebsd-x64": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.20.0.tgz",
- "integrity": "sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz",
+ "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==",
"cpu": [
"x64"
],
@@ -5347,9 +5440,9 @@
]
},
"node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.20.0.tgz",
- "integrity": "sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz",
+ "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==",
"cpu": [
"arm"
],
@@ -5361,9 +5454,9 @@
]
},
"node_modules/@oxc-resolver/binding-linux-arm-musleabihf": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.20.0.tgz",
- "integrity": "sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz",
+ "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==",
"cpu": [
"arm"
],
@@ -5375,13 +5468,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-arm64-gnu": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.20.0.tgz",
- "integrity": "sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz",
+ "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==",
"cpu": [
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5389,13 +5485,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-arm64-musl": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.20.0.tgz",
- "integrity": "sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz",
+ "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==",
"cpu": [
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5403,13 +5502,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-ppc64-gnu": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.20.0.tgz",
- "integrity": "sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz",
+ "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==",
"cpu": [
"ppc64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5417,13 +5519,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-riscv64-gnu": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.20.0.tgz",
- "integrity": "sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz",
+ "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==",
"cpu": [
"riscv64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5431,13 +5536,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-riscv64-musl": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.20.0.tgz",
- "integrity": "sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz",
+ "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==",
"cpu": [
"riscv64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5445,13 +5553,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-s390x-gnu": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.20.0.tgz",
- "integrity": "sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz",
+ "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==",
"cpu": [
"s390x"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5459,13 +5570,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-x64-gnu": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.20.0.tgz",
- "integrity": "sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz",
+ "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==",
"cpu": [
"x64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5473,13 +5587,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-x64-musl": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.20.0.tgz",
- "integrity": "sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz",
+ "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==",
"cpu": [
"x64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5487,9 +5604,9 @@
]
},
"node_modules/@oxc-resolver/binding-openharmony-arm64": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.20.0.tgz",
- "integrity": "sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz",
+ "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==",
"cpu": [
"arm64"
],
@@ -5501,9 +5618,9 @@
]
},
"node_modules/@oxc-resolver/binding-wasm32-wasi": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.20.0.tgz",
- "integrity": "sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz",
+ "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==",
"cpu": [
"wasm32"
],
@@ -5511,18 +5628,71 @@
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/core": "1.10.0",
- "@emnapi/runtime": "1.10.0",
- "@napi-rs/wasm-runtime": "^1.1.4"
+ "@emnapi/core": "1.11.0",
+ "@emnapi/runtime": "1.11.0",
+ "@napi-rs/wasm-runtime": "^1.1.5"
},
"engines": {
"node": ">=14.0.0"
}
},
+ "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz",
+ "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz",
+ "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
"node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.20.0.tgz",
- "integrity": "sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz",
+ "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==",
"cpu": [
"arm64"
],
@@ -5534,9 +5704,9 @@
]
},
"node_modules/@oxc-resolver/binding-win32-x64-msvc": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.20.0.tgz",
- "integrity": "sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz",
+ "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==",
"cpu": [
"x64"
],
@@ -6140,19 +6310,19 @@
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion": {
- "version": "1.2.13",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.13.tgz",
- "integrity": "sha512-xITxBB2p5m5tAe7M0F95kb4uAh7jSIKGlExMEm93HlW+XxZHV2eXFbPWLktd4JhRiwcnXNbO7iekcrbZy6ZCvA==",
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.14.tgz",
+ "integrity": "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-collapsible": "1.1.13",
- "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-collapsible": "1.1.14",
+ "@radix-ui/react-collection": "1.1.10",
"@radix-ui/react-compose-refs": "1.1.3",
"@radix-ui/react-context": "1.1.4",
"@radix-ui/react-direction": "1.1.2",
"@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-controllable-state": "1.2.3"
},
"peerDependencies": {
@@ -6171,12 +6341,12 @@
}
},
"node_modules/@radix-ui/react-arrow": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.9.tgz",
- "integrity": "sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig==",
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz",
+ "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.5"
+ "@radix-ui/react-primitive": "2.1.6"
},
"peerDependencies": {
"@types/react": "*",
@@ -6194,9 +6364,9 @@
}
},
"node_modules/@radix-ui/react-collapsible": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.13.tgz",
- "integrity": "sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA==",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.14.tgz",
+ "integrity": "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.4",
@@ -6204,7 +6374,7 @@
"@radix-ui/react-context": "1.1.4",
"@radix-ui/react-id": "1.1.2",
"@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-controllable-state": "1.2.3",
"@radix-ui/react-use-layout-effect": "1.1.2"
},
@@ -6224,15 +6394,15 @@
}
},
"node_modules/@radix-ui/react-collection": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.9.tgz",
- "integrity": "sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ==",
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz",
+ "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.3",
"@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-slot": "1.2.5"
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0"
},
"peerDependencies": {
"@types/react": "*",
@@ -6280,22 +6450,22 @@
}
},
"node_modules/@radix-ui/react-dialog": {
- "version": "1.1.16",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.16.tgz",
- "integrity": "sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw==",
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz",
+ "integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.4",
"@radix-ui/react-compose-refs": "1.1.3",
"@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
"@radix-ui/react-focus-guards": "1.1.4",
- "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-focus-scope": "1.1.10",
"@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-portal": "1.1.12",
"@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
"@radix-ui/react-use-controllable-state": "1.2.3",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.7.2"
@@ -6331,14 +6501,14 @@
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.12.tgz",
- "integrity": "sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz",
+ "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.4",
"@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-callback-ref": "1.1.2",
"@radix-ui/react-use-escape-keydown": "1.1.2"
},
@@ -6373,13 +6543,13 @@
}
},
"node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.9.tgz",
- "integrity": "sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ==",
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz",
+ "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-callback-ref": "1.1.2"
},
"peerDependencies": {
@@ -6416,25 +6586,25 @@
}
},
"node_modules/@radix-ui/react-navigation-menu": {
- "version": "1.2.15",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.15.tgz",
- "integrity": "sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg==",
+ "version": "1.2.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.16.tgz",
+ "integrity": "sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-collection": "1.1.10",
"@radix-ui/react-compose-refs": "1.1.3",
"@radix-ui/react-context": "1.1.4",
"@radix-ui/react-direction": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
"@radix-ui/react-id": "1.1.2",
"@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-callback-ref": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.2.3",
"@radix-ui/react-use-layout-effect": "1.1.2",
"@radix-ui/react-use-previous": "1.1.2",
- "@radix-ui/react-visually-hidden": "1.2.5"
+ "@radix-ui/react-visually-hidden": "1.2.6"
},
"peerDependencies": {
"@types/react": "*",
@@ -6452,23 +6622,23 @@
}
},
"node_modules/@radix-ui/react-popover": {
- "version": "1.1.16",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.16.tgz",
- "integrity": "sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw==",
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.17.tgz",
+ "integrity": "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.4",
"@radix-ui/react-compose-refs": "1.1.3",
"@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
"@radix-ui/react-focus-guards": "1.1.4",
- "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-focus-scope": "1.1.10",
"@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-popper": "1.3.0",
- "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
"@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
"@radix-ui/react-use-controllable-state": "1.2.3",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.7.2"
@@ -6489,16 +6659,16 @@
}
},
"node_modules/@radix-ui/react-popper": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.0.tgz",
- "integrity": "sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz",
+ "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
- "@radix-ui/react-arrow": "1.1.9",
+ "@radix-ui/react-arrow": "1.1.10",
"@radix-ui/react-compose-refs": "1.1.3",
"@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-callback-ref": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.2",
"@radix-ui/react-use-rect": "1.1.2",
@@ -6521,12 +6691,12 @@
}
},
"node_modules/@radix-ui/react-portal": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.11.tgz",
- "integrity": "sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz",
+ "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-layout-effect": "1.1.2"
},
"peerDependencies": {
@@ -6568,12 +6738,12 @@
}
},
"node_modules/@radix-ui/react-primitive": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.5.tgz",
- "integrity": "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA==",
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz",
+ "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-slot": "1.2.5"
+ "@radix-ui/react-slot": "1.3.0"
},
"peerDependencies": {
"@types/react": "*",
@@ -6591,18 +6761,18 @@
}
},
"node_modules/@radix-ui/react-roving-focus": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.12.tgz",
- "integrity": "sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz",
+ "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-collection": "1.1.10",
"@radix-ui/react-compose-refs": "1.1.3",
"@radix-ui/react-context": "1.1.4",
"@radix-ui/react-direction": "1.1.2",
"@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-callback-ref": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.2.3"
},
@@ -6622,9 +6792,9 @@
}
},
"node_modules/@radix-ui/react-scroll-area": {
- "version": "1.2.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.11.tgz",
- "integrity": "sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ==",
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.12.tgz",
+ "integrity": "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.2",
@@ -6633,7 +6803,7 @@
"@radix-ui/react-context": "1.1.4",
"@radix-ui/react-direction": "1.1.2",
"@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-primitive": "2.1.6",
"@radix-ui/react-use-callback-ref": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.2"
},
@@ -6653,9 +6823,9 @@
}
},
"node_modules/@radix-ui/react-slot": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.5.tgz",
- "integrity": "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
+ "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.3"
@@ -6671,9 +6841,9 @@
}
},
"node_modules/@radix-ui/react-tabs": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.14.tgz",
- "integrity": "sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA==",
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz",
+ "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.4",
@@ -6681,8 +6851,8 @@
"@radix-ui/react-direction": "1.1.2",
"@radix-ui/react-id": "1.1.2",
"@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
"@radix-ui/react-use-controllable-state": "1.2.3"
},
"peerDependencies": {
@@ -6837,12 +7007,12 @@
}
},
"node_modules/@radix-ui/react-visually-hidden": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.5.tgz",
- "integrity": "sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg==",
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz",
+ "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.5"
+ "@radix-ui/react-primitive": "2.1.6"
},
"peerDependencies": {
"@types/react": "*",
@@ -9240,12 +9410,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.9.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
- "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
+ "version": "26.0.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
+ "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
"license": "MIT",
"dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
+ "undici-types": "~8.3.0"
}
},
"node_modules/@types/node-fetch": {
@@ -10069,12 +10239,12 @@
"optional": true
},
"node_modules/@xyflow/react": {
- "version": "12.11.0",
- "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz",
- "integrity": "sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==",
+ "version": "12.11.1",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.1.tgz",
+ "integrity": "sha512-L+zBoLGSXham0MnlY8QqjfR7/C5JNw0zxkaey5aZ5XmCgJBAdH4+WRIu8CR40d3l/BdU635V6YbhBK1jMo8/6Q==",
"license": "MIT",
"dependencies": {
- "@xyflow/system": "0.0.77",
+ "@xyflow/system": "0.0.78",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
@@ -10122,9 +10292,9 @@
}
},
"node_modules/@xyflow/system": {
- "version": "0.0.77",
- "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.77.tgz",
- "integrity": "sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==",
+ "version": "0.0.78",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.78.tgz",
+ "integrity": "sha512-lY0z2qP33fUhTva9Vaxrk0lqZta2pkbxB1trHAx1omnJqRtPvDlAQYV2r5fhS6AdpkulYmbNW0svy+A4/t4B/g==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
@@ -14271,9 +14441,9 @@
}
},
"node_modules/eslint-plugin-sonarjs": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-4.0.3.tgz",
- "integrity": "sha512-5drkJKLC9qQddIiaATV0e8+ygbUc7b0Ti6VB7M2d3jmKNh3X0RaiIJYTs3dr9xnlhlrxo+/s1FoO3Jgv6O/c7g==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-4.1.0.tgz",
+ "integrity": "sha512-rh+FlVz0yfd2RNIb6WqSkuGh0addX/Qi5scwQ5FphXDFrM6fZKcxP1+attJ78yUKcyYfiu6MTaISPpAFPzqRJw==",
"dev": true,
"license": "LGPL-3.0-only",
"dependencies": {
@@ -14281,14 +14451,15 @@
"builtin-modules": "^3.3.0",
"bytes": "^3.1.2",
"functional-red-black-tree": "^1.0.1",
- "globals": "^17.4.0",
+ "globals": "^17.6.0",
"jsx-ast-utils-x": "^0.1.0",
"lodash.merge": "^4.6.2",
"minimatch": "^10.2.5",
"scslre": "^0.3.0",
- "semver": "^7.7.4",
+ "semver": "^7.8.4",
"ts-api-utils": "^2.5.0",
- "typescript": ">=5"
+ "typescript": ">=5",
+ "yaml": "^2.9.0"
},
"peerDependencies": {
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
@@ -15345,9 +15516,9 @@
}
},
"node_modules/fumadocs-core": {
- "version": "16.10.3",
- "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.10.3.tgz",
- "integrity": "sha512-xXhqz/fqbN7pLlshJb/B5L+vzMJOmWxoPj7+KMRTa/4A669hKeeCBPpRAiooMqjblWqIRSxLiO02/ds8ltvUPQ==",
+ "version": "16.10.5",
+ "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.10.5.tgz",
+ "integrity": "sha512-e/xrZnKvQo8bF/WYMwPuym8PR3OtjZzHy0S/EIOvGwjKRgVq9z6J58zaBpi4LvYtPVZxNGsxdZVlmZXCVWq4FQ==",
"license": "MIT",
"dependencies": {
"@orama/orama": "^3.1.18",
@@ -15384,7 +15555,7 @@
"next": "16.x.x",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "react-router": "7.x.x",
+ "react-router": "7.x.x || 8.x.x",
"waku": "*",
"zod": "4.x.x"
},
@@ -15445,6 +15616,28 @@
}
}
},
+ "node_modules/fumadocs-core/node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/fumadocs-mdx": {
"version": "15.0.12",
"resolved": "https://registry.npmjs.org/fumadocs-mdx/-/fumadocs-mdx-15.0.12.tgz",
@@ -15509,26 +15702,48 @@
}
}
},
+ "node_modules/fumadocs-mdx/node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/fumadocs-ui": {
- "version": "16.10.3",
- "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.10.3.tgz",
- "integrity": "sha512-0aSLdQ73EWoCmYcQYr2uNHlSB/s2fD+NMugtdZF3vC4lqs0MfyOtwnZPyYAskUnXNs6HECly/Hu6oY5JqmlkHg==",
+ "version": "16.10.5",
+ "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.10.5.tgz",
+ "integrity": "sha512-vd69ckYx/4a1aoJTCUJ5LBkqNeOFxm3r+8SK9bVYaeHJrY/n8+4W6b0soqxVqgj1UwNmgovoAg0vlsYmSxZBgQ==",
"license": "MIT",
"dependencies": {
"@fuma-translate/react": "^1.0.2",
"@fumadocs/tailwind": "0.0.5",
- "@radix-ui/react-accordion": "^1.2.13",
- "@radix-ui/react-collapsible": "^1.1.13",
- "@radix-ui/react-dialog": "^1.1.16",
+ "@radix-ui/react-accordion": "^1.2.14",
+ "@radix-ui/react-collapsible": "^1.1.14",
+ "@radix-ui/react-dialog": "^1.1.17",
"@radix-ui/react-direction": "^1.1.2",
- "@radix-ui/react-navigation-menu": "^1.2.15",
- "@radix-ui/react-popover": "^1.1.16",
+ "@radix-ui/react-navigation-menu": "^1.2.16",
+ "@radix-ui/react-popover": "^1.1.17",
"@radix-ui/react-presence": "^1.1.6",
- "@radix-ui/react-scroll-area": "^1.2.11",
- "@radix-ui/react-slot": "^1.2.5",
- "@radix-ui/react-tabs": "^1.1.14",
+ "@radix-ui/react-scroll-area": "^1.2.12",
+ "@radix-ui/react-slot": "^1.3.0",
+ "@radix-ui/react-tabs": "^1.1.15",
"class-variance-authority": "^0.7.1",
- "lucide-react": "^1.17.0",
+ "lucide-react": "^1.20.0",
"motion": "^12.40.0",
"next-themes": "^0.4.6",
"react-remove-scroll": "^2.7.2",
@@ -15542,7 +15757,7 @@
"@takumi-rs/image-response": "*",
"@types/mdx": "*",
"@types/react": "*",
- "fumadocs-core": "16.10.3",
+ "fumadocs-core": "16.10.5",
"next": "16.x.x",
"react": "^19.2.0",
"react-dom": "^19.2.0"
@@ -17870,9 +18085,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
- "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.0.0.tgz",
+ "integrity": "sha512-GSvaPUbk1U+FMZ7rJzF+F8e5YVtu7KnD40et/5rBXXRBv2jCO9L3qCewvIDDdudC0QycTFlf6EAA+h3kxBsuUw==",
"funding": [
{
"type": "github",
@@ -17888,7 +18103,7 @@
"argparse": "^2.0.1"
},
"bin": {
- "js-yaml": "bin/js-yaml.js"
+ "js-yaml": "bin/js-yaml.mjs"
}
},
"node_modules/jscpd": {
@@ -18205,9 +18420,9 @@
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
},
"node_modules/knip": {
- "version": "6.17.1",
- "resolved": "https://registry.npmjs.org/knip/-/knip-6.17.1.tgz",
- "integrity": "sha512-HcQsZSQ4Ymhuay4BVzJtM5pFZNDSomYYqcNCZOSITPQh9g18a09DqziWAxSt2G+BH9wGlG+0ZjWpEnaFlnKseQ==",
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/knip/-/knip-6.18.0.tgz",
+ "integrity": "sha512-RlT6fK3epETsEUCVdlz96CiJN3DQJwuxOuQhEeAVWjNRuAUJHWwe+XolTL8vIiwLPZ38tTdPWJUgZyTdskOEog==",
"dev": true,
"funding": [
{
@@ -18225,8 +18440,8 @@
"formatly": "^0.3.0",
"get-tsconfig": "4.14.0",
"jiti": "^2.7.0",
- "oxc-parser": "^0.135.0",
- "oxc-resolver": "^11.20.0",
+ "oxc-parser": "^0.137.0",
+ "oxc-resolver": "11.21.3",
"picomatch": "^4.0.4",
"smol-toml": "^1.6.1",
"strip-json-comments": "5.0.3",
@@ -19034,9 +19249,9 @@
"license": "MIT"
},
"node_modules/lint-staged": {
- "version": "17.0.7",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz",
- "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==",
+ "version": "17.0.8",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.8.tgz",
+ "integrity": "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -19198,6 +19413,29 @@
"node": ">= 6"
}
},
+ "node_modules/lockfile-lint/node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
@@ -19390,9 +19628,9 @@
}
},
"node_modules/lucide-react": {
- "version": "1.20.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.20.0.tgz",
- "integrity": "sha512-jhXLeC/7m0/tjL1nzMdKk6x256zWA6AtbhTVreHOiKPoeX2d6MK4FbyIQPpVq0E6iPWBisyy1TW+pEge/uMEuQ==",
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz",
+ "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -19553,9 +19791,9 @@
}
},
"node_modules/material-symbols": {
- "version": "0.45.1",
- "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.45.1.tgz",
- "integrity": "sha512-iQibeoKymxHthNqTjYg/jCN2pRySFQ+DAD0sYK9FS7rUpf38i1qR3N4I1jfi/bLS5qdEKvkkJAvf2mE89amSZw==",
+ "version": "0.45.2",
+ "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.45.2.tgz",
+ "integrity": "sha512-TxcmySpXFc+03tgyl0Z6J1Yvn5/eY9b9vnizJu5Xg3RrrL5jFLp3KlZKq5k2ttKj3SqH/omofH7BwZMO/zk1mw==",
"license": "Apache-2.0"
},
"node_modules/math-intrinsics": {
@@ -22068,9 +22306,9 @@
}
},
"node_modules/ora": {
- "version": "9.4.0",
- "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz",
- "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==",
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz",
+ "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==",
"license": "MIT",
"dependencies": {
"chalk": "^5.6.2",
@@ -22120,13 +22358,13 @@
}
},
"node_modules/oxc-parser": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.135.0.tgz",
- "integrity": "sha512-/DaPStu0s2zzNSRRniKyTPM6Z/o+DapOp2JYNKDL8AsgaBGPK2IdZyB87SQjVH+xeQPz+Qr9mrjglfkYgtbVRA==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.137.0.tgz",
+ "integrity": "sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "^0.135.0"
+ "@oxc-project/types": "^0.137.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -22135,32 +22373,32 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
- "@oxc-parser/binding-android-arm-eabi": "0.135.0",
- "@oxc-parser/binding-android-arm64": "0.135.0",
- "@oxc-parser/binding-darwin-arm64": "0.135.0",
- "@oxc-parser/binding-darwin-x64": "0.135.0",
- "@oxc-parser/binding-freebsd-x64": "0.135.0",
- "@oxc-parser/binding-linux-arm-gnueabihf": "0.135.0",
- "@oxc-parser/binding-linux-arm-musleabihf": "0.135.0",
- "@oxc-parser/binding-linux-arm64-gnu": "0.135.0",
- "@oxc-parser/binding-linux-arm64-musl": "0.135.0",
- "@oxc-parser/binding-linux-ppc64-gnu": "0.135.0",
- "@oxc-parser/binding-linux-riscv64-gnu": "0.135.0",
- "@oxc-parser/binding-linux-riscv64-musl": "0.135.0",
- "@oxc-parser/binding-linux-s390x-gnu": "0.135.0",
- "@oxc-parser/binding-linux-x64-gnu": "0.135.0",
- "@oxc-parser/binding-linux-x64-musl": "0.135.0",
- "@oxc-parser/binding-openharmony-arm64": "0.135.0",
- "@oxc-parser/binding-wasm32-wasi": "0.135.0",
- "@oxc-parser/binding-win32-arm64-msvc": "0.135.0",
- "@oxc-parser/binding-win32-ia32-msvc": "0.135.0",
- "@oxc-parser/binding-win32-x64-msvc": "0.135.0"
+ "@oxc-parser/binding-android-arm-eabi": "0.137.0",
+ "@oxc-parser/binding-android-arm64": "0.137.0",
+ "@oxc-parser/binding-darwin-arm64": "0.137.0",
+ "@oxc-parser/binding-darwin-x64": "0.137.0",
+ "@oxc-parser/binding-freebsd-x64": "0.137.0",
+ "@oxc-parser/binding-linux-arm-gnueabihf": "0.137.0",
+ "@oxc-parser/binding-linux-arm-musleabihf": "0.137.0",
+ "@oxc-parser/binding-linux-arm64-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-arm64-musl": "0.137.0",
+ "@oxc-parser/binding-linux-ppc64-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-riscv64-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-riscv64-musl": "0.137.0",
+ "@oxc-parser/binding-linux-s390x-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-x64-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-x64-musl": "0.137.0",
+ "@oxc-parser/binding-openharmony-arm64": "0.137.0",
+ "@oxc-parser/binding-wasm32-wasi": "0.137.0",
+ "@oxc-parser/binding-win32-arm64-msvc": "0.137.0",
+ "@oxc-parser/binding-win32-ia32-msvc": "0.137.0",
+ "@oxc-parser/binding-win32-x64-msvc": "0.137.0"
}
},
"node_modules/oxc-parser/node_modules/@oxc-project/types": {
- "version": "0.135.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.135.0.tgz",
- "integrity": "sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
+ "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -22168,34 +22406,34 @@
}
},
"node_modules/oxc-resolver": {
- "version": "11.20.0",
- "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.20.0.tgz",
- "integrity": "sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==",
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz",
+ "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
- "@oxc-resolver/binding-android-arm-eabi": "11.20.0",
- "@oxc-resolver/binding-android-arm64": "11.20.0",
- "@oxc-resolver/binding-darwin-arm64": "11.20.0",
- "@oxc-resolver/binding-darwin-x64": "11.20.0",
- "@oxc-resolver/binding-freebsd-x64": "11.20.0",
- "@oxc-resolver/binding-linux-arm-gnueabihf": "11.20.0",
- "@oxc-resolver/binding-linux-arm-musleabihf": "11.20.0",
- "@oxc-resolver/binding-linux-arm64-gnu": "11.20.0",
- "@oxc-resolver/binding-linux-arm64-musl": "11.20.0",
- "@oxc-resolver/binding-linux-ppc64-gnu": "11.20.0",
- "@oxc-resolver/binding-linux-riscv64-gnu": "11.20.0",
- "@oxc-resolver/binding-linux-riscv64-musl": "11.20.0",
- "@oxc-resolver/binding-linux-s390x-gnu": "11.20.0",
- "@oxc-resolver/binding-linux-x64-gnu": "11.20.0",
- "@oxc-resolver/binding-linux-x64-musl": "11.20.0",
- "@oxc-resolver/binding-openharmony-arm64": "11.20.0",
- "@oxc-resolver/binding-wasm32-wasi": "11.20.0",
- "@oxc-resolver/binding-win32-arm64-msvc": "11.20.0",
- "@oxc-resolver/binding-win32-x64-msvc": "11.20.0"
+ "@oxc-resolver/binding-android-arm-eabi": "11.21.3",
+ "@oxc-resolver/binding-android-arm64": "11.21.3",
+ "@oxc-resolver/binding-darwin-arm64": "11.21.3",
+ "@oxc-resolver/binding-darwin-x64": "11.21.3",
+ "@oxc-resolver/binding-freebsd-x64": "11.21.3",
+ "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3",
+ "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3",
+ "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-arm64-musl": "11.21.3",
+ "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3",
+ "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-x64-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-x64-musl": "11.21.3",
+ "@oxc-resolver/binding-openharmony-arm64": "11.21.3",
+ "@oxc-resolver/binding-wasm32-wasi": "11.21.3",
+ "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3",
+ "@oxc-resolver/binding-win32-x64-msvc": "11.21.3"
}
},
"node_modules/p-limit": {
@@ -26746,9 +26984,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"license": "MIT"
},
"node_modules/unicode-emoji-modifier-base": {
@@ -27996,6 +28234,29 @@
"node": ">=20.0"
}
},
+ "node_modules/xmlbuilder2/node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
@@ -28241,7 +28502,7 @@
},
"open-sse": {
"name": "@omniroute/open-sse",
- "version": "3.8.33"
+ "version": "3.8.34"
}
}
}
diff --git a/package.json b/package.json
index cebadbb6d5..a27020b7bc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "omniroute",
- "version": "3.8.33",
+ "version": "3.8.34",
"description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
"type": "module",
"bin": {
@@ -145,6 +145,7 @@
"check:complexity": "node scripts/check/check-complexity.mjs",
"check:dead-code": "node scripts/check/check-dead-code.mjs",
"check:cognitive-complexity": "node scripts/check/check-cognitive-complexity.mjs",
+ "check:release-green": "node scripts/quality/validate-release-green.mjs",
"check:type-coverage": "node scripts/check/check-type-coverage.mjs",
"check:lockfile": "node scripts/check/check-lockfile.mjs",
"check:bundle-size": "node scripts/check/check-bundle-size.mjs",
@@ -194,7 +195,7 @@
"build:cli-api": "node --import tsx/esm scripts/cli/generate-api-commands.mjs"
},
"dependencies": {
- "@aws-sdk/client-bedrock-runtime": "^3.1045.0",
+ "@aws-sdk/client-bedrock-runtime": "^3.1073.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -205,7 +206,7 @@
"@ngrok/ngrok": "^1.7.0",
"@swc/helpers": "0.5.23",
"@types/mdx": "^2.0.13",
- "@xyflow/react": "^12.10.2",
+ "@xyflow/react": "^12.11.1",
"axios": "^1.16.1",
"bcryptjs": "^3.0.3",
"bottleneck": "^2.19.5",
@@ -215,9 +216,9 @@
"express": "^5.2.1",
"fetch-socks": "^1.3.3",
"fflate": "^0.8.3",
- "fumadocs-core": "^16.9.0",
+ "fumadocs-core": "^16.10.5",
"fumadocs-mdx": "^15.0.7",
- "fumadocs-ui": "^16.9.0",
+ "fumadocs-ui": "^16.10.5",
"http-proxy-middleware": "^4.0.0",
"https-proxy-agent": "^9.0.0",
"ink": "^7.0.3",
@@ -225,13 +226,13 @@
"ink-text-input": "^6.0.0",
"ioredis": "^5.10.1",
"jose": "^6.2.3",
- "js-yaml": "^4.1.1",
+ "js-yaml": "^5.0.0",
"jsonc-parser": "^3.3.1",
"lowdb": "^7.0.1",
- "lucide-react": "^1.16.0",
+ "lucide-react": "^1.21.0",
"marked": "^18.0.4",
"marked-terminal": "^7.3.0",
- "material-symbols": "^0.45.1",
+ "material-symbols": "^0.45.2",
"mermaid": "^11.15.0",
"monaco-editor": "^0.55.1",
"next": "^16.2.6",
@@ -239,7 +240,7 @@
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
"open": "^11.0.0",
- "ora": "^9.4.0",
+ "ora": "^9.4.1",
"parse5": "^8.0.1",
"pino": "^10.3.1",
"pino-abstract-transport": "^3.0.0",
@@ -289,7 +290,7 @@
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "latest",
"@types/keytar": "^4.4.2",
- "@types/node": "^25.9.1",
+ "@types/node": "^26.0.0",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.18.0",
@@ -300,15 +301,15 @@
"dpdm": "^4.2.0",
"eslint": "^9.39.4",
"eslint-config-next": "16.2.9",
- "eslint-plugin-sonarjs": "^4.0.3",
+ "eslint-plugin-sonarjs": "^4.1.0",
"fast-check": "^4.8.0",
"glob": "^13.0.6",
"husky": "^9.1.7",
"jscpd": "^4.2.5",
"jsdom": "^29.1.1",
- "knip": "^6.16.1",
+ "knip": "^6.18.0",
"license-checker-rseidelsohn": "^5.0.1",
- "lint-staged": "^17.0.5",
+ "lint-staged": "^17.0.8",
"lockfile-lint": "^5.0.0",
"node-loader": "^2.1.0",
"prettier": "^3.8.3",
diff --git a/scripts/check/check-fabricated-docs.mjs b/scripts/check/check-fabricated-docs.mjs
index 7a4a867a07..02ba62ac33 100644
--- a/scripts/check/check-fabricated-docs.mjs
+++ b/scripts/check/check-fabricated-docs.mjs
@@ -131,6 +131,7 @@ const ENV_VAR_DENYLIST = new Set([
"REPOSITORY_MAP",
"AUTHZ_GUIDE",
"RESILIENCE_GUIDE",
+ "COMPRESSION_GUIDE",
"MCP_SERVER",
"MCP_AUDIT",
"MCP_TOOLS",
diff --git a/scripts/check/check-openapi-coverage.mjs b/scripts/check/check-openapi-coverage.mjs
index 57cf6651d6..0476107ea8 100644
--- a/scripts/check/check-openapi-coverage.mjs
+++ b/scripts/check/check-openapi-coverage.mjs
@@ -9,7 +9,7 @@
import fs from "node:fs";
import path from "node:path";
-import yaml from "js-yaml";
+import * as yaml from "js-yaml";
const ROOT = process.cwd();
const API_ROOT = path.join(ROOT, "src", "app", "api");
diff --git a/scripts/check/check-openapi-routes.mjs b/scripts/check/check-openapi-routes.mjs
index bb19af19b4..7f8b8b4923 100644
--- a/scripts/check/check-openapi-routes.mjs
+++ b/scripts/check/check-openapi-routes.mjs
@@ -9,7 +9,7 @@
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
-import yaml from "js-yaml";
+import * as yaml from "js-yaml";
import { assertNoStale } from "./lib/allowlist.mjs";
const ROOT = process.cwd();
diff --git a/scripts/check/check-openapi-security-tiers.mjs b/scripts/check/check-openapi-security-tiers.mjs
index 874dfed78e..c2acfd6d23 100644
--- a/scripts/check/check-openapi-security-tiers.mjs
+++ b/scripts/check/check-openapi-security-tiers.mjs
@@ -8,7 +8,7 @@
import fs from "node:fs";
import path from "node:path";
-import yaml from "js-yaml";
+import * as yaml from "js-yaml";
const ROOT = process.cwd();
const OPENAPI_PATH = path.join(ROOT, "docs", "reference", "openapi.yaml");
diff --git a/scripts/check/check-public-creds.mjs b/scripts/check/check-public-creds.mjs
index b6cc521db6..f73c4a296c 100644
--- a/scripts/check/check-public-creds.mjs
+++ b/scripts/check/check-public-creds.mjs
@@ -81,15 +81,15 @@ const ENV_KEY_RE = /(clientId|clientSecret|apiKey)Env\s*:/;
//
// 6A.8: Expanded scope to open-sse/** + src/lib/oauth/**. Newly discovered FPs:
//
-// open-sse/services/usage.ts L582: `getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn")`
+// open-sse/services/usage.ts L499: `getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn")`
// The CRED_KEY_RE matches `apiKey:` in the TypeScript function-parameter type annotation.
// "minimax" and "minimax-cn" are provider-name strings in the type annotation, NOT credentials.
// This is a false positive (the gate was designed for object-literal assignments, not fn params).
// TODO(6A.8): Consider tightening CRED_KEY_RE to exclude function-signature contexts — but
// that adds complexity; the FP rate is low (1 file). Frozen by file:line:value key.
export const KNOWN_LITERAL_CREDS = new Set([
- "open-sse/services/usage.ts:582:minimax", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (moved 543→547 by #3838/#4293, then 547→582 by the v3.8.33 usage.ts growth)
- "open-sse/services/usage.ts:582:minimax-cn", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (moved 543→547 by #3838/#4293, then 547→582 by the v3.8.33 usage.ts growth)
+ "open-sse/services/usage.ts:499:minimax", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (moved 582→499 by the OpenCode/Ollama usage extraction)
+ "open-sse/services/usage.ts:499:minimax-cn", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (moved 582→499 by the OpenCode/Ollama usage extraction)
]);
/**
diff --git a/scripts/cli/generate-api-commands.mjs b/scripts/cli/generate-api-commands.mjs
index 43bd10efad..ef4ce7d3a3 100644
--- a/scripts/cli/generate-api-commands.mjs
+++ b/scripts/cli/generate-api-commands.mjs
@@ -6,7 +6,7 @@
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
-import yaml from "js-yaml";
+import * as yaml from "js-yaml";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..");
diff --git a/scripts/docs/gen-openapi-module.mjs b/scripts/docs/gen-openapi-module.mjs
index 9bccdfd635..785ee055c2 100644
--- a/scripts/docs/gen-openapi-module.mjs
+++ b/scripts/docs/gen-openapi-module.mjs
@@ -18,7 +18,7 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
-import yaml from "js-yaml";
+import * as yaml from "js-yaml";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
diff --git a/scripts/quality/collect-metrics.mjs b/scripts/quality/collect-metrics.mjs
index 2807409831..7baa98fb65 100644
--- a/scripts/quality/collect-metrics.mjs
+++ b/scripts/quality/collect-metrics.mjs
@@ -10,7 +10,7 @@ import { promises as fsAsync } from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { execFileSync } from "node:child_process";
-import yaml from "js-yaml";
+import * as yaml from "js-yaml";
const cwd = process.cwd();
const out = {};
diff --git a/scripts/quality/validate-release-green.mjs b/scripts/quality/validate-release-green.mjs
new file mode 100644
index 0000000000..477cacea4c
--- /dev/null
+++ b/scripts/quality/validate-release-green.mjs
@@ -0,0 +1,250 @@
+#!/usr/bin/env node
+// scripts/quality/validate-release-green.mjs
+//
+// "Release-green" pre-flight validator (Solution C).
+//
+// WHY: the full gate (ci.yml — unit shards, vitest, ratchets, package-artifact)
+// runs ONLY on the release PR (PR → main). PRs into release/** only get the
+// fast-gates (quality.yml: TIA-impacted tests + typecheck + lint checks). So
+// reds accumulate silently on the release branch and explode — in layers — at
+// release time. This script reproduces the release-equivalent validation against
+// the CURRENT working tree so the maintainer (or the nightly, Solution D) can see
+// the real state of the release branch at any time.
+//
+// DESIGN — never blocking to contributors:
+// • HARD checks (typecheck, lint errors, unit, vitest, db-rules, public-creds,
+// optionally package-artifact) → a failure here is a real defect; exit 1.
+// • DRIFT checks (eslint WARNINGS, cognitive-complexity, file-size) → ratchet
+// drift accrued across the cycle is NOT a contributor's fault; it is reported
+// and rebaselined by the maintainer at release. Drift NEVER changes the exit
+// code, so wiring this as a check can never block anyone on drift.
+//
+// This script DIAGNOSES + REPORTS only (no auto-fix). The fix-to-green
+// orchestration lives in the (future) /green-prs + review-prs flows that call it.
+//
+// Usage:
+// node scripts/quality/validate-release-green.mjs [--json] [--with-build] [--quick]
+// --json emit machine-readable JSON to stdout (report goes to stderr)
+// --with-build also run check:pack-artifact (needs a dist/ build — slow)
+// --quick skip the slow unit + vitest suites (drift + typecheck + lint only)
+
+import { execFileSync } from "node:child_process";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const ROOT = join(__dirname, "..", "..");
+const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
+
+// ─── Pure helpers (exported for tests) ──────────────────────────────────────
+
+/** Read the committed ratchet baseline value for a metric (null if unknown). */
+export function baselineValue(metric, root = ROOT) {
+ try {
+ const raw = JSON.parse(readFileSync(join(root, "config/quality/quality-baseline.json"), "utf8"));
+ const metrics = raw.metrics || raw;
+ const v = metrics?.[metric]?.value;
+ return typeof v === "number" ? v : null;
+ } catch {
+ return null;
+ }
+}
+
+/** Best-effort "first meaningful failure line" from captured command output. */
+export function firstFailureLine(out) {
+ const lines = String(out || "")
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean);
+ const hit = lines.find((l) => /✖|not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l));
+ return (hit || lines[lines.length - 1] || "failed").slice(0, 200);
+}
+
+/** Sum {errorCount,warningCount} across an eslint --format json result array. */
+export function eslintCounts(parsed) {
+ let errors = 0;
+ let warnings = 0;
+ for (const f of parsed || []) {
+ errors += f.errorCount || 0;
+ warnings += f.warningCount || 0;
+ }
+ return { errors, warnings };
+}
+
+/** Parse the eslint JSON array out of mixed stdout (tolerates a leading banner). */
+export function parseEslintJson(out) {
+ const start = String(out || "").indexOf("[");
+ if (start < 0) return null;
+ try {
+ return JSON.parse(String(out).slice(start));
+ } catch {
+ return null;
+ }
+}
+
+/** Pull the cognitive-complexity violation count from the gate's output. */
+export function parseCognitiveCount(out) {
+ const m = String(out || "").match(/(\d+)\s+(?:function\(s\) exceed|violações|violations)/i);
+ return m ? Number(m[1]) : null;
+}
+
+/**
+ * Drift verdict for a ratchet: a metric that grew past its committed baseline is
+ * "drift" (reported, never blocking). `direction:"down"` metrics (warnings,
+ * complexity, file-size counts) regress when current > baseline.
+ */
+export function isDrift(current, baseline) {
+ if (typeof current !== "number" || typeof baseline !== "number") return false;
+ return current > baseline;
+}
+
+/** releaseGreen iff there are zero failing HARD checks (drift never blocks). */
+export function computeVerdict(results) {
+ const hardFailures = results.filter((r) => r.kind === "hard" && !r.ok);
+ const drift = results.filter((r) => r.kind === "drift" && !r.ok);
+ return { releaseGreen: hardFailures.length === 0, hardFailures, drift };
+}
+
+// ─── Orchestration (only when run directly) ─────────────────────────────────
+
+function run(cmd, cmdArgs) {
+ try {
+ const out = execFileSync(cmd, cmdArgs, {
+ cwd: ROOT,
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "pipe"],
+ maxBuffer: 256 * 1024 * 1024,
+ env: { ...process.env, FORCE_COLOR: "0" },
+ });
+ return { code: 0, out };
+ } catch (err) {
+ return {
+ code: typeof err.status === "number" ? err.status : 1,
+ out: `${err.stdout || ""}${err.stderr || ""}`,
+ };
+ }
+}
+
+function main() {
+ const args = new Set(process.argv.slice(2));
+ const JSON_OUT = args.has("--json");
+ const WITH_BUILD = args.has("--with-build");
+ const QUICK = args.has("--quick");
+
+ const results = [];
+ const record = (r) => {
+ results.push(r);
+ const icon = r.ok ? "✅" : r.kind === "drift" ? "🟡" : "❌";
+ process.stderr.write(`${icon} [${r.kind}] ${r.label}${r.detail ? ` — ${r.detail}` : ""}\n`);
+ };
+
+ const hardCmd = (id, label, cmd, cmdArgs) => {
+ const { code, out } = run(cmd, cmdArgs);
+ record({ id, label, kind: "hard", ok: code === 0, detail: code === 0 ? "pass" : firstFailureLine(out) });
+ };
+
+ process.stderr.write("🔎 Release-green validation (current working tree)\n\n");
+
+ hardCmd("typecheck", "Typecheck (core)", npmCmd, ["run", "typecheck:core"]);
+
+ // ESLint: ONE pass → errors (hard) + warnings (drift)
+ {
+ const { out } = run("npx", ["eslint", ".", "--format", "json"]);
+ const parsed = parseEslintJson(out);
+ if (!parsed) {
+ record({ id: "lint", label: "ESLint", kind: "hard", ok: false, detail: "could not parse eslint json" });
+ } else {
+ const { errors, warnings } = eslintCounts(parsed);
+ record({ id: "lint-errors", label: "ESLint errors", kind: "hard", ok: errors === 0, detail: `${errors} error(s)` });
+ const base = baselineValue("eslintWarnings");
+ const over = isDrift(warnings, base);
+ record({
+ id: "eslint-warnings",
+ label: "ESLint warnings (ratchet)",
+ kind: "drift",
+ ok: !over,
+ detail:
+ base == null
+ ? `${warnings} (no baseline)`
+ : `${warnings} vs baseline ${base}${over ? ` (+${warnings - base} drift → rebaseline at release)` : ""}`,
+ });
+ }
+ }
+
+ hardCmd("db-rules", "DB rules", npmCmd, ["run", "check:db-rules"]);
+ hardCmd("public-creds", "Public creds", npmCmd, ["run", "check:public-creds"]);
+
+ // Cognitive-complexity (drift)
+ {
+ const { out } = run(npmCmd, ["run", "check:cognitive-complexity"]);
+ const current = parseCognitiveCount(out);
+ const base = baselineValue("cognitiveComplexity");
+ const over = isDrift(current, base);
+ record({
+ id: "cognitive-complexity",
+ label: "Cognitive complexity (ratchet)",
+ kind: "drift",
+ ok: !over,
+ detail:
+ current == null
+ ? "could not parse count"
+ : `${current} vs baseline ${base}${over ? ` (+${current - base} drift → rebaseline at release)` : ""}`,
+ });
+ }
+
+ // file-size (drift)
+ {
+ const { code, out } = run(npmCmd, ["run", "check:file-size"]);
+ record({
+ id: "file-size",
+ label: "File-size ratchet",
+ kind: "drift",
+ ok: code === 0,
+ detail: code === 0 ? "within frozen caps" : firstFailureLine(out),
+ });
+ }
+
+ if (!QUICK) {
+ hardCmd("unit", "Unit tests (full, CI concurrency)", npmCmd, ["run", "test:unit:ci"]);
+ hardCmd("vitest", "Vitest (MCP / autoCombo / cache)", npmCmd, ["run", "test:vitest"]);
+ }
+ if (WITH_BUILD) {
+ hardCmd("pack-artifact", "Package artifact (npm pack policy)", npmCmd, ["run", "check:pack-artifact"]);
+ }
+
+ const { releaseGreen, hardFailures, drift } = computeVerdict(results);
+
+ process.stderr.write("\n──────── verdict ────────\n");
+ process.stderr.write(`HARD failures (block — real defects): ${hardFailures.length}\n`);
+ hardFailures.forEach((r) => process.stderr.write(` ❌ ${r.label}: ${r.detail}\n`));
+ process.stderr.write(`Ratchet drift (non-blocking — rebaseline at release): ${drift.length}\n`);
+ drift.forEach((r) => process.stderr.write(` 🟡 ${r.label}: ${r.detail}\n`));
+ process.stderr.write(
+ releaseGreen
+ ? "\n✅ RELEASE-GREEN (no hard failures). Any drift above is rebaselined at release, not a contributor concern.\n"
+ : "\n❌ NOT release-green — hard failures must be fixed (in the originating PR branch, via co-authorship).\n"
+ );
+
+ if (JSON_OUT) {
+ process.stdout.write(
+ JSON.stringify(
+ {
+ releaseGreen,
+ hardFailures: hardFailures.map((r) => ({ id: r.id, label: r.label, detail: r.detail })),
+ drift: drift.map((r) => ({ id: r.id, label: r.label, detail: r.detail })),
+ checks: results.map((r) => ({ id: r.id, kind: r.kind, ok: r.ok, detail: r.detail })),
+ },
+ null,
+ 2
+ ) + "\n"
+ );
+ }
+
+ process.exit(releaseGreen ? 0 : 1);
+}
+
+// Run only when invoked directly (so tests can import the pure helpers).
+if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
+ main();
+}
diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx
index f6d3fa56bb..1461f2215b 100644
--- a/src/app/(dashboard)/dashboard/HomePageClient.tsx
+++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx
@@ -13,10 +13,8 @@ import { useNotificationStore } from "@/store/notificationStore";
import { copyToClipboard } from "@/shared/utils/clipboard";
import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel";
import { useIsElectron, useOpenExternal } from "@/shared/hooks/useElectron";
-import { useLiveRequests } from "@/hooks/useLiveDashboard";
-import { selectActiveRequests } from "../home/topologyUtils";
+import { HomeProviderTopologySection } from "./HomeProviderTopologySection";
-const ProviderTopology = dynamic(() => import("../home/ProviderTopology"), { ssr: false });
const ProviderQuotaWidget = dynamic(() => import("../home/ProviderQuotaWidget"), { ssr: false });
import type { NewsAnnouncement } from "@/shared/utils/releaseNotes";
@@ -123,7 +121,8 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
const [updating, setUpdating] = useState(false);
// Platform detection and download links for Electron
- const platform = typeof globalThis.window === "undefined" ? undefined : globalThis.window.electronAPI?.platform;
+ const platform =
+ typeof globalThis.window === "undefined" ? undefined : globalThis.window.electronAPI?.platform;
const electronDownload = useMemo(() => {
const latest = versionInfo?.latest || "";
const cleanLatest = latest.replace(/^v/, "");
@@ -171,7 +170,8 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
}>({ status: "idle" });
useEffect(() => {
- if (!isElectron || typeof globalThis.window === "undefined" || !globalThis.window.electronAPI) return;
+ if (!isElectron || typeof globalThis.window === "undefined" || !globalThis.window.electronAPI)
+ return;
// Trigger initial check silently on mount
globalThis.window.electronAPI.checkForUpdates().catch((err: any) => {
@@ -196,9 +196,12 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
// Appearance settings for home page pinning
const [pinProviderQuotaToHome, setPinProviderQuotaToHome] = useState(false);
const [showQuickStartOnHome, setShowQuickStartOnHome] = useState(true); // default on
- const [showProviderTopologyOnHome, setShowProviderTopologyOnHome] = useState(true); // default on
+ // #4596: default hidden until appearance settings load, so the live-WS
+ // topology connection is never opened before we know the user wants it.
+ const [showProviderTopologyOnHome, setShowProviderTopologyOnHome] = useState(false);
const [autoRefreshProviderQuota, setAutoRefreshProviderQuota] = useState(false);
const [autoRefreshProviderQuotaInterval, setAutoRefreshProviderQuotaInterval] = useState(180);
+ const [appearanceSettingsLoaded, setAppearanceSettingsLoaded] = useState(false);
useEffect(() => {
// Fetch the pin settings (lightweight)
@@ -225,6 +228,9 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
})
.catch(() => {
/* ignore — defaults stay */
+ })
+ .finally(() => {
+ setAppearanceSettingsLoaded(true);
});
}, []);
@@ -236,10 +242,9 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
const fetchData = useCallback(async () => {
try {
- const [provRes, modelsRes, metricsRes, versionRes] = await Promise.all([
+ const [provRes, modelsRes, versionRes] = await Promise.all([
fetch("/api/providers"),
fetch("/api/models"),
- fetch("/api/provider-metrics"),
fetch("/api/system/version"),
]);
if (provRes.ok) {
@@ -250,10 +255,6 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
const modelsData = await modelsRes.json();
setModels(modelsData.models || []);
}
- if (metricsRes.ok) {
- const metricsData = await metricsRes.json();
- setProviderMetrics(metricsData.metrics || {});
- }
if (versionRes.ok) {
const versionData = await versionRes.json();
setVersionInfo(versionData);
@@ -278,6 +279,10 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
}, []);
useEffect(() => {
+ if (!appearanceSettingsLoaded || !showProviderTopologyOnHome) {
+ return;
+ }
+
let cancelled = false;
let timeoutId: ReturnType | null = null;
let controller: AbortController | null = null;
@@ -317,7 +322,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
if (timeoutId) clearTimeout(timeoutId);
controller?.abort();
};
- }, []);
+ }, [appearanceSettingsLoaded, showProviderTopologyOnHome]);
// T07: Check for unhealthy API keys and show notification (once per session)
const notifiedUnhealthyKeys = useRef>(new Set());
@@ -1061,9 +1066,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
{pinProviderQuotaToHome && (
}>
)}
@@ -1159,35 +1162,13 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
)}
- {/* Provider Topology (controlled by Appearance setting, default on) */}
{showProviderTopologyOnHome && (
-
-
-
-
{t("providerTopology")}
-
- Connected providers routing through OmniRoute in real time
-
-
-
-
- Active
-
-
- Recent
-
-
- Error
-
-
-
-
-
+
)}
{/* Provider Models Modal */}
diff --git a/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx b/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx
new file mode 100644
index 0000000000..53abbbeed2
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import dynamic from "next/dynamic";
+
+import { Card } from "@/shared/components";
+import { useLiveRequests } from "@/hooks/useLiveDashboard";
+import { selectActiveRequests } from "../home/topologyUtils";
+
+const ProviderTopology = dynamic(() => import("../home/ProviderTopology"), { ssr: false });
+
+type TopologyProvider = {
+ id: string;
+ provider: string;
+ name?: string;
+};
+
+export function HomeProviderTopologySection({
+ providers,
+ lastProvider,
+ errorProvider,
+ enabled = true,
+}: {
+ providers: TopologyProvider[];
+ lastProvider: string;
+ errorProvider: string;
+ enabled?: boolean;
+}) {
+ const t = useTranslations("home");
+ // #4596: gate the live-WS connection so it only opens while the topology
+ // section is actually shown on the home page.
+ const { activeRequests: liveActiveRequests } = useLiveRequests({ enabled });
+
+ return (
+
+
+
+
{t("providerTopology")}
+
+ Connected providers routing through OmniRoute in real time
+
+
+
+
+ Active
+
+
+ Recent
+
+
+ Error
+
+
+
+
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.ts b/src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.ts
index 7530b48d11..39474592f0 100644
--- a/src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.ts
+++ b/src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.ts
@@ -111,6 +111,7 @@ export function formatUsdCost(value: number, locale: string): string {
*/
export function maskKey(fullKey: string | null | undefined): string {
if (!fullKey) return "";
+ if (fullKey.includes("****")) return fullKey;
return fullKey.length > 8 ? `${fullKey.slice(0, 8)}...` : fullKey;
}
diff --git a/src/app/(dashboard)/dashboard/cli-code/components/OpenClawToolCard.tsx b/src/app/(dashboard)/dashboard/cli-code/components/OpenClawToolCard.tsx
index ff023a2f31..c817d72c4c 100644
--- a/src/app/(dashboard)/dashboard/cli-code/components/OpenClawToolCard.tsx
+++ b/src/app/(dashboard)/dashboard/cli-code/components/OpenClawToolCard.tsx
@@ -339,6 +339,20 @@ export default function OpenClawToolCard({
: t("installCliPrompt", { tool: "Open Claw" })}
+ {/*
+ Always surface Manual Config even when the CLI is not
+ detected locally — typical of remote OmniRoute
+ deployments where the CLI lives on the user's machine,
+ not on the server. Upstream report: #579.
+ */}
+
)}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx
index 1e44881b18..7f0447d4db 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx
@@ -12,8 +12,6 @@ import {
getProviderBaseUrlHint,
getProviderBaseUrlPlaceholder,
isGlmProvider,
- parseRoutingTagsInput,
- parseExcludedModelsInput,
getWebSessionCredentialLabel,
getWebSessionCredentialHint,
getWebSessionCredentialCheckLabel,
@@ -28,7 +26,8 @@ import { getWebSessionCredentialRequirement } from "../../webSessionCredentials"
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
-import { assignCcCompatibleRequestDefaults } from "./ccCompatibleRequestDefaults";
+import { buildAddProviderSpecificData } from "./connectionProviderSpecificData";
+import QuotaScrapingFields, { EMPTY_QUOTA_SCRAPING_FIELDS } from "./QuotaScrapingFields";
export interface AddApiKeyModalProps {
isOpen: boolean;
@@ -112,6 +111,7 @@ export default function AddApiKeyModal({
customUserAgent: "",
accountId: "",
consoleApiKey: "",
+ ...EMPTY_QUOTA_SCRAPING_FIELDS,
ccCompatibleContext1m: false,
ccCompatibleRedactThinking: false,
passthroughModels: false,
@@ -283,47 +283,27 @@ export default function AddApiKeyModal({
}
}
- const providerSpecificData: Record = {};
- if (formData.customUserAgent.trim()) {
- providerSpecificData.customUserAgent = formData.customUserAgent.trim();
- }
- openRouterPreset.applyTo(providerSpecificData);
- if (formData.routingTags.trim()) {
- providerSpecificData.tags = parseRoutingTagsInput(formData.routingTags);
- }
- if (formData.excludedModels.trim()) {
- providerSpecificData.excludedModels = parseExcludedModelsInput(formData.excludedModels);
- }
- if (formData.passthroughModels) {
- providerSpecificData.passthroughModels = true;
- }
- if (showFreeModelsToggle && formData.importFreeModelsOnly) {
- providerSpecificData.importFreeModelsOnly = true;
- }
- if (provider === "bailian-coding-plan" && formData.consoleApiKey.trim()) {
- providerSpecificData.consoleApiKey = formData.consoleApiKey.trim();
- }
- if (isGooglePse && formData.cx.trim()) {
- providerSpecificData.cx = formData.cx.trim();
- }
- if (usesBaseUrl) {
- providerSpecificData.baseUrl = validatedBaseUrl;
- } else if (showsRegion) {
- providerSpecificData.region = formData.region.trim() || defaultRegion;
- } else if (isGlm) {
- providerSpecificData.apiRegion = formData.apiRegion;
- } else if (isCloudflare && formData.accountId.trim()) {
- providerSpecificData.accountId = formData.accountId.trim();
- }
- if (isCcCompatible) assignCcCompatibleRequestDefaults(providerSpecificData, formData);
+ const providerSpecificData = buildAddProviderSpecificData({
+ provider,
+ formData,
+ openRouterPreset,
+ showFreeModelsToggle,
+ isGooglePse,
+ usesBaseUrl,
+ validatedBaseUrl,
+ showsRegion,
+ defaultRegion,
+ isGlm,
+ isCloudflare,
+ isCcCompatible,
+ });
const payload = {
name: formData.name,
apiKey: credentialInput.trim() || undefined,
priority: formData.priority,
testStatus: "active",
- providerSpecificData:
- Object.keys(providerSpecificData).length > 0 ? providerSpecificData : undefined,
+ providerSpecificData,
};
const error = await onSave(payload);
@@ -710,6 +690,12 @@ export default function AddApiKeyModal({
)}
{freeModelsToggle}
+ setFormData({ ...formData, ...patch })}
+ t={t}
+ />
{isCompatible && !isCcCompatible && (
{isAnthropic
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx
index 306a0caccd..049797e4df 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx
@@ -41,8 +41,11 @@ export default function EditCompatibleNodeModal({
});
const [saving, setSaving] = useState(false);
const [checkKey, setCheckKey] = useState("");
+ const [checkModelId, setCheckModelId] = useState("");
const [validating, setValidating] = useState(false);
- const [validationResult, setValidationResult] = useState(null);
+ const [validationResult, setValidationResult] = useState<
+ null | { valid: boolean; error?: string | null; method?: string | null }
+ >(null);
const [showAdvanced, setShowAdvanced] = useState(false);
useEffect(() => {
@@ -113,12 +116,17 @@ export default function EditCompatibleNodeModal({
compatMode: isCcCompatible ? "cc" : undefined,
chatPath: formData.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""),
modelsPath: isCcCompatible ? "" : formData.modelsPath,
+ modelId: checkModelId.trim() || undefined,
}),
});
const data = await res.json();
- setValidationResult(data.valid ? "success" : "failed");
+ setValidationResult({
+ valid: !!data.valid,
+ error: data.error ?? null,
+ method: data.method ?? null,
+ });
} catch {
- setValidationResult("failed");
+ setValidationResult({ valid: false, error: "Network error" });
} finally {
setValidating(false);
}
@@ -259,10 +267,26 @@ export default function EditCompatibleNodeModal({
+ setCheckModelId(e.target.value)}
+ placeholder={t("testModelIdPlaceholder")}
+ hint={t("testModelIdHint")}
+ />
{validationResult && (
-
- {validationResult === "success" ? t("valid") : t("invalid")}
-
+
+
+ {validationResult.valid ? t("valid") : t("invalid")}
+
+ {validationResult.error && (
+
+ {validationResult.error}
+
+ )}
+
)}
+ setFormData({ ...formData, ...patch })}
+ t={t}
+ editMode
+ />
{supportsGoogleProjectId && (
{isAntigravity && (
@@ -1020,10 +1013,9 @@ export default function EditConnectionModal({
return (
- {statusIcon} {t("primaryKey")}: {connection.apiKey.slice(0, 6)}...
- {connection.apiKey.slice(-4)}
+ {statusIcon} {t("primaryKey")}: {connection.apiKey}
{health && (
+) {
+ if (provider === "opencode-go") {
+ target.opencodeGoWorkspaceId = values.opencodeGoWorkspaceId.trim() || undefined;
+ if (values.opencodeGoAuthCookie.trim()) {
+ target.opencodeGoAuthCookie = values.opencodeGoAuthCookie.trim();
+ }
+ } else if (provider === "ollama-cloud" && values.ollamaCloudUsageCookie.trim()) {
+ target.ollamaCloudUsageCookie = values.ollamaCloudUsageCookie.trim();
+ }
+}
+
+type QuotaScrapingFieldsProps = {
+ provider?: string;
+ values: QuotaScrapingFieldValues;
+ onChange: (patch: Partial) => void;
+ t: ProviderMessageTranslator;
+ editMode?: boolean;
+};
+
+export default function QuotaScrapingFields({
+ provider,
+ values,
+ onChange,
+ t,
+ editMode = false,
+}: QuotaScrapingFieldsProps) {
+ if (provider === "opencode-go") {
+ return (
+
+ onChange({ opencodeGoWorkspaceId: e.target.value })}
+ placeholder="workspace_..."
+ hint={providerText(
+ t,
+ "opencodeGoWorkspaceIdHint",
+ "Required for quota scraping. Copy it from the OpenCode Go workspace URL."
+ )}
+ autoComplete="off"
+ spellCheck={false}
+ />
+ onChange({ opencodeGoAuthCookie: e.target.value })}
+ placeholder="auth=..."
+ hint={providerText(
+ t,
+ "opencodeGoAuthCookieHint",
+ editMode
+ ? "Leave blank to keep the stored cookie. Paste auth=... or only the cookie value to replace it."
+ : "Paste the auth cookie value from opencode.ai. The auth= prefix is accepted."
+ )}
+ autoComplete="off"
+ spellCheck={false}
+ autoCapitalize="off"
+ />
+
+ );
+ }
+
+ if (provider === "ollama-cloud") {
+ return (
+
+ onChange({ ollamaCloudUsageCookie: e.target.value })}
+ placeholder="__Secure-session=..."
+ hint={providerText(
+ t,
+ "ollamaCloudUsageCookieHint",
+ editMode
+ ? "Leave blank to keep the stored cookie. Paste the __Secure-session cookie value from ollama.com/settings to replace it."
+ : "Required for quota scraping. Paste the __Secure-session cookie value from ollama.com/settings."
+ )}
+ autoComplete="off"
+ spellCheck={false}
+ autoCapitalize="off"
+ />
+
+ );
+ }
+
+ return null;
+}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/__tests__/connModals.test.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/__tests__/connModals.test.tsx
index d74f01579f..24c3ce6efe 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/__tests__/connModals.test.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/__tests__/connModals.test.tsx
@@ -149,6 +149,31 @@ describe("conn-modals (Phase 1c extraction)", () => {
expect(c.querySelector("*")).not.toBeNull();
});
+ it("EditConnectionModal renders the API key value returned by the provider connection API", () => {
+ const onSave = vi.fn().mockResolvedValue(undefined);
+ const fullApiKey = "test-provider-key-full-1234567890abcdef78b9";
+ const connection = {
+ id: "conn-full-key",
+ name: "Test Connection",
+ provider: "openai",
+ authType: "apikey",
+ priority: 1,
+ apiKey: fullApiKey,
+ };
+ const c = renderModal(
+
+ );
+
+ expect(c.textContent).toContain(fullApiKey);
+ expect(c.textContent).not.toContain("test-p...78b9");
+ });
+
it("EditConnectionModal renders OpenRouter preset when provider comes from the page", () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const connection = {
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/connectionProviderSpecificData.ts b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/connectionProviderSpecificData.ts
new file mode 100644
index 0000000000..69abfc2f4b
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/connectionProviderSpecificData.ts
@@ -0,0 +1,125 @@
+import { parseExcludedModelsInput, parseRoutingTagsInput } from "../../providerPageHelpers";
+import {
+ assignCcCompatibleRequestDefaults,
+ mergeCcCompatibleRequestDefaults,
+} from "./ccCompatibleRequestDefaults";
+import {
+ assignQuotaScrapingProviderData,
+ type QuotaScrapingFieldValues,
+} from "./QuotaScrapingFields";
+
+type FormData = QuotaScrapingFieldValues & {
+ accountId: string;
+ apiRegion: string;
+ ccCompatibleContext1m: boolean;
+ ccCompatibleRedactThinking: boolean;
+ consoleApiKey: string;
+ customUserAgent: string;
+ cx: string;
+ excludedModels: string;
+ importFreeModelsOnly: boolean;
+ passthroughModels: boolean;
+ region: string;
+ routingTags: string;
+ tag?: string;
+ validationModelId?: string;
+};
+type ProviderSpecificData = Record;
+
+export function buildAddProviderSpecificData(options: {
+ provider?: string;
+ formData: FormData;
+ openRouterPreset: { applyTo: (target: ProviderSpecificData) => void };
+ showFreeModelsToggle: boolean;
+ isGooglePse: boolean;
+ usesBaseUrl: boolean;
+ validatedBaseUrl: string | null;
+ showsRegion: boolean;
+ defaultRegion: string;
+ isGlm: boolean;
+ isCloudflare: boolean;
+ isCcCompatible?: boolean;
+}) {
+ const {
+ provider,
+ formData,
+ openRouterPreset,
+ showFreeModelsToggle,
+ isGooglePse,
+ usesBaseUrl,
+ validatedBaseUrl,
+ showsRegion,
+ defaultRegion,
+ isGlm,
+ isCloudflare,
+ isCcCompatible,
+ } = options;
+ const data: ProviderSpecificData = {};
+ if (formData.customUserAgent.trim()) data.customUserAgent = formData.customUserAgent.trim();
+ openRouterPreset.applyTo(data);
+ if (formData.routingTags.trim()) data.tags = parseRoutingTagsInput(formData.routingTags);
+ if (formData.excludedModels.trim()) {
+ data.excludedModels = parseExcludedModelsInput(formData.excludedModels);
+ }
+ if (formData.passthroughModels) data.passthroughModels = true;
+ if (showFreeModelsToggle && formData.importFreeModelsOnly) data.importFreeModelsOnly = true;
+ if (provider === "bailian-coding-plan" && formData.consoleApiKey.trim()) {
+ data.consoleApiKey = formData.consoleApiKey.trim();
+ }
+ assignQuotaScrapingProviderData(provider, formData, data);
+ if (isGooglePse && formData.cx.trim()) data.cx = formData.cx.trim();
+ if (usesBaseUrl) data.baseUrl = validatedBaseUrl;
+ else if (showsRegion) data.region = formData.region.trim() || defaultRegion;
+ else if (isGlm) data.apiRegion = formData.apiRegion;
+ else if (isCloudflare && formData.accountId.trim()) data.accountId = formData.accountId.trim();
+ if (isCcCompatible) assignCcCompatibleRequestDefaults(data, formData);
+ return Object.keys(data).length > 0 ? data : undefined;
+}
+
+export function assignEditApiKeyProviderSpecificData(options: {
+ provider: string;
+ formData: FormData;
+ target: ProviderSpecificData;
+ extraApiKeys: string[];
+ openRouterPreset: { getPatch: () => ProviderSpecificData };
+ usesBaseUrl: boolean;
+ validatedBaseUrl: string | null;
+ showsRegion: boolean;
+ defaultRegion: string;
+ isGlm: boolean;
+ isCloudflare: boolean;
+ supportsGoogleProjectId: boolean;
+ trimmedCloudCodeProjectId: string;
+ isGooglePse: boolean;
+ isCcCompatible: boolean;
+}) {
+ const o = options;
+ Object.assign(o.target, {
+ extraApiKeys: o.extraApiKeys.filter((key) => key.trim().length > 0),
+ tag: o.formData.tag.trim() || undefined,
+ tags: parseRoutingTagsInput(o.formData.routingTags),
+ excludedModels: parseExcludedModelsInput(o.formData.excludedModels),
+ customUserAgent: o.formData.customUserAgent.trim(),
+ ...o.openRouterPreset.getPatch(),
+ ...(o.formData.passthroughModels ? { passthroughModels: true } : {}),
+ });
+ if (o.provider === "bailian-coding-plan") {
+ o.target.consoleApiKey = o.formData.consoleApiKey.trim() || undefined;
+ }
+ assignQuotaScrapingProviderData(o.provider, o.formData, o.target);
+ if (o.formData.validationModelId) o.target.validationModelId = o.formData.validationModelId;
+ if (o.isGooglePse) o.target.cx = o.formData.cx.trim() || undefined;
+ if (o.usesBaseUrl) o.target.baseUrl = o.validatedBaseUrl;
+ else if (o.showsRegion) o.target.region = o.formData.region.trim() || o.defaultRegion;
+ else if (o.isGlm) o.target.apiRegion = o.formData.apiRegion;
+ else if (o.isCloudflare && o.formData.accountId.trim()) {
+ o.target.accountId = o.formData.accountId.trim();
+ }
+ if (o.supportsGoogleProjectId) o.target.projectId = o.trimmedCloudCodeProjectId || null;
+ if (o.isCcCompatible) {
+ o.target.requestDefaults = mergeCcCompatibleRequestDefaults(
+ o.target.requestDefaults,
+ o.formData
+ );
+ }
+}
diff --git a/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx b/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx
index 8b0b10a8cd..5eecba769e 100644
--- a/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx
+++ b/src/app/(dashboard)/dashboard/providers/components/AddCompatibleProviderModal.tsx
@@ -90,8 +90,11 @@ export default function AddCompatibleProviderModal({
const [formData, setFormData] = useState(() => createInitialForm(mode));
const [submitting, setSubmitting] = useState(false);
const [checkKey, setCheckKey] = useState("");
+ const [checkModelId, setCheckModelId] = useState("");
const [validating, setValidating] = useState(false);
- const [validationResult, setValidationResult] = useState<"success" | "failed" | null>(null);
+ const [validationResult, setValidationResult] = useState<
+ null | { valid: boolean; error?: string | null; method?: string | null }
+ >(null);
const [showAdvanced, setShowAdvanced] = useState(false);
const apiTypeOptions = useMemo(
@@ -209,6 +212,8 @@ export default function AddCompatibleProviderModal({
body.compatMode = defaults.compatMode;
body.chatPath = formData.chatPath || CC_DEFAULT_CHAT_PATH;
}
+ const trimmedModelId = checkModelId.trim();
+ if (trimmedModelId) body.modelId = trimmedModelId;
const res = await fetch("/api/provider-nodes/validate", {
method: "POST",
@@ -216,9 +221,13 @@ export default function AddCompatibleProviderModal({
body: JSON.stringify(body),
});
const data = await res.json();
- setValidationResult(data.valid ? "success" : "failed");
+ setValidationResult({
+ valid: !!data.valid,
+ error: data.error ?? null,
+ method: data.method ?? null,
+ });
} catch {
- setValidationResult("failed");
+ setValidationResult({ valid: false, error: "Network error" });
} finally {
setValidating(false);
}
@@ -322,10 +331,26 @@ export default function AddCompatibleProviderModal({
+ setCheckModelId(e.target.value)}
+ placeholder={t("testModelIdPlaceholder")}
+ hint={t("testModelIdHint")}
+ />
{validationResult && (
-
- {validationResult === "success" ? t("valid") : t("invalid")}
-
+
+
+ {validationResult.valid ? t("valid") : t("invalid")}
+
+ {validationResult.error && (
+
+ {validationResult.error}
+
+ )}
+
)}
diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx
index 731457bfb5..8a2567af1b 100644
--- a/src/app/(dashboard)/dashboard/providers/page.tsx
+++ b/src/app/(dashboard)/dashboard/providers/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState, useEffect, useCallback, useMemo } from "react";
import { Card, CardSkeleton, Badge, Button, CollapsibleSection } from "@/shared/components";
import {
AGGREGATOR_PROVIDER_IDS,
@@ -9,7 +9,6 @@ import {
IDE_PROVIDER_IDS,
IMAGE_ONLY_PROVIDER_IDS,
VIDEO_PROVIDER_IDS,
- isClaudeCodeCompatibleProvider,
} from "@/shared/constants/providers";
import { useRouter, useSearchParams } from "next/navigation";
import { getErrorCode, getRelativeTime } from "@/shared/utils";
@@ -19,6 +18,7 @@ import { useNotificationStore } from "@/store/notificationStore";
import { useTranslations } from "next-intl";
import {
buildStaticProviderEntries,
+ buildCompatibleProviderGroups,
filterConfiguredProviderEntries,
shouldFilterProviderEntriesForDisplayMode,
shouldShowFirstProviderHint,
@@ -480,37 +480,18 @@ export default function ProvidersPage() {
}
};
- const compatibleProviders = providerNodes
- .filter((node) => node.type === "openai-compatible")
- .map((node) => ({
- id: node.id,
- name: node.name || t("openaiCompatibleName"),
- color: "#10A37F",
- textIcon: "OC",
- apiType: node.apiType,
- }));
-
- const anthropicCompatibleProviders = providerNodes
- .filter(
- (node) => node.type === "anthropic-compatible" && !isClaudeCodeCompatibleProvider(node.id)
- )
- .map((node) => ({
- id: node.id,
- name: node.name || t("anthropicCompatibleName"),
- color: "#D97757",
- textIcon: "AC",
- }));
-
- const ccCompatibleProviders = providerNodes
- .filter(
- (node) => node.type === "anthropic-compatible" && isClaudeCodeCompatibleProvider(node.id)
- )
- .map((node) => ({
- id: node.id,
- name: node.name || ccCompatibleLabel,
- color: "#B45309",
- textIcon: "CC",
- }));
+ const compatibleProviderGroups = useMemo(
+ () =>
+ buildCompatibleProviderGroups(providerNodes, {
+ openaiCompatibleName: t("openaiCompatibleName"),
+ anthropicCompatibleName: t("anthropicCompatibleName"),
+ claudeCodeCompatibleName: ccCompatibleLabel,
+ }),
+ [ccCompatibleLabel, providerNodes, t]
+ );
+ const compatibleProviders = compatibleProviderGroups.openai;
+ const anthropicCompatibleProviders = compatibleProviderGroups.anthropic;
+ const ccCompatibleProviders = compatibleProviderGroups.claudeCode;
const effectiveProviderDisplayMode =
providerDisplayMode === "configured" && connections.length === 0 ? "all" : providerDisplayMode;
@@ -530,7 +511,7 @@ export default function ProvidersPage() {
activeServiceKind
);
- const blockedProviderSet = new Set(blockedProviders);
+ const blockedProviderSet = useMemo(() => new Set(blockedProviders), [blockedProviders]);
const rawNoAuthEntriesAll = buildStaticProviderEntries("no-auth", getProviderStats);
const noAuthEntriesAll = rawNoAuthEntriesAll.filter(({ providerId, provider }) => {
const alias = typeof provider.alias === "string" ? provider.alias : null;
@@ -970,8 +951,8 @@ export default function ProvidersPage() {
}`}
title={t("testAllCompatible")}
>
-
- {testingMode === "compatible" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "compatible" ? t("testing") : t("testAll")}
@@ -1065,8 +1046,8 @@ export default function ProvidersPage() {
title={t("testAllOAuth")}
aria-label={t("testAllOAuth")}
>
-
- {testingMode === "oauth" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "oauth" ? t("testing") : t("testAll")}
@@ -1115,8 +1096,8 @@ export default function ProvidersPage() {
title={t("testAll")}
aria-label={t("testAll")}
>
-
- {testingMode === "ide" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "ide" ? t("testing") : t("testAll")}
@@ -1172,8 +1153,8 @@ export default function ProvidersPage() {
}`}
title={t("testAll")}
>
-
- {testingMode === "web-cookie" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "web-cookie" ? t("testing") : t("testAll")}
@@ -1216,8 +1197,8 @@ export default function ProvidersPage() {
}`}
title={t("testAll")}
>
-
- {testingMode === "free" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "free" ? t("testing") : t("testAll")}
@@ -1261,8 +1242,8 @@ export default function ProvidersPage() {
title={t("testAllApiKey")}
aria-label={t("testAllApiKey")}
>
-
- {testingMode === "apikey" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "apikey" ? t("testing") : t("testAll")}
@@ -1313,8 +1294,8 @@ export default function ProvidersPage() {
}`}
title={t("testAll")}
>
-
- {testingMode === "no-auth" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "no-auth" ? t("testing") : t("testAll")}
@@ -1357,8 +1338,8 @@ export default function ProvidersPage() {
}`}
title={t("testAll")}
>
-
- {testingMode === "upstream-proxy" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "upstream-proxy" ? t("testing") : t("testAll")}
@@ -1500,8 +1481,8 @@ export default function ProvidersPage() {
}`}
title={t("testAll")}
>
-
- {testingMode === "cloud-agent" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "cloud-agent" ? t("testing") : t("testAll")}
@@ -1548,8 +1529,8 @@ export default function ProvidersPage() {
}`}
title={t("testAll")}
>
-
- {testingMode === "local" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "local" ? t("testing") : t("testAll")}
@@ -1592,8 +1573,8 @@ export default function ProvidersPage() {
}`}
title={t("testAll")}
>
-
- {testingMode === "search" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "search" ? t("testing") : t("testAll")}
@@ -1702,8 +1683,8 @@ export default function ProvidersPage() {
}`}
title={t("testAll")}
>
-
- {testingMode === "audio" ? "sync" : "play_arrow"}
+
+ play_arrow
{testingMode === "audio" ? t("testing") : t("testAll")}
diff --git a/src/app/(dashboard)/dashboard/providers/providerPageUtils.ts b/src/app/(dashboard)/dashboard/providers/providerPageUtils.ts
index f7a6b4d739..32da435927 100644
--- a/src/app/(dashboard)/dashboard/providers/providerPageUtils.ts
+++ b/src/app/(dashboard)/dashboard/providers/providerPageUtils.ts
@@ -7,6 +7,7 @@ import {
type ResolvedProviderCatalogEntry,
type StaticProviderCatalogCategory,
} from "@/lib/providers/catalog";
+import { isClaudeCodeCompatibleProvider } from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { providerHasServiceKind } from "@/lib/providers/serviceKindIndex";
import { compareTr, matchesSearch } from "@/shared/utils/turkishText";
@@ -25,6 +26,20 @@ export interface ProviderEntry> {
toggleAuthType: "oauth" | "free" | "apikey" | "no-auth";
}
+export type CompatibleProviderInfo = {
+ id: string;
+ name: string;
+ color: string;
+ textIcon: string;
+ apiType?: string;
+};
+
+export type CompatibleProviderGroups = {
+ openai: CompatibleProviderInfo[];
+ anthropic: CompatibleProviderInfo[];
+ claudeCode: CompatibleProviderInfo[];
+};
+
export function shouldApplyConfiguredOnlyFilter(
showConfiguredOnly: boolean,
connectionCount: number
@@ -110,6 +125,53 @@ export function buildStaticProviderEntries(
);
}
+export function buildCompatibleProviderGroups(
+ providerNodes: Array<{ id: string; name?: string; type?: string; apiType?: string }>,
+ labels: {
+ openaiCompatibleName: string;
+ anthropicCompatibleName: string;
+ claudeCodeCompatibleName: string;
+ }
+): CompatibleProviderGroups {
+ const openai: CompatibleProviderInfo[] = [];
+ const anthropic: CompatibleProviderInfo[] = [];
+ const claudeCode: CompatibleProviderInfo[] = [];
+
+ for (const node of providerNodes) {
+ if (node.type === "openai-compatible") {
+ openai.push({
+ id: node.id,
+ name: node.name || labels.openaiCompatibleName,
+ color: "#10A37F",
+ textIcon: "OC",
+ apiType: node.apiType,
+ });
+ continue;
+ }
+
+ if (node.type !== "anthropic-compatible") continue;
+
+ if (isClaudeCodeCompatibleProvider(node.id)) {
+ claudeCode.push({
+ id: node.id,
+ name: node.name || labels.claudeCodeCompatibleName,
+ color: "#B45309",
+ textIcon: "CC",
+ });
+ continue;
+ }
+
+ anthropic.push({
+ id: node.id,
+ name: node.name || labels.anthropicCompatibleName,
+ color: "#D97757",
+ textIcon: "AC",
+ });
+ }
+
+ return { openai, anthropic, claudeCode };
+}
+
export function filterConfiguredProviderEntries(
entries: ProviderEntry[],
showConfiguredOnly: boolean,
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx
index c09bb79c18..9c42bb0647 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx
@@ -91,6 +91,7 @@ export default function QuotaCard({
quotas={quotas}
loading={loading}
error={error}
+ message={quota?.message ?? null}
refreshedAt={displayRefreshedAt}
hasStaleData={hasStaleData}
onRefresh={onRefresh}
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/formatters.ts b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/formatters.ts
new file mode 100644
index 0000000000..d5eaf86caa
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/formatters.ts
@@ -0,0 +1,6 @@
+export function formatAutoRefreshCountdown(ms: number): string {
+ const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+}
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx
index fcd9f47316..7176942e8a 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx
@@ -21,6 +21,8 @@ import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import { useNotificationStore } from "@/store/notificationStore";
import QuotaCutoffModal from "./QuotaCutoffModal";
import QuotaCardGrid from "./QuotaCardGrid";
+import { useVisibleQuotaData } from "./useVisibleQuotaData";
+import { formatAutoRefreshCountdown } from "./formatters";
import { translateUsageOrFallback, type UsageTranslationValues } from "./i18nFallback";
import { compareTr } from "@/shared/utils/turkishText";
@@ -33,7 +35,6 @@ const MIN_FETCH_INTERVAL_MS = 30000;
const QUOTA_BAR_GREEN_THRESHOLD = 50;
const QUOTA_BAR_YELLOW_THRESHOLD = 20;
-// Display label per known provider; the icon is resolved by ProviderIcon.
const PROVIDER_LABEL: Record = {
antigravity: "Antigravity",
"gemini-cli": "Gemini CLI",
@@ -46,6 +47,7 @@ const PROVIDER_LABEL: Record = {
zai: "Z.AI",
glmt: "GLM Thinking",
"opencode-go": "OpenCode Go",
+ "ollama-cloud": "Ollama Cloud",
"kimi-coding": "Kimi Coding",
minimax: "MiniMax",
"minimax-cn": "MiniMax CN",
@@ -53,8 +55,7 @@ const PROVIDER_LABEL: Record = {
deepseek: "DeepSeek",
};
-// Group ordering — single source of truth for "where does Codex sit
-// relative to Antigravity on the page".
+// Group ordering — single source of truth for provider placement.
const PROVIDER_ORDER: Record = {
antigravity: 1,
"gemini-cli": 2,
@@ -66,10 +67,11 @@ const PROVIDER_ORDER: Record = {
zai: 8,
glmt: 9,
"opencode-go": 10,
- "kimi-coding": 11,
- minimax: 12,
- "minimax-cn": 13,
- nanogpt: 14,
+ "ollama-cloud": 11,
+ "kimi-coding": 12,
+ minimax: 13,
+ "minimax-cn": 14,
+ nanogpt: 15,
};
const TIER_FILTERS = [
@@ -213,13 +215,6 @@ function aggregateWorst(statuses: StatusKey[]): "critical" | "alert" | "ok" | "e
return worst;
}
-function formatAutoRefreshCountdown(ms: number): string {
- const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
- const minutes = Math.floor(totalSeconds / 60);
- const seconds = totalSeconds % 60;
- return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
-}
-
interface ProviderLimitsProps {
showFilters?: boolean;
autoRefreshInterval?: number;
@@ -564,6 +559,7 @@ export default function ProviderLimits({
(a, b) => (PROVIDER_ORDER[a.provider] || 99) - (PROVIDER_ORDER[b.provider] || 99)
);
}, [filteredConnections]);
+ const visibleQuotaData = useVisibleQuotaData(sortedConnections, quotaData);
const resolvedPlanByConnection = useMemo(() => {
const out: Record = {};
@@ -613,10 +609,10 @@ export default function ProviderLimits({
const statusByConnection = useMemo(() => {
const out: Record = {};
for (const conn of sortedConnections) {
- out[conn.id] = getWorstStatus(quotaData[conn.id]?.quotas);
+ out[conn.id] = getWorstStatus(visibleQuotaData[conn.id]?.quotas);
}
return out;
- }, [sortedConnections, quotaData]);
+ }, [sortedConnections, visibleQuotaData]);
const purchaseTypeCounts = useMemo(() => {
const counts: Record = {
@@ -697,8 +693,8 @@ export default function ProviderLimits({
const sa = statusRank[statusByConnection[a.id] || "empty"];
const sb = statusRank[statusByConnection[b.id] || "empty"];
if (sa !== sb) return sa - sb;
- const ra = getSoonestResetMs(quotaData[a.id]?.quotas);
- const rb = getSoonestResetMs(quotaData[b.id]?.quotas);
+ const ra = getSoonestResetMs(visibleQuotaData[a.id]?.quotas);
+ const rb = getSoonestResetMs(visibleQuotaData[b.id]?.quotas);
return ra - rb;
});
}, [
@@ -711,7 +707,7 @@ export default function ProviderLimits({
statusByConnection,
envFilter,
providerFilter,
- quotaData,
+ visibleQuotaData,
]);
// Distinct provider keys present in the current connection set (after the
@@ -1055,7 +1051,7 @@ export default function ProviderLimits({
renderInlineQuotaSummary(quota.quotas)}
onRefresh={refreshProvider}
onOpenCutoff={(conn) => {
- const windows = (quotaData[conn.id]?.quotas || []).filter(
+ const windows = (visibleQuotaData[conn.id]?.quotas || []).filter(
(q: any) => q && typeof q.name === "string" && !q.isCredits
);
setCutoffModalWindows(windows);
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx
index 2b6c2511cf..47641ec896 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx
@@ -25,6 +25,7 @@ interface Props {
quotas: any[];
loading: boolean;
error: string | null;
+ message?: string | null;
refreshedAt?: string;
hasStaleData: boolean;
onRefresh: () => void;
@@ -109,6 +110,7 @@ export default function QuotaCardExpanded({
quotas,
loading,
error,
+ message,
refreshedAt,
hasStaleData,
onRefresh,
@@ -143,6 +145,10 @@ export default function QuotaCardExpanded({
error
{error}
+ ) : quotas.length === 0 && message ? (
+
+ {message}
+
) : quotas.length === 0 ? (
{t("noQuotaData")}
) : (
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts
new file mode 100644
index 0000000000..dfed75ba20
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts
@@ -0,0 +1,189 @@
+import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts";
+import { safePercentage } from "@/shared/utils/formatting";
+
+const GLM_QUOTA_ORDER: Record = { session: 0, weekly: 1, mcp_monthly: 2 };
+
+function quotaEntries(data: any): Array<[string, any]> {
+ return data?.quotas && typeof data.quotas === "object" ? Object.entries(data.quotas) : [];
+}
+
+function isUnlimitedEmpty(quota: any): boolean {
+ return Boolean(quota?.unlimited && (!quota?.total || quota.total <= 0));
+}
+
+function isPastResetWindow(resetAt: any): boolean {
+ if (!resetAt) return false;
+ const resetTime =
+ typeof resetAt === "number" ? resetAt : typeof resetAt === "string" ? Date.parse(resetAt) : NaN;
+ return Number.isFinite(resetTime) && Date.now() >= resetTime;
+}
+
+function getResetAdjustedQuota(quota: any) {
+ const usedRaw = Number(quota?.used || 0);
+ const totalRaw = Number(quota?.total || 0);
+ const total = Number.isFinite(totalRaw) ? totalRaw : 0;
+ const remainingRaw = safePercentage(quota?.remainingPercentage);
+ const hasPendingUsage = usedRaw > 0 || (remainingRaw !== undefined && remainingRaw < 100);
+ const staleAfterReset = isPastResetWindow(quota?.resetAt || null) && hasPendingUsage;
+
+ return {
+ staleAfterReset,
+ total,
+ used: staleAfterReset ? 0 : usedRaw,
+ remainingPercentage: staleAfterReset && total > 0 ? 100 : remainingRaw,
+ };
+}
+
+function normalizeQuotaEntry(name: string, quota: any = {}, extras: any = {}) {
+ const adjusted = getResetAdjustedQuota(quota);
+ return {
+ name,
+ used: Number.isFinite(adjusted.used) ? adjusted.used : 0,
+ total: adjusted.total,
+ resetAt: quota?.resetAt || null,
+ staleAfterReset: adjusted.staleAfterReset,
+ ...(adjusted.remainingPercentage !== undefined
+ ? { remainingPercentage: adjusted.remainingPercentage }
+ : {}),
+ ...extras,
+ };
+}
+
+function parseGeneric(data: any) {
+ return quotaEntries(data).map(([name, quota]) => normalizeQuotaEntry(name, quota));
+}
+
+function parseGithub(data: any) {
+ return quotaEntries(data)
+ .filter(([, quota]) => !isUnlimitedEmpty(quota))
+ .map(([name, quota]) => normalizeQuotaEntry(name, quota));
+}
+
+function parseGlmFamily(data: any) {
+ return quotaEntries(data).map(([name, quota]) =>
+ normalizeQuotaEntry(name, quota, {
+ displayName: quota?.displayName,
+ details: Array.isArray(quota?.details) ? quota.details : undefined,
+ isPercentageOnly:
+ Number(quota?.total || 0) === 100 && quota?.remainingPercentage !== undefined,
+ })
+ );
+}
+
+function buildCreditsQuota(
+ name: string,
+ remaining: number,
+ remainingPercentage: number,
+ extra = {}
+) {
+ return {
+ name,
+ used: 0,
+ total: 0,
+ remaining,
+ resetAt: null,
+ unlimited: false,
+ isCredits: true,
+ remainingPercentage,
+ creditCount: remaining,
+ ...extra,
+ };
+}
+
+function parseAntigravityQuota(modelKey: string, quota: any) {
+ if (modelKey === "credits") {
+ const remaining = Number(quota?.remaining ?? 0);
+ return buildCreditsQuota("credits", remaining, remaining > 50 ? 100 : remaining > 10 ? 60 : 20);
+ }
+ if (modelKey === "models" || isUnlimitedEmpty(quota)) return null;
+ return normalizeQuotaEntry(modelKey, quota, {
+ modelKey,
+ isPercentageOnly: quota?.fractionReported === true,
+ ...(quota?.quotaSource ? { quotaSource: quota.quotaSource } : {}),
+ ...(quota?.fractionReported !== undefined ? { fractionReported: quota.fractionReported } : {}),
+ });
+}
+
+function parseAntigravity(data: any) {
+ return quotaEntries(data)
+ .map(([modelKey, quota]) => parseAntigravityQuota(modelKey, quota))
+ .filter(Boolean);
+}
+
+function parseCodex(data: any) {
+ return quotaEntries(data).map(([quotaType, quota]) =>
+ normalizeQuotaEntry(quotaType, quota, {
+ displayName: quota?.displayName,
+ isPercentageOnly: true,
+ })
+ );
+}
+
+function parseClaude(data: any) {
+ if (data?.message)
+ return [{ name: "error", used: 0, total: 0, resetAt: null, message: data.message }];
+ return quotaEntries(data).map(([name, quota]) =>
+ normalizeQuotaEntry(name, quota, { isPercentageOnly: true })
+ );
+}
+
+function parseGeminiCli(data: any) {
+ return quotaEntries(data).map(([modelKey, quota]) =>
+ normalizeQuotaEntry(modelKey, quota, { modelKey })
+ );
+}
+
+function parseDeepseekQuota(quotaKey: string, quota: any) {
+ const match = quotaKey.match(/^credits(?:_([a-z]{3}))?$/);
+ if (!match) return normalizeQuotaEntry(quotaKey, quota);
+ const remaining = Number(quota?.remaining ?? 0);
+ const currency = quota?.currency ?? (match[1] ? match[1].toUpperCase() : "USD");
+ return buildCreditsQuota(currency, remaining, remaining > 20 ? 100 : remaining > 5 ? 60 : 20, {
+ currency,
+ });
+}
+
+function parseDeepseek(data: any) {
+ return quotaEntries(data).map(([quotaKey, quota]) => parseDeepseekQuota(quotaKey, quota));
+}
+
+function parseProviderQuotas(providerId: string, data: any) {
+ if (providerId === "github") return parseGithub(data);
+ if (["glm", "glm-cn", "glmt", "opencode-go"].includes(providerId)) return parseGlmFamily(data);
+ if (providerId === "antigravity" || providerId === "agy") return parseAntigravity(data);
+ if (providerId === "codex") return parseCodex(data);
+ if (providerId === "claude") return parseClaude(data);
+ if (providerId === "gemini-cli") return parseGeminiCli(data);
+ if (providerId === "deepseek") return parseDeepseek(data);
+ return parseGeneric(data);
+}
+
+function sortProviderModelOrder(provider: string, quotas: any[]) {
+ const modelOrder = getModelsByProviderId(provider);
+ if (modelOrder.length === 0) return;
+ const orderMap = new Map(modelOrder.map((m, i) => [m.id, i]));
+ quotas.sort(
+ (a, b) =>
+ (orderMap.get(a.modelKey || a.name) ?? 999) - (orderMap.get(b.modelKey || b.name) ?? 999)
+ );
+}
+
+function sortGlmOrder(providerId: string, quotas: any[]) {
+ if (!["glm", "glm-cn", "glmt", "opencode-go"].includes(providerId)) return;
+ quotas.sort((a, b) => (GLM_QUOTA_ORDER[a.name] ?? 99) - (GLM_QUOTA_ORDER[b.name] ?? 99));
+}
+
+export function parseQuotaData(provider: string | undefined, data: any) {
+ if (!data || typeof data !== "object") return [];
+ const providerId = String(provider || "").toLowerCase();
+
+ try {
+ const normalizedQuotas = parseProviderQuotas(providerId, data);
+ sortProviderModelOrder(provider, normalizedQuotas);
+ sortGlmOrder(providerId, normalizedQuotas);
+ return normalizedQuotas;
+ } catch (error) {
+ console.error(`Error parsing quota data for ${provider}:`, error);
+ return [];
+ }
+}
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useVisibleQuotaData.ts b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useVisibleQuotaData.ts
new file mode 100644
index 0000000000..ecf6b9618d
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useVisibleQuotaData.ts
@@ -0,0 +1,66 @@
+import { useEffect, useMemo, useState } from "react";
+
+import { collectHiddenQuotaModelIds, filterHiddenModelQuotas } from "./utils";
+
+function getProviderKey(connections: any[]): string {
+ const providers = new Set();
+ for (const conn of connections) {
+ if (typeof conn?.provider === "string" && conn.provider) providers.add(conn.provider);
+ }
+ return Array.from(providers).sort().join("|");
+}
+
+export function useVisibleQuotaData(
+ connections: any[],
+ quotaData: Record
+): Record {
+ const [hiddenModelsByProvider, setHiddenModelsByProvider] = useState>(
+ {}
+ );
+ const providerKey = useMemo(() => getProviderKey(connections), [connections]);
+
+ useEffect(() => {
+ if (!providerKey) return;
+
+ let alive = true;
+ const providers = providerKey.split("|").filter(Boolean);
+
+ Promise.all(
+ providers.map(async (provider) => {
+ try {
+ const response = await fetch(
+ `/api/provider-models?provider=${encodeURIComponent(provider)}`
+ );
+ if (!response.ok) return [provider, []] as const;
+ const data = await response.json();
+ return [provider, collectHiddenQuotaModelIds(provider, data)] as const;
+ } catch {
+ return [provider, []] as const;
+ }
+ })
+ ).then((entries) => {
+ if (alive) setHiddenModelsByProvider(Object.fromEntries(entries));
+ });
+
+ return () => {
+ alive = false;
+ };
+ }, [providerKey]);
+
+ return useMemo(() => {
+ const next: Record = {};
+ for (const conn of connections) {
+ const data = quotaData[conn.id];
+ if (!data) continue;
+ next[conn.id] = {
+ ...data,
+ quotas: filterHiddenModelQuotas(
+ conn.provider,
+ data.quotas,
+ hiddenModelsByProvider[conn.provider]
+ ),
+ };
+ }
+ return next;
+ }, [connections, hiddenModelsByProvider, quotaData]);
+}
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx
index b8dec9cddb..e19f8b973b 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx
@@ -1,5 +1,4 @@
-import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts";
-import { safePercentage } from "@/shared/utils/formatting";
+export { parseQuotaData } from "./quotaParsing";
const PROVIDER_PLAN_FALLBACKS = new Set([
"claude code",
@@ -36,12 +35,6 @@ const QUOTA_LABEL_MAP: Record = {
time_limit: "Time Limit",
};
-const GLM_QUOTA_ORDER: Record = {
- session: 0,
- weekly: 1,
- mcp_monthly: 2,
-};
-
function toRecord(value: unknown): Record {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record)
@@ -179,267 +172,6 @@ export function calculatePercentage(used, total) {
return Math.round(((total - used) / total) * 100);
}
-function isPastResetWindow(resetAt) {
- if (!resetAt) return false;
- const resetTime =
- typeof resetAt === "number" ? resetAt : typeof resetAt === "string" ? Date.parse(resetAt) : NaN;
- if (!Number.isFinite(resetTime)) return false;
- return Date.now() >= resetTime;
-}
-
-function normalizeQuotaEntry(name: string, quota: any = {}, extras: any = {}) {
- const usedRaw = Number(quota?.used || 0);
- const totalRaw = Number(quota?.total || 0);
- const resetAt = quota?.resetAt || null;
-
- // T13: Only consider it stale if the reset time passed AND there's still usage shown.
- // If usage is already 0 (or remaining is 100%), it's naturally reset and doesn't need to be marked as stale.
- const passedReset = isPastResetWindow(resetAt);
- const remainingPercentageRaw = safePercentage(quota?.remainingPercentage);
- const hasPendingUsage =
- usedRaw > 0 || (remainingPercentageRaw !== undefined && remainingPercentageRaw < 100);
- const staleAfterReset = passedReset && hasPendingUsage;
-
- const used = staleAfterReset ? 0 : usedRaw;
- const total = Number.isFinite(totalRaw) ? totalRaw : 0;
-
- const remainingPercentage =
- staleAfterReset && total > 0
- ? 100
- : remainingPercentageRaw !== undefined
- ? remainingPercentageRaw
- : undefined;
-
- return {
- name,
- used: Number.isFinite(used) ? used : 0,
- total,
- resetAt,
- staleAfterReset,
- ...(remainingPercentage !== undefined ? { remainingPercentage } : {}),
- ...extras,
- };
-}
-
-/**
- * Parse provider-specific quota structures into normalized array
- * @param {string} provider - Provider name (github, antigravity, codex, kiro, claude)
- * @param {Object} data - Raw quota data from provider
- * @returns {Array