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:
chan1103
2026-07-11 22:49:12 +09:00
parent 3df97d5097
commit c2456d882f
8 changed files with 189 additions and 48 deletions
+15 -11
View File
@@ -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)