From a598fbb090159c2298403b5d90463c9788ddf812 Mon Sep 17 00:00:00 2001 From: "Andrew B." <37745667+AndrianBalanescu@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:08:02 -0500 Subject: [PATCH] fix(combo): least-used quota strategy and wildcard UI preservation (#8894) Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip) --- open-sse/services/combo/quotaStrategies.ts | 31 ++++- src/app/(dashboard)/dashboard/combos/page.tsx | 100 +++++++++++--- src/domain/quotaCache.ts | 63 +++++++-- src/lib/combos/controlCenter.ts | 25 +++- src/lib/combos/steps.ts | 127 +++++++++++++++++- src/lib/db/contextHandoffs.ts | 38 ++++++ tests/unit/combo-control-center.test.ts | 24 ++++ 7 files changed, 361 insertions(+), 47 deletions(-) diff --git a/open-sse/services/combo/quotaStrategies.ts b/open-sse/services/combo/quotaStrategies.ts index 3e0f27ee99..2e117f74fe 100644 --- a/open-sse/services/combo/quotaStrategies.ts +++ b/open-sse/services/combo/quotaStrategies.ts @@ -18,10 +18,18 @@ * and orderTargetsByResetWindow shares the same rrCounters Map from ./rrState.ts * (D7a) so reset-aware tie rotation stays consistent with round-robin routing. * + * @changes + * - [2026-07-24] [Composer] - Exclude Antigravity accounts without stored projectId from reset-aware pool + * - [2026-07-24] [Composer] - Skip quota-exhausted and rate-limited connections in reset-aware expansion + * * Pure leaf: this module never imports from the combo barrel. */ -import { getRuntimeProviderProfile, type ProviderProfile } from "../accountFallback.ts"; +import { + getRuntimeProviderProfile, + isAccountUnavailable, + type ProviderProfile, +} from "../accountFallback.ts"; import { PRE_SCREEN_CONCURRENCY } from "../comboConfig.ts"; import { getQuotaFetcher } from "../quotaPreflight.ts"; import { getCircuitBreaker } from "../../../src/shared/utils/circuitBreaker"; @@ -37,6 +45,8 @@ import { type QuotaFetchCacheConfig, } from "./quotaScoring.ts"; import { rankByHeadroom, type HeadroomSaturation } from "./headroomRanking.ts"; +import { preferAntigravityConnectionsWithStoredProject } from "../antigravityProjectPersistence.ts"; +import { isQuotaExhaustedForRequest } from "../../../src/domain/quotaCache.ts"; const RESET_AWARE_CONNECTION_CACHE_TTL_MS = 30_000; const RESET_AWARE_QUOTA_FETCH_CONCURRENCY = 5; @@ -75,9 +85,14 @@ async function getQuotaAwareConnectionsForTarget( (async () => { try { const connections = await getCachedProviderConnections({ provider, isActive: true }); - const activeConnections = Array.isArray(connections) + let activeConnections = Array.isArray(connections) ? (connections as Array>) : []; + if (provider === "antigravity" || provider === "agy") { + activeConnections = preferAntigravityConnectionsWithStoredProject( + activeConnections + ) as Array>; + } if ( !resetAwareConnectionCache.has(provider) && resetAwareConnectionCache.size >= MAX_RESET_AWARE_CACHE @@ -199,6 +214,18 @@ async function expandTargetsByQuotaAwareConnections( } for (const connectionId of connectionIds) { + const provider = getResetAwareProvider(target); + const connection = connectionById.get(connectionId); + if ( + connection && + typeof connection.rateLimitedUntil === "string" && + isAccountUnavailable(connection.rateLimitedUntil) + ) { + continue; + } + if (provider && isQuotaExhaustedForRequest(connectionId, provider, target.modelStr || null)) { + continue; + } expandedTargets.push({ ...target, connectionId, diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 9433fb4ab8..f03c79687e 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -54,6 +54,7 @@ import { normalizeIntelligentRoutingFilter, normalizeIntelligentRoutingConfig, } from "@/lib/combos/intelligentRouting"; +import { getComboStepTarget } from "@/lib/combos/steps"; import { resolveServerErrorMessage } from "@/lib/api/serverErrorMessage"; import { useTranslations } from "next-intl"; @@ -570,6 +571,13 @@ function normalizeModelEntry(entry) { weight: entry.weight || 0, }; } + if (entry?.kind === "provider-wildcard") { + return { + ...entry, + model: getComboStepTarget(entry), + weight: entry.weight || 0, + }; + } return { ...entry, model: entry.model, @@ -580,6 +588,7 @@ function normalizeModelEntry(entry) { function getModelString(entry) { if (typeof entry === "string") return entry; if (entry?.kind === "combo-ref") return entry.comboName; + if (entry?.kind === "provider-wildcard") return getComboStepTarget(entry); return entry.model; } @@ -635,6 +644,37 @@ function formatComboEntryDisplay( return `Combo → ${normalizedEntry.comboName}`; } + if (normalizedEntry.kind === "provider-wildcard") { + const providerIdentifier = normalizedEntry.providerId; + const builderProvider = findBuilderProviderByIdentifier(builderProviders, providerIdentifier); + const providerNode = findProviderNodeByIdentifier(providerNodes, providerIdentifier); + const providerLabel = + builderProvider?.displayName || providerNode?.name || providerIdentifier || "provider"; + const patternLabel = normalizedEntry.modelPattern || "*"; + const wildcardLabel = `${providerLabel}/${patternLabel}`; + + if (!includeConnection) { + return wildcardLabel; + } + + const connectionId = normalizedEntry.connectionId || null; + const rawConnectionLabel = + (connectionId && + builderProvider?.connections?.find((connection) => connection.id === connectionId) + ?.label) || + normalizedEntry.label || + null; + const connectionLabel = rawConnectionLabel + ? pickDisplayValue([rawConnectionLabel], showFullEmails, rawConnectionLabel) + : null; + + if (connectionId) { + return `${wildcardLabel} · ${connectionLabel || `acct ${connectionId.slice(0, 8)}`}`; + } + + return `${wildcardLabel} · dynamic account`; + } + const parsed = parseQualifiedModel(normalizedEntry.model); if (!parsed) return normalizedEntry.model; @@ -3420,15 +3460,25 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
{entry.kind === "combo-ref" ? getI18nOrFallback(t, "builderComboRefStep", "Nested combo reference") - : entry.connectionId - ? getI18nOrFallback(t, "builderPinnedAccount", "Pinned account") - : entry.providerId - ? getI18nOrFallback( - t, - "builderDynamicAccountShort", - "Dynamic account" - ) - : getI18nOrFallback(t, "builderLegacyEntry", "Legacy model entry")} + : entry.kind === "provider-wildcard" + ? getI18nOrFallback( + t, + "builderProviderWildcard", + "All matching provider models" + ) + : entry.connectionId + ? getI18nOrFallback(t, "builderPinnedAccount", "Pinned account") + : entry.providerId + ? getI18nOrFallback( + t, + "builderDynamicAccountShort", + "Dynamic account" + ) + : getI18nOrFallback( + t, + "builderLegacyEntry", + "Legacy model entry" + )}
@@ -4542,19 +4592,25 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo "builderComboRefStep", "Nested combo reference" ) - : entry.connectionId - ? getI18nOrFallback(t, "builderPinnedAccount", "Pinned account") - : entry.providerId - ? getI18nOrFallback( - t, - "builderDynamicAccountShort", - "Dynamic account" - ) - : getI18nOrFallback( - t, - "builderLegacyEntry", - "Legacy model entry" - )} + : entry.kind === "provider-wildcard" + ? getI18nOrFallback( + t, + "builderProviderWildcard", + "All matching provider models" + ) + : entry.connectionId + ? getI18nOrFallback(t, "builderPinnedAccount", "Pinned account") + : entry.providerId + ? getI18nOrFallback( + t, + "builderDynamicAccountShort", + "Dynamic account" + ) + : getI18nOrFallback( + t, + "builderLegacyEntry", + "Legacy model entry" + )} {strategy === "weighted" && entry.weight > 0 ? ` · ${entry.weight}%` : ""} diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index 6371cde370..89a925236d 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -10,6 +10,9 @@ * - Active accounts (quota > 0%): refetch every 5 minutes * - Exhausted accounts: refetch every 5 minutes (or immediately after resetAt passes) * + * @changes + * - [2026-07-24] [Composer] - Scope Antigravity per-model exhaustion to exact model + family weekly windows + * * @module domain/quotaCache */ @@ -236,6 +239,43 @@ export function __clearForTests() { getState().cache.clear(); } +function resolveAntigravityQuotaWindowsForModel( + quotaNames: string[], + requestedModel: string +): string[] { + const requestedFamily = getAntigravityQuotaFamily(requestedModel); + const cleanRequestedModel = requestedModel.replace(/^(antigravity|agy)\//, ""); + const bareModel = cleanRequestedModel.includes("/") + ? cleanRequestedModel.slice(cleanRequestedModel.lastIndexOf("/") + 1) + : cleanRequestedModel; + + if (requestedFamily === "other") { + return quotaNames.filter((windowName) => { + const bare = windowName.replace(/^(antigravity|agy)\//, ""); + return bare === bareModel || bare === cleanRequestedModel; + }); + } + + const familyAggregates = + requestedFamily === "gemini" + ? ["gemini_weekly"] + : requestedFamily === "claude" + ? ["claude_gpt_weekly"] + : []; + + const exactWindows = quotaNames.filter((windowName) => { + const bare = windowName.replace(/^(antigravity|agy)\//, ""); + return bare === bareModel; + }); + const aggregateWindows = familyAggregates.filter((key) => quotaNames.includes(key)); + const scoped = [...exactWindows, ...aggregateWindows]; + if (scoped.length > 0) return scoped; + + return quotaNames.filter( + (windowName) => getAntigravityQuotaFamily(windowName) === requestedFamily + ); +} + function isAntigravityQuotaExhausted( connectionId: string, entry: QuotaCacheEntry, @@ -244,18 +284,13 @@ function isAntigravityQuotaExhausted( if (!requestedModel) return entry.exhausted; const quotaNames = Object.keys(entry.quotas || {}); if (quotaNames.length === 0) return entry.exhausted; - const requestedFamily = getAntigravityQuotaFamily(requestedModel); - const cleanRequestedModel = requestedModel.replace(/^(antigravity|agy)\//, ""); - const matchingWindows = quotaNames.filter((windowName) => { - if (requestedFamily === "other") { - return windowName.replace(/^(antigravity|agy)\//, "") === cleanRequestedModel; - } - return getAntigravityQuotaFamily(windowName) === requestedFamily; - }); + const matchingWindows = resolveAntigravityQuotaWindowsForModel(quotaNames, requestedModel); return ( matchingWindows.length > 0 && matchingWindows.every( - (windowName) => getQuotaWindowStatus(connectionId, windowName, 100)?.reachedThreshold + (windowName) => + getQuotaWindowStatus(connectionId, windowName, DEFAULT_QUOTA_THRESHOLD_PERCENT) + ?.reachedThreshold ) ); } @@ -273,7 +308,9 @@ function isCodexQuotaExhausted( return ( scopedWindowNames.length > 0 && scopedWindowNames.every( - (windowName) => getQuotaWindowStatus(connectionId, windowName, 100)?.reachedThreshold + (windowName) => + getQuotaWindowStatus(connectionId, windowName, DEFAULT_QUOTA_THRESHOLD_PERCENT) + ?.reachedThreshold ) ); } @@ -512,7 +549,11 @@ export function getQuotaWindowStatus( usedPercentage, resetAt, // If reset time has already passed, avoid stale cached percentages blocking selection. - reachedThreshold: windowExpired ? false : usedPercentage >= thresholdPercent, + reachedThreshold: windowExpired + ? false + : remainingPercentage <= 0 + ? true + : usedPercentage >= thresholdPercent, }; } diff --git a/src/lib/combos/controlCenter.ts b/src/lib/combos/controlCenter.ts index 767840e22b..825f96942c 100644 --- a/src/lib/combos/controlCenter.ts +++ b/src/lib/combos/controlCenter.ts @@ -66,7 +66,7 @@ export interface ComboControlCenterTargetHealth { export interface ComboControlCenterTarget { id: string; - kind: "model" | "combo-ref"; + kind: "model" | "combo-ref" | "provider-wildcard"; index: number; label: string; model: string; @@ -134,6 +134,9 @@ function getStepTags(step: ComboStep): string[] { function getStepLabel(step: ComboStep): string { if (step.label) return step.label; if (step.kind === "combo-ref") return `Combo → ${step.comboName}`; + if (step.kind === "provider-wildcard") { + return `All ${step.providerId}/${step.modelPattern}`; + } return step.model; } @@ -153,12 +156,19 @@ export function getComboControlCenterTargets( } return steps.map((step, index) => { - const model = step.kind === "combo-ref" ? step.comboName : step.model; + const model = + step.kind === "combo-ref" + ? step.comboName + : step.kind === "provider-wildcard" + ? `${step.providerId}/${step.modelPattern}` + : step.model; const healthEntry = healthByStepId.get(step.id) || healthByModel.get(model) || null; const provider = step.kind === "model" ? step.providerId || providerFromModel(step.model) || healthEntry?.provider || null - : null; + : step.kind === "provider-wildcard" + ? step.providerId + : null; return { id: step.id, @@ -167,7 +177,10 @@ export function getComboControlCenterTargets( label: getStepLabel(step), model, provider, - connectionId: step.kind === "model" ? step.connectionId || null : null, + connectionId: + step.kind === "model" || step.kind === "provider-wildcard" + ? step.connectionId || null + : null, weight: step.weight || 0, tags: getStepTags(step), health: healthEntry, @@ -252,7 +265,9 @@ export function summarizeComboControlCenter( strategy: combo.strategy || "priority", isActive: combo.isActive !== false, targetCount: targets.length, - modelTargetCount: targets.filter((target) => target.kind === "model").length, + modelTargetCount: targets.filter( + (target) => target.kind === "model" || target.kind === "provider-wildcard" + ).length, nestedComboCount: targets.filter((target) => target.kind === "combo-ref").length, providerCount: providers.size, totalRequests, diff --git a/src/lib/combos/steps.ts b/src/lib/combos/steps.ts index 155bcd77b4..d9f4ea7673 100644 --- a/src/lib/combos/steps.ts +++ b/src/lib/combos/steps.ts @@ -1,3 +1,10 @@ +/** + * @file steps.ts + * @description Combo step normalization helpers for model, combo-ref, provider-wildcard, and routing metadata. + * + * @changes + * - [2026-07-25] [Composer] - Preserve provider-wildcard steps during combo normalization + */ type JsonRecord = Record; export const COMBO_SCHEMA_VERSION = 2; @@ -28,7 +35,18 @@ export interface ComboRefStep { label?: string; } -export type ComboStep = ComboModelStep | ComboRefStep; +export interface ComboProviderWildcardStep { + id: string; + kind: "provider-wildcard"; + providerId: string; + modelPattern: string; + connectionId?: string | null; + allowedConnectionIds?: string[] | null; + weight: number; + label?: string; +} + +export type ComboStep = ComboModelStep | ComboRefStep | ComboProviderWildcardStep; type ComboCollectionLike = | Array<{ name?: unknown } | string> @@ -103,6 +121,18 @@ function parseProviderId(model: string): string | null { return providerId.length > 0 ? providerId : null; } +function parseProviderWildcardPattern( + target: string +): { providerId: string; modelPattern: string } | null { + const trimmed = target.trim(); + const slashIndex = trimmed.indexOf("/"); + if (slashIndex <= 0) return null; + const providerId = trimmed.slice(0, slashIndex).trim(); + const modelPattern = trimmed.slice(slashIndex + 1).trim(); + if (!providerId || !modelPattern.includes("*")) return null; + return { providerId, modelPattern }; +} + function toFullModelString(model: string, providerId?: string | null): string { const trimmedModel = model.trim(); if (trimmedModel.includes("/")) return trimmedModel; @@ -124,12 +154,9 @@ function buildStepId( index: number, seed: string ) { - const parts = [ - slugify(comboName || "combo"), - kind === "combo-ref" ? "ref" : "model", - String(index + 1), - slugify(seed), - ]; + const kindSlug = + kind === "combo-ref" ? "ref" : kind === "provider-wildcard" ? "wildcard" : "model"; + const parts = [slugify(comboName || "combo"), kindSlug, String(index + 1), slugify(seed)]; return parts.join("-").slice(0, 200); } @@ -187,6 +214,11 @@ export function getComboStepTarget( if (!isRecord(value)) return null; if (value.kind === "combo-ref") return toTrimmedString(value.comboName); + if (value.kind === "provider-wildcard") { + const providerId = toTrimmedString(value.providerId); + const modelPattern = toTrimmedString(value.modelPattern) || "*"; + return providerId ? `${providerId}/${modelPattern}` : null; + } const rawModel = toTrimmedString(value.model); if (!rawModel) return null; @@ -223,6 +255,22 @@ export function normalizeComboStep( }; } + const wildcardPattern = parseProviderWildcardPattern(target); + if (wildcardPattern) { + return { + id: buildStepId( + "provider-wildcard", + comboName, + index, + `${wildcardPattern.providerId}/${wildcardPattern.modelPattern}` + ), + kind: "provider-wildcard", + providerId: wildcardPattern.providerId, + modelPattern: wildcardPattern.modelPattern, + weight: 0, + }; + } + const providerId = parseProviderId(target); return { id: buildStepId("model", comboName, index, target), @@ -252,6 +300,41 @@ export function normalizeComboStep( }; } + if (value.kind === "provider-wildcard") { + const providerId = + toTrimmedString(value.providerId) || + toTrimmedString(value.provider) || + parseProviderId(toTrimmedString(value.model) || ""); + if (!providerId) return null; + const modelPattern = toTrimmedString(value.modelPattern) || "*"; + const connectionId = + value.connectionId === null ? null : toTrimmedString(value.connectionId) || undefined; + const allowedConnectionIds = Array.isArray(value.allowedConnectionIds) + ? value.allowedConnectionIds + .map((connId) => toTrimmedString(connId)) + .filter((connId): connId is string => !!connId) + : undefined; + return { + id: + explicitId || + buildStepId( + "provider-wildcard", + comboName, + index, + connectionId + ? `${providerId}/${modelPattern}:${connectionId}` + : `${providerId}/${modelPattern}` + ), + kind: "provider-wildcard", + providerId, + modelPattern, + ...(connectionId !== undefined ? { connectionId } : {}), + weight, + ...(label ? { label } : {}), + ...(allowedConnectionIds && allowedConnectionIds.length > 0 ? { allowedConnectionIds } : {}), + }; + } + const rawModel = toTrimmedString(value.model); if (!rawModel) return null; const isExplicitModel = value.kind === "model"; @@ -261,6 +344,36 @@ export function normalizeComboStep( toTrimmedString(value.provider) || parseProviderId(rawModel); + const wildcardPattern = parseProviderWildcardPattern(rawModel); + if (wildcardPattern) { + const connectionId = + value.connectionId === null ? null : toTrimmedString(value.connectionId) || undefined; + const allowedConnectionIds = Array.isArray(value.allowedConnectionIds) + ? value.allowedConnectionIds + .map((connId) => toTrimmedString(connId)) + .filter((connId): connId is string => !!connId) + : undefined; + return { + id: + explicitId || + buildStepId( + "provider-wildcard", + comboName, + index, + connectionId + ? `${wildcardPattern.providerId}/${wildcardPattern.modelPattern}:${connectionId}` + : `${wildcardPattern.providerId}/${wildcardPattern.modelPattern}` + ), + kind: "provider-wildcard", + providerId: wildcardPattern.providerId, + modelPattern: wildcardPattern.modelPattern, + ...(connectionId !== undefined ? { connectionId } : {}), + weight, + ...(label ? { label } : {}), + ...(allowedConnectionIds && allowedConnectionIds.length > 0 ? { allowedConnectionIds } : {}), + }; + } + if (!isExplicitModel && shouldTreatAsComboRef(rawModel, providerId, options)) { return { id: explicitId || buildStepId("combo-ref", comboName, index, rawModel), diff --git a/src/lib/db/contextHandoffs.ts b/src/lib/db/contextHandoffs.ts index 8bcd768e2b..12b315f340 100644 --- a/src/lib/db/contextHandoffs.ts +++ b/src/lib/db/contextHandoffs.ts @@ -22,6 +22,7 @@ type JsonRecord = Record; interface StatementLike { get: (...params: unknown[]) => TRow | undefined; + all: (...params: unknown[]) => TRow[]; run: (...params: unknown[]) => { changes: number }; } @@ -253,3 +254,40 @@ export function deleteSessionModelHistory(sessionId: string, comboName: string): .run(sessionId, comboName); return result.changes ?? 0; } + +/** + * Get usage counts for ALL models in a session's history. + * Returns a Map<{model}> -> {count} for least-used strategy. + * + * Queries the session_model_history table and aggregates by model_str. + * Can optionally filter by connectionId if provided. + * + * @param connectionId - Optional connection ID to filter by. If not provided, returns all connections. + * @returns Promise> of model strings to their usage count. + */ +export async function getSessionModelUsageCounts( + connectionId?: string +): Promise> { + const db = getDbInstance() as any; + + let sql = `SELECT model_str, COUNT(*) as count + FROM session_model_history + WHERE 1=1`; + const params: unknown[] = []; + + if (connectionId) { + sql += ` AND connection_id = ?`; + params.push(connectionId); + } + + sql += ` GROUP BY model_str ORDER BY count ASC`; + + const rows = db.prepare(sql).all(...params) as Array<{ model_str: string; count: number }>; + + const usageMap = new Map(); + rows.forEach((row: { model_str: string; count: number }) => { + usageMap.set(row.model_str, row.count); + }); + + return usageMap; +} diff --git a/tests/unit/combo-control-center.test.ts b/tests/unit/combo-control-center.test.ts index 2adf706555..f58fe1df80 100644 --- a/tests/unit/combo-control-center.test.ts +++ b/tests/unit/combo-control-center.test.ts @@ -57,6 +57,30 @@ test("getComboControlCenterTargets normalizes legacy, structured and nested comb assert.equal(targets[2].label, "Combo → cheap-fallback"); }); +test("getComboControlCenterTargets normalizes provider-wildcard steps", () => { + const targets = getComboControlCenterTargets({ + name: "alibabafree", + models: [ + { + id: "alibaba-main", + kind: "provider-wildcard", + providerId: "alibaba", + modelPattern: "*", + connectionId: "conn-main", + weight: 0, + label: "main", + }, + ], + }); + + assert.equal(targets.length, 1); + assert.equal(targets[0].kind, "provider-wildcard"); + assert.equal(targets[0].model, "alibaba/*"); + assert.equal(targets[0].provider, "alibaba"); + assert.equal(targets[0].connectionId, "conn-main"); + assert.equal(targets[0].label, "main"); +}); + test("summarizeComboControlCenter combines health and runtime metrics", () => { const summary = summarizeComboControlCenter( {