mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* fix(usage): harden account identity reconciliation * fix(db): renumber codex strong-identity migration 128 -> 129 release/v3.8.49 tip took slot 128 via #7839 (auto_candidate_overrides) after this branch forked; renumber the new migration and its test references. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
391 lines
13 KiB
TypeScript
391 lines
13 KiB
TypeScript
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-account-analytics-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
process.env.API_KEY_SECRET = "test-account-analytics-secret";
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const localDb = await import("../../src/lib/localDb.ts");
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
|
|
const { resolveUsageAccountIdentity } = await import("../../src/lib/usage/accountIdentity.ts");
|
|
const analyticsRoute = await import("../../src/app/api/usage/analytics/route.ts");
|
|
|
|
function makeRequest() {
|
|
return new Request("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z");
|
|
}
|
|
|
|
async function readAnalytics() {
|
|
const response = await analyticsRoute.GET(makeRequest());
|
|
assert.equal(response.status, 200);
|
|
return (await response.json()) as {
|
|
summary: Record<string, unknown>;
|
|
byAccount: Array<Record<string, unknown>>;
|
|
};
|
|
}
|
|
|
|
async function readAccounts() {
|
|
return (await readAnalytics()).byAccount;
|
|
}
|
|
|
|
test.beforeEach(() => {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
usageHistory.clearPendingRequests();
|
|
});
|
|
|
|
test.after(() => {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
});
|
|
|
|
test("Codex account grouping follows workspace and user identity, not email", async () => {
|
|
await localDb.updatePricing({ codex: { "gpt-5.5": { input: 1, output: 2 } } });
|
|
const specs = [
|
|
{ workspaceId: "workspace-a", chatgptUserId: "user-a", email: "old@example.com" },
|
|
{ workspaceId: "workspace-a", chatgptUserId: "user-b", email: "old@example.com" },
|
|
{ workspaceId: "workspace-b", chatgptUserId: "user-a", email: "old@example.com" },
|
|
];
|
|
|
|
for (const [index, spec] of specs.entries()) {
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "codex",
|
|
authType: "oauth",
|
|
email: spec.email,
|
|
displayName: `Account ${index + 1}`,
|
|
providerSpecificData: spec,
|
|
});
|
|
await usageHistory.saveRequestUsage({
|
|
provider: "codex",
|
|
model: "gpt-5.5",
|
|
connectionId: connection.id as string,
|
|
tokens: { input: 1000, output: 500 },
|
|
timestamp: `2026-01-0${index + 1}T00:00:00.000Z`,
|
|
});
|
|
}
|
|
|
|
const sameUser = await providersDb.createProviderConnection({
|
|
provider: "codex",
|
|
authType: "oauth",
|
|
email: "new@example.com",
|
|
displayName: "Account 1 renamed",
|
|
providerSpecificData: { workspaceId: "workspace-a", chatgptUserId: "user-a" },
|
|
});
|
|
await usageHistory.saveRequestUsage({
|
|
provider: "codex",
|
|
model: "gpt-5.5",
|
|
connectionId: sameUser.id as string,
|
|
tokens: { input: 1000, output: 500 },
|
|
timestamp: "2026-01-04T00:00:00.000Z",
|
|
});
|
|
|
|
const accounts = await readAccounts();
|
|
assert.equal(accounts.length, 3);
|
|
assert.deepEqual(accounts.map((account) => [account.account, account.requests]).sort(), [
|
|
["Account 1 renamed", 2],
|
|
["Account 2", 1],
|
|
["Account 3", 1],
|
|
]);
|
|
assert.equal(
|
|
accounts.reduce((sum, account) => sum + Number(account.cost), 0),
|
|
0.008
|
|
);
|
|
});
|
|
|
|
test("updating an established Codex user or workspace does not reattribute earlier usage", async () => {
|
|
const changes = [
|
|
{
|
|
name: "user boundary",
|
|
before: { workspaceId: "workspace-user", chatgptUserId: "user-a" },
|
|
after: { workspaceId: "workspace-user", chatgptUserId: "user-b" },
|
|
},
|
|
{
|
|
name: "workspace boundary",
|
|
before: { workspaceId: "workspace-a", chatgptUserId: "stable-user" },
|
|
after: { workspaceId: "workspace-b", chatgptUserId: "stable-user" },
|
|
},
|
|
];
|
|
|
|
for (const [index, change] of changes.entries()) {
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "codex",
|
|
authType: "oauth",
|
|
displayName: `${change.name} before`,
|
|
providerSpecificData: change.before,
|
|
});
|
|
await usageHistory.saveRequestUsage({
|
|
provider: "codex",
|
|
model: "gpt-5.5",
|
|
connectionId: connection.id as string,
|
|
tokens: { input: 10, output: 5 },
|
|
timestamp: `2026-02-0${index + 1}T00:00:00.000Z`,
|
|
});
|
|
|
|
await providersDb.updateProviderConnection(connection.id as string, {
|
|
displayName: `${change.name} after`,
|
|
providerSpecificData: change.after,
|
|
});
|
|
await usageHistory.saveRequestUsage({
|
|
provider: "codex",
|
|
model: "gpt-5.5",
|
|
connectionId: connection.id as string,
|
|
tokens: { input: 20, output: 10 },
|
|
timestamp: `2026-02-1${index + 1}T00:00:00.000Z`,
|
|
});
|
|
}
|
|
|
|
const accounts = await readAccounts();
|
|
assert.deepEqual(
|
|
accounts.map((account) => [account.account, account.requests, account.totalTokens]).sort(),
|
|
[
|
|
["user boundary after", 1, 30],
|
|
["user boundary before", 1, 15],
|
|
["workspace boundary after", 1, 30],
|
|
["workspace boundary before", 1, 15],
|
|
]
|
|
);
|
|
});
|
|
|
|
test("deleting and recreating the same Codex account preserves one historical account", async () => {
|
|
await localDb.updatePricing({ codex: { "gpt-5.5": { input: 1, output: 2 } } });
|
|
const identity = {
|
|
workspaceId: "workspace-internal-recreated",
|
|
chatgptUserId: "user-internal-recreated",
|
|
};
|
|
const original = await providersDb.createProviderConnection({
|
|
provider: "codex",
|
|
authType: "oauth",
|
|
email: "member@example.com",
|
|
displayName: "Production Codex <member@example.com>",
|
|
providerSpecificData: identity,
|
|
});
|
|
await usageHistory.saveRequestUsage({
|
|
provider: "codex",
|
|
model: "gpt-5.5",
|
|
connectionId: original.id as string,
|
|
tokens: { input: 1000, output: 500 },
|
|
timestamp: "2026-03-01T00:00:00.000Z",
|
|
});
|
|
|
|
assert.equal(await providersDb.deleteProviderConnection(original.id as string), true);
|
|
const recreated = await providersDb.createProviderConnection({
|
|
provider: "codex",
|
|
authType: "oauth",
|
|
email: "member@example.com",
|
|
providerSpecificData: identity,
|
|
});
|
|
assert.notEqual(recreated.id, original.id);
|
|
await usageHistory.saveRequestUsage({
|
|
provider: "codex",
|
|
model: "gpt-5.5",
|
|
connectionId: recreated.id as string,
|
|
tokens: { input: 1000, output: 500 },
|
|
timestamp: "2026-03-02T00:00:00.000Z",
|
|
});
|
|
|
|
const analytics = await readAnalytics();
|
|
assert.deepEqual(
|
|
analytics.byAccount.map((account) => ({
|
|
account: account.account,
|
|
requests: account.requests,
|
|
promptTokens: account.promptTokens,
|
|
completionTokens: account.completionTokens,
|
|
totalTokens: account.totalTokens,
|
|
cost: account.cost,
|
|
})),
|
|
[
|
|
{
|
|
account: "Production Codex <member@example.com>",
|
|
requests: 2,
|
|
promptTokens: 2000,
|
|
completionTokens: 1000,
|
|
totalTokens: 3000,
|
|
cost: 0.004,
|
|
},
|
|
]
|
|
);
|
|
|
|
const serialized = JSON.stringify(analytics);
|
|
const { accountKey } = resolveUsageAccountIdentity({
|
|
id: recreated.id,
|
|
provider: "codex",
|
|
authType: "oauth",
|
|
email: "member@example.com",
|
|
providerSpecificData: identity,
|
|
});
|
|
for (const privateValue of [
|
|
accountKey,
|
|
identity.workspaceId,
|
|
identity.chatgptUserId,
|
|
recreated.id as string,
|
|
]) {
|
|
assert.equal(serialized.includes(privateValue), false);
|
|
}
|
|
});
|
|
|
|
test("account cost and usage share the same trimmed legacy connection fallback", async () => {
|
|
await localDb.updatePricing({ codex: { "gpt-5.5": { input: 1, output: 2 } } });
|
|
const timestamp = new Date().toISOString();
|
|
core
|
|
.getDbInstance()
|
|
.prepare(
|
|
`INSERT INTO usage_history
|
|
(provider, model, connection_id, account_label, account_label_priority,
|
|
tokens_input, tokens_output, success, timestamp)
|
|
VALUES ('codex', 'gpt-5.5', ' legacy-id ', 'Legacy Codex', 4,
|
|
1000, 500, 1, ?)`
|
|
)
|
|
.run(timestamp);
|
|
|
|
const analytics = await readAnalytics();
|
|
assert.equal(analytics.byAccount.length, 1);
|
|
assert.deepEqual(
|
|
{
|
|
account: analytics.byAccount[0].account,
|
|
requests: analytics.byAccount[0].requests,
|
|
totalTokens: analytics.byAccount[0].totalTokens,
|
|
cost: analytics.byAccount[0].cost,
|
|
},
|
|
{ account: "Legacy Codex", requests: 1, totalTokens: 1500, cost: 0.002 }
|
|
);
|
|
assert.equal(analytics.summary.totalCost, analytics.byAccount[0].cost);
|
|
});
|
|
|
|
test("analytics prefers the highest-priority historical label and reports blank orphan IDs as unknown", async () => {
|
|
await localDb.updatePricing({ codex: { "gpt-5.5": { input: 1, output: 2 } } });
|
|
const db = core.getDbInstance();
|
|
const insert = db.prepare(`INSERT INTO usage_history
|
|
(provider, model, connection_id, account_key, account_label, account_label_priority,
|
|
tokens_input, tokens_output, success, timestamp)
|
|
VALUES (?, 'gpt-5.5', ?, ?, ?, ?, 10, 5, 1, ?)`);
|
|
insert.run(
|
|
"codex",
|
|
"old-uuid",
|
|
"stable-account",
|
|
" Production Codex ",
|
|
4,
|
|
"2026-01-01T00:00:00.000Z"
|
|
);
|
|
insert.run("codex", "new-uuid", "stable-account", " ", 5, "2026-01-02T00:00:00.000Z");
|
|
insert.run("codex", " ", null, null, 0, "2026-01-03T00:00:00.000Z");
|
|
|
|
const analytics = await readAnalytics();
|
|
assert.deepEqual(
|
|
analytics.byAccount.map((account) => [
|
|
account.account,
|
|
account.requests,
|
|
account.totalTokens,
|
|
account.cost,
|
|
]),
|
|
[
|
|
["Production Codex", 2, 30, 0.00004],
|
|
["unknown", 1, 15, 0.00002],
|
|
]
|
|
);
|
|
});
|
|
|
|
test("generic provider and Codex workspace identities stay distinct under a shared email", async () => {
|
|
const accountSpecs = [
|
|
{ provider: "codex", providerSpecificData: { workspaceId: "workspace-a" } },
|
|
{ provider: "codex", providerSpecificData: { workspaceId: "workspace-b" } },
|
|
{ provider: "openai", providerSpecificData: {} },
|
|
];
|
|
|
|
for (const [index, spec] of accountSpecs.entries()) {
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: spec.provider,
|
|
authType: "oauth",
|
|
email: "shared@example.com",
|
|
accessToken: `secret-${index}`,
|
|
providerSpecificData: spec.providerSpecificData,
|
|
});
|
|
await usageHistory.saveRequestUsage({
|
|
provider: spec.provider,
|
|
model: spec.provider === "codex" ? "gpt-5.5" : "gpt-4o",
|
|
connectionId: connection.id as string,
|
|
tokens: { input: 100 + index, output: 50 + index },
|
|
timestamp: `2026-01-0${index + 1}T00:00:00.000Z`,
|
|
});
|
|
}
|
|
|
|
const accounts = await readAccounts();
|
|
// Three separate accounts share one email label; grouping must not collapse
|
|
// them because workspace and provider identities differ.
|
|
assert.equal(accounts.length, 3);
|
|
assert.deepEqual(accounts.map((account) => account.account).sort(), [
|
|
"shared@example.com",
|
|
"shared@example.com",
|
|
"shared@example.com",
|
|
]);
|
|
assert.deepEqual(accounts.map((account) => account.requests).sort(), [1, 1, 1]);
|
|
});
|
|
|
|
test("a nonblank orphaned connection ID stays a truthful fallback label", async () => {
|
|
core
|
|
.getDbInstance()
|
|
.prepare(
|
|
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
)
|
|
.run("codex", "gpt-5.5", "deleted-legacy-uuid", 10, 5, 1, 25, "2026-01-01T00:00:00.000Z");
|
|
|
|
const accounts = await readAccounts();
|
|
assert.equal(accounts.length, 1);
|
|
assert.equal(accounts[0].account, "deleted-legacy-uuid");
|
|
});
|
|
|
|
test("the newest label wins for one account at equal priority", async () => {
|
|
const db = core.getDbInstance();
|
|
const accountKey = resolveUsageAccountIdentity({
|
|
id: "new-uuid",
|
|
provider: "codex",
|
|
authType: "oauth",
|
|
email: "member@example.com",
|
|
providerSpecificData: { chatgptUserId: "user-a" },
|
|
}).accountKey;
|
|
const now = Date.now();
|
|
const insert = db.prepare(
|
|
`INSERT INTO usage_history
|
|
(provider, model, connection_id, account_key, account_label, account_label_priority,
|
|
tokens_input, tokens_output, success, latency_ms, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
);
|
|
insert.run(
|
|
"codex",
|
|
"gpt-5.5",
|
|
"old-uuid",
|
|
accountKey,
|
|
"Zulu before rename",
|
|
4,
|
|
10,
|
|
5,
|
|
1,
|
|
20,
|
|
new Date(now - 60_000).toISOString()
|
|
);
|
|
insert.run(
|
|
"codex",
|
|
"gpt-5.5",
|
|
"new-uuid",
|
|
accountKey,
|
|
"Alpha after rename",
|
|
4,
|
|
20,
|
|
10,
|
|
1,
|
|
30,
|
|
new Date(now).toISOString()
|
|
);
|
|
|
|
const analytics = await readAnalytics();
|
|
assert.equal(analytics.summary.uniqueAccounts, 1);
|
|
assert.equal(analytics.byAccount.length, 1);
|
|
assert.equal(analytics.byAccount[0].account, "Alpha after rename");
|
|
assert.equal(analytics.byAccount[0].requests, 2);
|
|
});
|