mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
Multi-connection Quota Sharing pools resolve a DIFFERENT provider plan depending on which member connection actually served a request (write path, enforceQuotaShare/recordConsumption) vs. the pool's primary connection (dashboard read path, /api/quota/pools/[id]/usage). The wizard's "Limite" step PUTs a manual plan override only to the primary connection, so any other pool member fell back to a different (catalog/empty) plan shape. Since the quota_consumption dimension key is poolId:unit:window, a different unit/window meant recordConsumption wrote to a bucket the dashboard never read, so real traffic served via a non-primary connection never appeared as "consumed". Fix: resolve the plan from the pool's canonical primary connection (pool.connectionId) in both enforceQuotaShare and recordConsumption, matching the dashboard's read path. recordConsumption now keeps the matched pool object (not just its id) so it can reach connectionId. getSaturation(input.connectionId, ...) is untouched — that signal is legitimately per-connection.
This commit is contained in:
committed by
GitHub
parent
460c6075b1
commit
0d31fd3d24
@@ -0,0 +1 @@
|
||||
- fix(quota): resolve Quota Sharing plan/limits from the pool's canonical primary connection in `enforceQuotaShare` and `recordConsumption`, not the serving connection — a multi-connection pool whose "Limite" wizard override only ever lands on the primary connection was writing/checking consumption under a different dimension key when a request was actually served via a non-primary pool member, so the dashboard's "consumed" amount never reflected real traffic (#13876)
|
||||
@@ -112,7 +112,14 @@ export async function enforceQuotaShare(input: EnforceInput): Promise<EnforceDec
|
||||
const store = await getQuotaStore();
|
||||
|
||||
// 3. Resolve the provider plan (dimensions).
|
||||
const plan = resolvePlan(input.connectionId, input.provider);
|
||||
//
|
||||
// Resolved from the pool's canonical (primary) connection — NOT
|
||||
// input.connectionId — because the wizard's "Limite" step only ever PUTs a
|
||||
// manual plan override to the pool's primary connection (connectionIds[0]).
|
||||
// A request served through a different pool member must still be checked
|
||||
// against that same plan/limit, or a non-primary connection silently falls
|
||||
// back to a different (catalog/empty) plan shape (#13876).
|
||||
const plan = resolvePlan(pool.connectionId, input.provider);
|
||||
|
||||
// 3b. Per-(key, model) model-cap pre-check (Fase 3 #7).
|
||||
//
|
||||
@@ -302,8 +309,10 @@ export async function recordConsumption(input: RecordConsumptionInput): Promise<
|
||||
|
||||
if (!allocations.length) return;
|
||||
|
||||
// Find the pool matching this connection
|
||||
let poolId: string | null = null;
|
||||
// Find the pool matching this connection.
|
||||
// Keep the matched pool object (not just its id) so plan resolution below
|
||||
// can use the pool's canonical primary connection — see (#13876).
|
||||
let matchedPool: import("@/lib/db/quotaPools").QuotaPool | null = null;
|
||||
for (const { poolId: pid } of allocations) {
|
||||
let p: import("@/lib/db/quotaPools").QuotaPool | null = null;
|
||||
try {
|
||||
@@ -318,14 +327,18 @@ export async function recordConsumption(input: RecordConsumptionInput): Promise<
|
||||
? p.connectionIds.includes(input.connectionId)
|
||||
: p.connectionId === input.connectionId)
|
||||
) {
|
||||
poolId = pid;
|
||||
matchedPool = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!poolId) return;
|
||||
if (!matchedPool) return;
|
||||
const poolId = matchedPool.id;
|
||||
|
||||
const plan = resolvePlan(input.connectionId, input.provider);
|
||||
// Resolved from the pool's canonical (primary) connection so writes always
|
||||
// land under the same dimension shape the dashboard reads back via
|
||||
// resolvePlan(pool.connectionId, ...) — see enforceQuotaShare above (#13876).
|
||||
const plan = resolvePlan(matchedPool.connectionId, input.provider);
|
||||
const store = await getQuotaStore();
|
||||
|
||||
// Pool-level dimension consumption (existing behaviour).
|
||||
|
||||
84
tests/unit/quota-share-multiconn-enforce-cap.test.ts
Normal file
84
tests/unit/quota-share-multiconn-enforce-cap.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Companion regression for #13876 — enforceQuotaShare side.
|
||||
*
|
||||
* A cap configured via the pool's PRIMARY connection's plan (the only
|
||||
* connection the wizard's "Limite" step ever PUTs an override to) must also
|
||||
* be enforced correctly for a request that is actually served by a
|
||||
* NON-primary pool member connection. Before the fix, enforceQuotaShare
|
||||
* resolved the plan from `input.connectionId` (the serving connection), so a
|
||||
* non-primary connection with no override fell back to an empty plan and the
|
||||
* cap was silently never enforced for traffic routed through it.
|
||||
*
|
||||
* getSaturation(input.connectionId, ...) remains per-connection by design —
|
||||
* only plan/dimension resolution moves to the pool's canonical connection.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-13876-enf-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const poolsDb = await import("../../src/lib/db/quotaPools.ts");
|
||||
const plansDb = await import("../../src/lib/db/providerPlans.ts");
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
if (fs.existsSync(TEST_DATA_DIR)) {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
});
|
||||
|
||||
test("multi-connection pool: a cap set on the primary connection's plan is enforced for a request served by a non-primary member", async () => {
|
||||
const { enforceQuotaShare, recordConsumption } = await import("../../src/lib/quota/enforce.ts");
|
||||
|
||||
const PROVIDER = "quota-13876-enforce-test-provider"; // not in the built-in catalog on purpose
|
||||
|
||||
poolsDb.createPool({
|
||||
connectionId: "conn-primary-2",
|
||||
connectionIds: ["conn-primary-2", "conn-secondary-2"],
|
||||
name: "Repro Pool #13876 (enforce)",
|
||||
allocations: [{ apiKeyId: "key-shared-2", weight: 100, policy: "hard" }],
|
||||
});
|
||||
|
||||
// The operator sets a small daily token cap via the wizard — only the
|
||||
// primary connection gets the manual override. The pool has 2 member
|
||||
// connections, so the effective (global) limit is limit × accountCount —
|
||||
// set the per-account limit small enough that a single request still
|
||||
// saturates the whole pool.
|
||||
plansDb.upsertPlan(
|
||||
"conn-primary-2",
|
||||
PROVIDER,
|
||||
[{ unit: "tokens", window: "daily", limit: 100 }],
|
||||
"manual"
|
||||
);
|
||||
|
||||
// A request served via the secondary connection consumes past the
|
||||
// effective (accountCount-multiplied) cap of 200 tokens.
|
||||
await recordConsumption({
|
||||
apiKeyId: "key-shared-2",
|
||||
connectionId: "conn-secondary-2",
|
||||
provider: PROVIDER,
|
||||
cost: { tokens: 1500, usd: 0, requests: 1 },
|
||||
});
|
||||
|
||||
// The NEXT request, also served via the secondary connection, must now be
|
||||
// blocked by the cap that was configured only on the primary connection's
|
||||
// plan — matching what the dashboard (which reads via pool.connectionId)
|
||||
// shows as consumed.
|
||||
const decision = await enforceQuotaShare({
|
||||
apiKeyId: "key-shared-2",
|
||||
connectionId: "conn-secondary-2",
|
||||
provider: PROVIDER,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
decision.kind,
|
||||
"block",
|
||||
`expected the primary connection's cap to block a request served via the secondary connection, got ${JSON.stringify(decision)}`
|
||||
);
|
||||
});
|
||||
114
tests/unit/quota-share-multiconn-plan-mismatch.test.ts
Normal file
114
tests/unit/quota-share-multiconn-plan-mismatch.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Repro for issue #13876 — Quota Sharing dashboard "consumed" does not update.
|
||||
*
|
||||
* Root cause: the "Limite" wizard step writes a manual plan override (PUT
|
||||
* /api/quota/plans/[connectionId]) ONLY for the pool's primary connection
|
||||
* (connectionIds[0]) — see PoolWizard.tsx. But a multi-connection pool
|
||||
* actually serves traffic through ANY member connection (fallback /
|
||||
* round-robin), and:
|
||||
* - the POST-hook (enforce.ts recordConsumption) resolved the plan with
|
||||
* resolvePlan(input.connectionId, provider) — the connection that
|
||||
* actually served THIS request.
|
||||
* - the dashboard read path (GET /api/quota/pools/[id]/usage) resolves the
|
||||
* plan with resolvePlan(pool.connectionId, provider) — always the
|
||||
* primary connection.
|
||||
*
|
||||
* When the primary connection has a manual override and a non-primary member
|
||||
* connection does not (falls through to catalog / empty dimensions), the two
|
||||
* paths used DIFFERENT dimension shapes. Consumption got written under one
|
||||
* dimensionKey (poolId:unit:window) while the dashboard read a different
|
||||
* one, so real traffic never appeared as "consumed" even though it happened.
|
||||
*
|
||||
* Fix: recordConsumption / enforceQuotaShare now resolve the plan from the
|
||||
* pool's canonical (primary) connection, matching the dashboard read path.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-13876-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const poolsDb = await import("../../src/lib/db/quotaPools.ts");
|
||||
const plansDb = await import("../../src/lib/db/providerPlans.ts");
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
if (fs.existsSync(TEST_DATA_DIR)) {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
});
|
||||
|
||||
test("multi-connection pool: consumption served via a non-primary connection is visible on the usage dashboard", async () => {
|
||||
const { recordConsumption } = await import("../../src/lib/quota/enforce.ts");
|
||||
const { resolvePlan } = await import("../../src/lib/quota/planResolver.ts");
|
||||
const { getSqliteQuotaStore } = await import("../../src/lib/quota/sqliteQuotaStore.ts");
|
||||
|
||||
const PROVIDER = "quota-13876-test-provider"; // not in the built-in catalog on purpose
|
||||
|
||||
// Pool with 2 member connections sharing quota for one API key.
|
||||
// conn-primary is connectionIds[0] — the one the wizard PUTs the plan to.
|
||||
// conn-secondary is a normal pool member that can also serve requests
|
||||
// (fallback/round-robin across accounts in the same pool).
|
||||
const pool = poolsDb.createPool({
|
||||
connectionId: "conn-primary",
|
||||
connectionIds: ["conn-primary", "conn-secondary"],
|
||||
name: "Repro Pool #13876",
|
||||
allocations: [{ apiKeyId: "key-shared", weight: 100, policy: "hard" }],
|
||||
});
|
||||
|
||||
// The operator opens the wizard's "Limite" step and sets a custom plan —
|
||||
// this PUTs only to /api/quota/plans/conn-primary (PoolWizard.tsx primaryConnectionId).
|
||||
plansDb.upsertPlan(
|
||||
"conn-primary",
|
||||
PROVIDER,
|
||||
[{ unit: "tokens", window: "daily", limit: 100000 }],
|
||||
"manual"
|
||||
);
|
||||
|
||||
// Sanity: conn-secondary has no override and the provider has no catalog
|
||||
// entry, so resolvePlan for it falls through to the empty plan.
|
||||
const secondaryPlan = resolvePlan("conn-secondary", PROVIDER);
|
||||
assert.equal(
|
||||
secondaryPlan.dimensions.length,
|
||||
0,
|
||||
"precondition: conn-secondary must resolve to a DIFFERENT (empty) plan than conn-primary"
|
||||
);
|
||||
|
||||
// A real request gets served through conn-secondary (the pool's fallback
|
||||
// member) and completes successfully — the POST-hook fires.
|
||||
await recordConsumption({
|
||||
apiKeyId: "key-shared",
|
||||
connectionId: "conn-secondary",
|
||||
provider: PROVIDER,
|
||||
cost: { tokens: 5000, usd: 0, requests: 1 },
|
||||
});
|
||||
|
||||
// The Quota Sharing dashboard reads usage the same way the REST route does:
|
||||
// resolve the plan from pool.connectionId (always the primary) and read
|
||||
// poolUsageWithDimensions with those dimensions.
|
||||
const primaryPlan = resolvePlan(pool.connectionId, PROVIDER);
|
||||
assert.equal(
|
||||
primaryPlan.dimensions.length,
|
||||
1,
|
||||
"primary connection plan should have the override"
|
||||
);
|
||||
|
||||
const store = getSqliteQuotaStore();
|
||||
const snapshot = await store.poolUsageWithDimensions(pool.id, primaryPlan.dimensions);
|
||||
|
||||
const tokenDim = snapshot.dimensions.find((d) => d.unit === "tokens");
|
||||
assert.ok(tokenDim, "tokens dimension should be present in the snapshot");
|
||||
|
||||
// Consumption served via conn-secondary must be visible under the pool's
|
||||
// canonical dimension key, exactly as the dashboard reads it.
|
||||
assert.equal(
|
||||
tokenDim!.consumedTotal,
|
||||
5000,
|
||||
`dashboard should show the 5000 tokens actually consumed via conn-secondary, got ${tokenDim!.consumedTotal}`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user