fix(ai): org-qualify account/project fallback identities; org-decisive routing on either side
Addresses the fourth review round (Codex no-email finding on d37e3992c, confirmed and scoped by internal review): - resolveProviderCredentialIdentityKey: the anthropic org qualifier now rides on whichever base identity exists (email > account > project), not only email. The account UUID is identical across the orgs of one login account, so the bare account fallback let a second subscription replace the first whenever the email could not be recovered (token response omits it AND bootstrap fails). Org-only credentials key on the org alone instead of losing identity entirely. - matchesReplacementCredential: the one-way legacy claim strips a trailing |org: from ANY anthropic base key (account/project included); only anthropic keys carry the qualifier, so other providers are unaffected. - Usage-report dedupe falls back to the org-qualified account for no-email anthropic reports instead of returning no identifiers. - Broker report/overlay routing (matchUsageReport/findMatchingReportIndex) is org-decisive on EITHER side: an org-less legacy credential no longer receives an org-attributed sibling's pool via the lone-candidate or email/account fallback, and an org-less overlay only merges into org-less reports. - omp usage unreported-account attribution follows the same either-side rule, so a legacy row whose fetch failed surfaces as 'no usage data' instead of being hidden by a sibling's report. - Regression tests: no-email identity coexistence/replace/claim, no-email report dedupe, org-less broker routing, either-side unreported attribution.
This commit is contained in:
@@ -298,17 +298,21 @@ export function collectUnreportedAccounts(
|
||||
const providerReports = byProvider.get(account.provider) ?? [];
|
||||
if (providerReports.length === 0) return true;
|
||||
if (account.type === "api_key") return false;
|
||||
// Org-scoped account (Anthropic multi-subscription): when reports carry
|
||||
// org identity, attribution must match on the org — the shared email
|
||||
// would otherwise mark BOTH subscriptions as covered by one report.
|
||||
if (account.orgId) {
|
||||
const orgId = account.orgId.toLowerCase();
|
||||
const reportedOrgs = new Set<string>();
|
||||
for (const report of providerReports) {
|
||||
const metaOrg = report.metadata?.orgId;
|
||||
if (typeof metaOrg === "string" && metaOrg) reportedOrgs.add(metaOrg.toLowerCase());
|
||||
}
|
||||
if (reportedOrgs.size > 0) return !reportedOrgs.has(orgId);
|
||||
// Org-decisive attribution when EITHER side carries an org (Anthropic
|
||||
// multi-subscription): two orgs share every other identifier, so an
|
||||
// org-scoped account is covered only by its own org's report, and an
|
||||
// org-less legacy account is never covered by an org-attributed sibling
|
||||
// report — its own fetch failing must surface as "no usage data". The
|
||||
// email/account fallback below applies only when both sides are
|
||||
// org-less.
|
||||
const accountOrg = account.orgId?.toLowerCase();
|
||||
const reportedOrgs = new Set<string>();
|
||||
for (const report of providerReports) {
|
||||
const metaOrg = report.metadata?.orgId;
|
||||
if (typeof metaOrg === "string" && metaOrg) reportedOrgs.add(metaOrg.toLowerCase());
|
||||
}
|
||||
if (accountOrg || reportedOrgs.size > 0) {
|
||||
return !(accountOrg !== undefined && reportedOrgs.has(accountOrg));
|
||||
}
|
||||
const ids = [account.email, account.accountId, account.projectId]
|
||||
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
||||
|
||||
Reference in New Issue
Block a user