mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
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)
This commit is contained in:
@@ -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<Record<string, unknown>>)
|
||||
: [];
|
||||
if (provider === "antigravity" || provider === "agy") {
|
||||
activeConnections = preferAntigravityConnectionsWithStoredProject(
|
||||
activeConnections
|
||||
) as Array<Record<string, unknown>>;
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
<div className="text-[10px] text-text-muted truncate">
|
||||
{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"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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}%`
|
||||
: ""}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
|
||||
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),
|
||||
|
||||
@@ -22,6 +22,7 @@ type JsonRecord = Record<string, unknown>;
|
||||
|
||||
interface StatementLike<TRow = unknown> {
|
||||
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<Map<string, number>> of model strings to their usage count.
|
||||
*/
|
||||
export async function getSessionModelUsageCounts(
|
||||
connectionId?: string
|
||||
): Promise<Map<string, number>> {
|
||||
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<string, number>();
|
||||
rows.forEach((row: { model_str: string; count: number }) => {
|
||||
usageMap.set(row.model_str, row.count);
|
||||
});
|
||||
|
||||
return usageMap;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user