Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ fi

# Cheap, deterministic local gates (re-enabled). Slower checks (i18n drift,
# openapi coverage/security-tiers, env-doc sync) run in CI to keep commits fast.
sh scripts/check/check-git-identity.sh
npx lint-staged
node scripts/check/check-docs-sync.mjs
npm run check:any-budget:t11
Expand Down
1 change: 1 addition & 0 deletions changelog.d/fixes/0000-claude-extra-usage-preflight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(claude):** `blockExtraUsage: false` no longer lets 5h quota preflight skip the account; extra usage is billed after the session bar is gone, so the request must reach Anthropic
2 changes: 2 additions & 0 deletions changelog.d/fixes/12711-batch-delete-click-event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- **fix(dashboard):** batch-deleting provider keys no longer toasts failure after a successful delete when the confirm button's click event is forwarded as `onAfter` ([#12711](https://github.com/diegosouzapw/OmniRoute/pull/12711))
- **fix(glm):** drop the extra 16th argument to `createSSETransformStreamWithLogger` that TypeScript rejected (TS2554) and that never reached the TransformStream
1 change: 1 addition & 0 deletions changelog.d/fixes/12868-runtime-quotagroup-nodemap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(dashboard):** pass `nodeMap` into Runtime `QuotaGroup` so a quota monitor in error/exhausted/alerting no longer throws `ReferenceError: nodeMap is not defined`. ([#12868](https://github.com/diegosouzapw/OmniRoute/pull/12868))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Restricted API keys whose `allowedModels` lists a combo name no longer skip every combo member at pre-dispatch (`ALL_TARGETS_SKIPPED`). Inner-target filtering still applies when the allow-list is a provider prefix or `disableNonPublicModels` is on ([#12899](https://github.com/diegosouzapw/OmniRoute/pull/12899)).
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(combos):** Effort-suffixed combo members inherit the base model's `model_context_overrides` row so priority order is not inverted on large requests ([#12926](https://github.com/diegosouzapw/OmniRoute/pull/12926)) — thanks @HouMinXi
1 change: 1 addition & 0 deletions changelog.d/fixes/dashboard-stale-tab-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(pwa):** do not intercept dashboard navigations so Chrome can fall back from a stale HTTP/3 Alt-Svc advertisement (UDP :20128 is unpublished; F5 on a long-lived tab hung until a new tab opened a fresh TCP connection).
1 change: 1 addition & 0 deletions changelog.d/fixes/glm-sse-transform-arity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(glm):** drop the extra 16th argument to `createSSETransformStreamWithLogger` that TypeScript rejected (TS2554) and that never reached TransformStream
1 change: 1 addition & 0 deletions changelog.d/fixes/grok-cli-reset-credits-readonly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(grok-cli):** Provider Limits shows grok-cli banked reset credits from `GetRemainingResets` (including a real zero; a failed RPC omits the row) and the existing View credits button now calls `ConsumerUiSvc/RedeemReset` for grok-cli. Live tokens use nested fields 10/20/30 (id + Timestamp), not compact 1/2/3.
1 change: 1 addition & 0 deletions changelog.d/fixes/moonshot-balance-pct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(dashboard):** Moonshot/Kimi Open Platform voucher and cash leftover percentages follow the bucket balance, so an empty wallet no longer paints those rows as 100% while Available is 0%
2 changes: 1 addition & 1 deletion changelog.d/fixes/reset-aware-model-family.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Keep Antigravity Gemini usable when the same connection's Claude weekly quota is empty; generic quota cache stays per-connection for every other provider.
- Keep Antigravity Gemini usable when the same connection's Claude weekly quota is empty; generic quota cache stays per-connection for every other provider.
11 changes: 7 additions & 4 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"_rebaseline_2026_09_06_runtime_quotagroup_nodemap": "Own growth: src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx 1201->1222 (+21, check-file-size split-newline). QuotaGroup is a module-level sibling and was reading nodeMap from RuntimePageClient's closure; that identifier is not in scope, so a quota monitor with status error/exhausted/alerting throws ReferenceError. Fix threads nodeMap as a prop (3 call sites + parameter + ProviderNodeEntry import). Prettier wraps the long import and the three QuotaGroup JSX tags. Covered by tests/unit/ui/runtime-page-client.test.tsx (empty monitors stay green; error+exhausted fixtures mount QuotaGroup).",
"_rebaseline_2026_09_05_claude_extra_usage_preflight": "Own growth: open-sse/services/combo.ts 4080->4084 (+4). buildAutoCandidates now forwards connection.providerSpecificData into evaluateQuotaCutoff so a Claude account with blockExtraUsage=false is not dropped at the 5h bar. Irreducible at the existing cutoff call site; the helper lives in claudeExtraUsage.ts (under cap). Covered by tests/unit/quota-preflight.test.ts.",
"_rebaseline_2026_09_04_12697_combo_pin_allowlist": "PR #12697 own growth: src/sse/handlers/chat.ts 2454->2458 (+4). checkModelAvailable preflight and handleSingleModelChat now call comboPinAllowlist so a pin-only combo step cannot scan the provider pool after 502/429. Helper lives in src/lib/combos/steps.ts under cap. Covered by tests/unit/combo-pin-implicit-allowlist.test.ts (11/11).",
"_rebaseline_2026_09_03_reset_aware_model_family": "Own growth: open-sse/services/combo.ts 4036->4041 (+5). buildAutoCandidates now keys the reset-aware quota cache by getQuotaFetchScope and spreads requestedModel onto the connection so Gemini windows stay off a Claude-empty Antigravity account. Irreducible wiring at the existing fetchResetAwareQuotaWithCache call site; the family helper itself lives in antigravityQuotaFamily.ts. Covered by tests/unit/reset-aware-request-scope-12600.test.ts.",
"_rebaseline_2026_09_03_overloaded_not_provider_breaker": "fix/overloaded-not-provider-breaker own growth: open-sse/services/combo.ts 4036->4075 (check-file-size split-newline, +39). Circuit-open pre-skip now records the breaker retryAfter and, when every target was skipped that way, waits the short reset via resolveCircuitOpenWaitDecision (new leaf in comboCooldownRetry.ts) instead of crystallizing ALL_TARGETS_SKIPPED in ~43ms. skippedForCircuitOpen / earliestCircuitOpenRetryMs reset each setTry so a later iteration cannot inherit a stale retryAfter. Irreducible at the existing ALL_TARGETS_SKIPPED chokepoint (same pattern as #7301/#8213 cooldown-wait). Predicate itself lives in circuitBreaker.ts / comboPredicates.ts / chatPredicates.ts, all under cap. Covered by tests/unit/overloaded-not-provider-breaker.test.ts + combo-cooldown-retry.test.ts.",
"_rebaseline_2026_09_03_12649_free_tier_reaudit_gateways": "PR #12649 (fix/free-tier-quota-reaudit) own growth: src/shared/constants/providers/apikey/gateways.ts 1459->1462 (+3 = the nara authHint rewritten for the re-audited 7M/day plan now wraps to two lines, plus the Prettier reflow of two pre-existing >100-col authHint lines (oneminai, freebuff) that lint-staged enforces on any touch of the file; additive text at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines: #11786 seekai, #10987 logfare, #10531 freebuff). Covered by tests/unit/free-tier-reaudit-2026-09.test.ts and tests/unit/free-providers-batch-2026-07.test.ts.",
Expand Down Expand Up @@ -425,7 +428,7 @@
"open-sse/mcp-server/server.ts": 1572,
"open-sse/services/accountFallback.ts": 2467,
"open-sse/services/adobeFireflyBrowserLogin.ts": 1401,
"open-sse/services/combo.ts": 4080,
"open-sse/services/combo.ts": 4084,
"open-sse/translator/response/openai-responses.ts": 1466,
"open-sse/utils/cursorAgentProtobuf.ts": 1547,
"open-sse/utils/proxyFetch.ts": 1271,
Expand All @@ -439,7 +442,7 @@
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2491,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1631,
"src/app/(dashboard)/dashboard/providers/page.tsx": 2025,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1222,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1475,
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1271,
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1607,
Expand All @@ -457,7 +460,7 @@
"src/shared/components/RequestLoggerV2.tsx": 1718,
"src/shared/constants/providers/apikey/gateways.ts": 1462,
"src/shared/services/cliRuntime.ts": 1296,
"src/sse/handlers/chat.ts": 2454,
"src/sse/handlers/chat.ts": 2458,
"src/sse/services/auth.ts": 3450,
"tests/unit/account-fallback-service.test.ts": 2453,
"tests/unit/provider-validation-specialty.test.ts": 4656
Expand Down Expand Up @@ -575,7 +578,7 @@
"src/app/(dashboard)/dashboard/health/page.tsx": "1165",
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": "1324",
"src/app/(dashboard)/dashboard/providers/page.tsx": "1944",
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": "1201",
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": "1222",
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": "1019",
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": "1470",
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": "1123",
Expand Down
6 changes: 3 additions & 3 deletions docs/guides/PWA_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ OmniRoute includes a service worker (`sw.js`) that provides intelligent caching:
| **App Shell** | Cache-first | `/`, `/offline`, manifest, and icons are pre-cached on install |
| **Static assets** (CSS, JS, images, fonts) | Network-first with cache fallback | Fetches fresh from the network; falls back to cache if offline |
| **Next.js bundles** (`/_next/`) | Network-first with cache update | Fetches from network and updates cache; serves cached version if offline |
| **Navigation requests** | Network-only with offline fallback | Always fetches from network; shows `/offline` page if network is unavailable |
| **API routes** (`/api/`, `/a2a`, `/dashboard/endpoint`) | Bypass (never cached) | Always goes directly to the server — never intercepted by the service worker |
| **Navigation requests** | Bypass (never intercepted) | Browser owns HTTP/3→HTTP/2 fallback; a dead QUIC socket must not become `Response.error()` |
| **API / dashboard routes** (`/api/`, `/a2a`, `/dashboard`) | Bypass (never cached) | Always goes directly to the server — never intercepted by the service worker |

### Offline Page

Expand Down Expand Up @@ -118,7 +118,7 @@ A vanilla service worker (no framework dependencies) with:
- **Install phase**: Pre-caches the app shell (root, offline page, manifest, icons)
- **Activate phase**: Cleans up old cache versions and claims all clients
- **Fetch phase**: Intelligent routing based on request type (navigation, static asset, API)
- **Cache versioning**: `omniroute-pwa-v2` — bump this to force a fresh cache on update
- **Cache versioning**: `omniroute-pwa-v3` — bump this to force a fresh cache on update

### Layout Metadata (`src/app/layout.tsx`)

Expand Down
9 changes: 3 additions & 6 deletions open-sse/executors/glm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ export function translateSseResponse(
suppressThinkClose: boolean = false
): Response {
if (!response.body) return response;
// GLM is a high-throughput provider — use a larger stream buffer (64KB) to
// keep provider → client pacing ahead of the model's token emission rate.
// Helper has 15 parameters; a 16th positional (65536) was a TS2554 and
// never reached TransformStream. highWaterMark stays at the helper default.
const transform = createSSETransformStreamWithLogger(
FORMATS.CLAUDE,
FORMATS.OPENAI,
Expand All @@ -238,10 +238,7 @@ export function translateSseResponse(
null,
null,
false,
suppressThinkClose,
undefined,
undefined,
65536
suppressThinkClose
);
const headers = cloneHeaders(response.headers);
headers.set("content-type", "text/event-stream");
Expand Down
6 changes: 5 additions & 1 deletion open-sse/services/combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,11 @@ export async function buildAutoCandidates(
const cutoffDecision = evaluateQuotaCutoff(
quota as QuotaInfo | null,
buildAutoQuotaThresholds(provider, connection, resilienceSettings),
{ provider, requestedModel: modelStr }
{
provider,
requestedModel: modelStr,
providerSpecificData: connection?.providerSpecificData,
}
);
if (!cutoffDecision.proceed) {
quotaCutoffBlocked = true;
Expand Down
15 changes: 11 additions & 4 deletions open-sse/services/combo/comboStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

import { getModelContextLimit } from "../../../src/lib/modelCapabilities";
import { getHiddenModelsByProvider } from "../../../src/lib/db/models";
import { getComboModelString, normalizeComboStep } from "../../../src/lib/combos/steps.ts";
import {
getComboModelString,
implicitPinAllowlist,
normalizeComboStep,
} from "../../../src/lib/combos/steps.ts";
import { getProviderByAlias, getProviderById } from "../../../src/shared/constants/providers.ts";
import { estimateTokens } from "../contextManager.ts";
import { containsMediaKind } from "../../utils/mediaParts.ts";
Expand Down Expand Up @@ -121,20 +125,23 @@ function normalizeRuntimeStep(
const modelStr = getComboModelString(step);
if (!modelStr) return null;

const connectionId = toTrimmedString(step.connectionId);
const allowedConnectionIds = implicitPinAllowlist(connectionId, step.allowedConnectionIds);

return {
kind: "model",
stepId: step.id,
executionKey,
modelStr,
provider: getTargetProvider(modelStr, step.providerId),
providerId: step.providerId || null,
connectionId: step.connectionId || null,
connectionId,
// #3266: a per-step account allowlist scopes round-robin/weighted selection
// to a subset of the provider's connections. This is the second writer of
// `allowedConnectionIds` (tag routing is the first); both feed the existing
// credential-selection filter in auth.ts.
...(Array.isArray(step.allowedConnectionIds) && step.allowedConnectionIds.length > 0
? { allowedConnectionIds: step.allowedConnectionIds }
...(allowedConnectionIds && allowedConnectionIds.length > 0
? { allowedConnectionIds }
: {}),
weight,
label,
Expand Down
52 changes: 51 additions & 1 deletion open-sse/services/combo/contextOverrideGate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,56 @@
*/

import { getResolvedModelContextOverride } from "../../../src/lib/modelCapabilities";
import { parseModel } from "../model.ts";

/**
* Longest-first so `-xhigh` is not eaten by `-high`. Mirrors
* `stripKnownEffortSuffix` in modelCapabilities.ts, but that helper's array
* order still matches `-high` first (`"…-xhigh".endsWith("-high")`).
*/
const EFFORT_SUFFIXES_LONGEST_FIRST = [
"minimal",
"medium",
"xhigh",
"none",
"high",
"max",
"low",
] as const;

function stripTrailingEffortSuffix(modelId: string): string | null {
const normalized = String(modelId || "").trim();
if (!normalized) return null;
const lowered = normalized.toLowerCase();
for (const suffix of EFFORT_SUFFIXES_LONGEST_FIRST) {
const token = `-${suffix}`;
if (lowered.length > token.length && lowered.endsWith(token)) {
return normalized.slice(0, -token.length);
}
}
return null;
}

/**
* Exact override first; if missing, inherit the base id after stripping a
* trailing effort tier (#12475). Combo members are stored as
* `provider/GLM-5.3-high` while `model_context_overrides` is keyed on
* `GLM-5.3`. Dispatcher already strips the suffix; the compat filter did not.
*/
function lookupOverrideWithEffortInheritance(modelStr: string): number | null {
const exact = getResolvedModelContextOverride(modelStr);
if (exact != null) return exact;

const parsed = parseModel(modelStr);
const modelId = typeof parsed.model === "string" ? parsed.model.trim() : "";
const base = stripTrailingEffortSuffix(modelId);
if (!base || base === modelId) return null;

if (parsed.provider) {
return getResolvedModelContextOverride({ provider: parsed.provider, model: base });
}
return getResolvedModelContextOverride(base);
}

/**
* Resolve the context-fit verdict from a persisted per-model override, if one
Expand All @@ -35,7 +85,7 @@ function resolveContextOverrideVerdict(
requiredContextTokens: number
): boolean | undefined {
if (!modelStr) return undefined;
const override = getResolvedModelContextOverride(modelStr);
const override = lookupOverrideWithEffortInheritance(modelStr);
if (override == null) return undefined;
return override >= requiredContextTokens;
}
Expand Down
20 changes: 20 additions & 0 deletions open-sse/services/combo/fingerprintExpansion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ export function buildFingerprintExecutionKey(
return `${originalKey}@fp:${fingerprint}`;
}

function rewriteAllowlistIds(
allowlist: string[] | null | undefined,
fromId: string,
toId: string
): string[] | null {
if (!Array.isArray(allowlist) || allowlist.length === 0) return null;
const rewritten = allowlist.map((id) => {
if (id === fromId) return toId;
const pin = splitFingerprintPin(id);
return pin ? pin.realConnectionId : id;
});
return [...new Set(rewritten)];
}

/**
* Expand `expandedTargets` by splitting targets whose connection carries
* multiple fingerprints into one target per fingerprint.
Expand Down Expand Up @@ -116,6 +130,11 @@ export function expandTargetsByFingerprints(
// find the connection at all.
const pin = splitFingerprintPin(connectionId);
if (pin) {
const rewrittenAllowlist = rewriteAllowlistIds(
target.allowedConnectionIds,
connectionId,
pin.realConnectionId
);
result.push({
...target,
connectionId: pin.realConnectionId,
Expand All @@ -125,6 +144,7 @@ export function expandTargetsByFingerprints(
pin.pinnedFingerprint,
false
),
...(rewrittenAllowlist ? { allowedConnectionIds: rewrittenAllowlist } : {}),
});
continue;
}
Expand Down
6 changes: 5 additions & 1 deletion open-sse/services/combo/quotaExhaustionCutoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ export async function resolveQuotaExhaustionCutoffForTarget(
const cutoffDecision = evaluateQuotaCutoff(
quota as QuotaInfo | null,
buildAutoQuotaThresholds(provider, connection, resilienceSettings),
{ provider, requestedModel: requestedModel ?? null }
{
provider,
requestedModel: requestedModel ?? null,
providerSpecificData: connection?.providerSpecificData,
}
);
if (!cutoffDecision.proceed) {
return { blocked: true, reason: cutoffDecision.reason || "quota_exhausted" };
Expand Down
14 changes: 12 additions & 2 deletions open-sse/services/combo/quotaStrategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ export async function expandTargetsByQuotaAwareConnections(
targets: ResolvedComboTarget[],
comboName: string,
log: { warn?: (...args: unknown[]) => void },
apiKeyAllowedConnectionIds?: string[] | null
apiKeyAllowedConnectionIds?: string[] | null,
opts?: { skipExhaustionFilter?: boolean }
): Promise<{
connectionById: Map<string, Record<string, unknown>>;
expandedTargets: ResolvedComboTarget[];
Expand Down Expand Up @@ -228,7 +229,16 @@ export async function expandTargetsByQuotaAwareConnections(
) {
continue;
}
if (provider && isQuotaExhaustedForRequest(connectionId, provider, target.modelStr || null)) {
if (
!opts?.skipExhaustionFilter &&
provider &&
isQuotaExhaustedForRequest(
connectionId,
provider,
target.modelStr || null,
connection?.providerSpecificData
)
) {
continue;
}
expandedTargets.push({
Expand Down
Loading
Loading