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 changelog.d/fixes/14176-auto-routing-manual-weights.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(combos):** Editing auto-routing factor sliders now selects custom scoring, so an existing mode pack no longer silently discards reset-window and other manual weights. Preserve additional combo settings when editing routing controls. Auto reset affinity combines weekly (65%) and session (35%) urgency, while retaining explicit window selections.
7 changes: 4 additions & 3 deletions open-sse/services/combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ import {
} from "./combo/autoStrategy.ts";
import {
resolveResetWindowConfig,
calculateResetWindowAffinity,
calculateAutoResetWindowAffinity,
resolveAutoResetWindowConfig,
type ResetWindowConfig,
} from "./combo/quotaScoring.ts";
import { fetchResetAwareQuotaWithCache, preScreenTargets } from "./combo/quotaStrategies.ts";
Expand Down Expand Up @@ -287,7 +288,7 @@ export async function buildAutoCandidates(
targets: ResolvedComboTarget[],
comboName: string,
sessionId: string | null | undefined = null,
resetWindowConfig: ResetWindowConfig = resolveResetWindowConfig(null),
resetWindowConfig: ResetWindowConfig = resolveAutoResetWindowConfig(null),
resilienceSettings: ResilienceSettings | null = null
): Promise<AutoProviderCandidate[]> {
const hiddenModelsMap = getHiddenModelsByProvider();
Expand Down Expand Up @@ -484,7 +485,7 @@ export async function buildAutoCandidates(
);
}
const quota = await quotaPromises.get(quotaKey)!;
resetWindowAffinity = calculateResetWindowAffinity(quota, resetWindowConfig);
resetWindowAffinity = calculateAutoResetWindowAffinity(quota, resetWindowConfig);
if (!quotaCutoffBlocked) {
quotaRemaining = quotaRemainingPercentFromQuota(quota, {
provider,
Expand Down
4 changes: 2 additions & 2 deletions open-sse/services/combo/autoConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "../autoCombo/scoring.ts";
import { getModePack } from "../autoCombo/modePacks.ts";
import { isRecord } from "./comboData.ts";
import { resolveResetWindowConfig, resolveSlaRoutingPolicy } from "./quotaScoring.ts";
import { resolveAutoResetWindowConfig, resolveSlaRoutingPolicy } from "./quotaScoring.ts";
import type { ComboLike, ResolvedComboTarget } from "./types.ts";

/**
Expand Down Expand Up @@ -60,7 +60,7 @@ export function parseAutoConfig(combo: ComboLike, eligibleTargets: ResolvedCombo
const weights = normalizeScoringWeights(
modePack ? getModePack(modePack) || configuredWeights : configuredWeights
);
const resetWindowConfig = resolveResetWindowConfig(autoConfigSource);
const resetWindowConfig = resolveAutoResetWindowConfig(autoConfigSource);
const slaPolicy = resolveSlaRoutingPolicy(autoConfigSource);

return {
Expand Down
32 changes: 32 additions & 0 deletions open-sse/services/combo/quotaScoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,35 @@ export function calculateResetWindowAffinity(quota: unknown, config: ResetWindow
if (msUntilReset <= 0) return 1;
return clamp01(1 - msUntilReset / getResetWindowHorizonMs(config.windows));
}

/** Auto scoring combines reset urgency without letting a short session hide weekly expiry. */
export function calculateAutoResetWindowAffinity(
quota: unknown,
config?: ResetWindowConfig,
now: number = Date.now()
): number {
if (!isRecord(quota)) return 0.5;
if (quota.limitReached === true) return 0;
const windows = config?.windows ?? ["weekly", "session"];
let total = 0;
let weightSum = 0;
for (const name of windows) {
const window = resolveQuotaWindowByName(quota, name);
const resetMs = parseResetTimeMs(window?.resetAt);
if (!Number.isFinite(resetMs)) continue;
const weight = name === "session" ? 0.35 : 0.65;
const horizon = getResetWindowHorizonMs([name]);
total += weight * clamp01(1 - Math.max(0, resetMs - now) / horizon);
weightSum += weight;
}
if (weightSum > 0) return total / weightSum;
// Preserve generic/model-keyed provider windows and unknown-quota neutrality.
const resetMs = getResetWindowTimestampMs(quota, windows);
return Number.isFinite(resetMs)
? clamp01(1 - Math.max(0, resetMs - now) / getResetWindowHorizonMs(windows))
: 0.5;
}

export function resolveAutoResetWindowConfig(config: Record<string, unknown> | null | undefined) {
return resolveResetWindowConfig({ resetWindowIncludeSession: true, ...config });
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useMemo } from "react";
import Card from "@/shared/components/Card";
import {
DEFAULT_INTELLIGENT_WEIGHTS,
applyIntelligentRoutingConfigPatch,
FACTOR_LABELS,
MODE_PACK_OPTIONS,
ROUTER_STRATEGY_OPTIONS,
Expand All @@ -12,7 +13,12 @@ import {
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { compareTr } from "@/shared/utils/turkishText";

function getI18nOrFallback(t: any, key: string, fallback: string, values?: Record<string, unknown>) {
function getI18nOrFallback(
t: any,
key: string,
fallback: string,
values?: Record<string, unknown>
) {
try {
if (typeof t?.has === "function" && t.has(key)) return t(key, values);
} catch {
Expand Down Expand Up @@ -91,14 +97,7 @@ export default function BuilderIntelligentStep({
);

const updateConfig = (patch: Record<string, unknown>) => {
onChange({
...normalizedConfig,
...patch,
weights: {
...normalizedConfig.weights,
...((patch.weights as Record<string, number>) || {}),
},
});
onChange(applyIntelligentRoutingConfigPatch(config, patch));
};

const toggleCandidateProvider = (providerId: string) => {
Expand Down
21 changes: 21 additions & 0 deletions src/lib/combos/intelligentRouting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,24 @@ export function buildIntelligentProviderScores(combo: {
factors: weights,
}));
}

/** Manual factor edits select custom scoring; unrelated edits preserve the preset. */
export function applyIntelligentRoutingConfigPatch(
config: Record<string, unknown>,
patch: Record<string, unknown>
): Record<string, unknown> & IntelligentRoutingConfig {
const normalized = normalizeIntelligentRoutingConfig(config);
const editsWeights = isRecord(patch.weights);
return {
...config,
...normalized,
...patch,
modePack: editsWeights
? "custom"
: ((patch.modePack as string | undefined) ?? normalized.modePack),
weights: {
...normalized.weights,
...(editsWeights ? patch.weights : {}),
},
};
}
105 changes: 105 additions & 0 deletions tests/unit/intelligent-routing-weight-edit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import test from "node:test";
import * as routing from "../../src/lib/combos/intelligentRouting.ts";
import { parseAutoConfig } from "../../open-sse/services/combo/autoConfig.ts";
import { calculateAutoResetWindowAffinity } from "../../open-sse/services/combo/quotaScoring.ts";
import { calculateFactors, calculateScore } from "../../open-sse/services/autoCombo/scoring.ts";

const patchConfig = (config: Record<string, unknown>, patch: Record<string, unknown>) => {
const fn = Reflect.get(routing, "applyIntelligentRoutingConfigPatch");
assert.equal(typeof fn, "function", "weight edits need to deactivate the preset");
return fn(config, patch);
};

test("editing reset weight switches off ship-fast and activates the saved distribution", () => {
const saved = {
modePack: "ship-fast",
weights: { quota: 0.25, resetWindowAffinity: 0.15 },
resetWindowWindows: ["weekly"],
};
const next = patchConfig(saved, { weights: { resetWindowAffinity: 0.3 } });
assert.equal(next.modePack, "custom");
assert.equal(next.weights.quota, 0.25);
assert.equal(next.weights.resetWindowAffinity, 0.3);
assert.deepEqual(next.resetWindowWindows, ["weekly"]);
assert.equal(saved.modePack, "ship-fast");
const cfg = parseAutoConfig({ name: "test", config: next }, []);
assert.ok(cfg.weights.resetWindowAffinity > 0);
});

test("unrelated edits preserve the preset and choosing a preset remains explicit", () => {
const saved = { modePack: "ship-fast", weights: { resetWindowAffinity: 0.3 } };
assert.equal(patchConfig(saved, { explorationRate: 0.02 }).modePack, "ship-fast");
const next = patchConfig({ ...saved, modePack: "custom" }, { modePack: "ship-fast" });
assert.equal(next.modePack, "ship-fast");
assert.equal(parseAutoConfig({ name: "test", config: next }, []).weights.resetWindowAffinity, 0);
});

test("edited reset weight favors a near weekly reset despite a nearer competing session reset", () => {
const next = patchConfig(
{ modePack: "ship-fast" },
{
weights: {
...Object.fromEntries(Object.keys(routing.DEFAULT_INTELLIGENT_WEIGHTS).map((k) => [k, 0])),
resetWindowAffinity: 1,
},
}
);
const cfg = parseAutoConfig({ name: "test", config: next }, []);
const now = Date.now();
const quota = (weeklyMinutes: number, sessionMinutes: number) => ({
window7d: { percentUsed: 0.5, resetAt: new Date(now + weeklyMinutes * 60000).toISOString() },
window5h: { percentUsed: 0.1, resetAt: new Date(now + sessionMinutes * 60000).toISOString() },
});
const candidate = (weeklyMinutes: number, sessionMinutes: number) => ({
provider: "codex",
model: "test",
quotaRemaining: 50,
quotaTotal: 100,
circuitBreakerState: "CLOSED" as const,
costPer1MTokens: 1,
p95LatencyMs: 100,
latencyStdDev: 10,
errorRate: 0,
resetWindowAffinity: calculateAutoResetWindowAffinity(
quota(weeklyMinutes, sessionMinutes),
cfg.resetWindowConfig
),
});
const pool = [candidate(17, 299), candidate(6 * 1440, 1)];
const scores = pool.map((c) =>
calculateScore(
calculateFactors(c, pool, "default", () => 0.5),
cfg.weights
)
);
assert.ok(scores[0] > scores[1], "weekly reset must contribute to actual auto scoring");
});

test("auto reset affinity weights weekly 65 percent and session 35 percent", async () => {
const scoring = await import("../../open-sse/services/combo/quotaScoring.ts");
const fn = Reflect.get(scoring, "calculateAutoResetWindowAffinity");
assert.equal(typeof fn, "function");
const now = Date.now();
const q = (weekly: number, session: number) => ({
window7d: { percentUsed: 0.2, resetAt: new Date(now + weekly).toISOString() },
window5h: { percentUsed: 0.2, resetAt: new Date(now + session).toISOString() },
});
const week = 7 * 24 * 3600000,
session = 5 * 3600000;
assert.ok(Math.abs(fn(q(0, session), undefined, now) - 0.65) < 0.00001);
assert.ok(Math.abs(fn(q(week, 0), undefined, now) - 0.35) < 0.00001);
assert.equal(
fn(
q(week, 0),
{ windows: ["weekly"], tieBandMs: 0, quotaCacheTtlMs: 0, quotaCacheMaxStaleMs: 0 },
now
),
0
);
assert.equal(
fn({ window7d: { percentUsed: 0.2, resetAt: new Date(now).toISOString() } }, undefined, now),
1
);
assert.equal(fn({ limitReached: true }, undefined, now), 0);
});
38 changes: 38 additions & 0 deletions tests/unit/ui/intelligent-routing-weight-edit.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @vitest-environment jsdom
import React from "react";
import { afterEach, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import BuilderIntelligentStep from "../../../src/app/(dashboard)/dashboard/combos/BuilderIntelligentStep";
import { DEFAULT_INTELLIGENT_WEIGHTS } from "../../../src/lib/combos/intelligentRouting";

afterEach(cleanup);

it("a manual reset slider edit selects custom weights and retains unrelated config", () => {
const onChange = vi.fn();
const t = Object.assign((key: string) => key, { has: () => false });
render(
<BuilderIntelligentStep
t={t}
activeProviders={[]}
config={{
modePack: "ship-fast",
weights: DEFAULT_INTELLIGENT_WEIGHTS,
resetWindowWindows: ["weekly"],
}}
onChange={onChange}
/>
);
const slider = screen
.getByText("Reset Window")
.closest("div")
?.parentElement?.querySelector("input");
expect(slider).toBeTruthy();
fireEvent.change(slider!, { target: { value: "0.3" } });
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
modePack: "custom",
resetWindowWindows: ["weekly"],
weights: expect.objectContaining({ resetWindowAffinity: 0.3 }),
})
);
});
Loading