fix(ai): sized usage requests by unfiltered account batch and updated cache methods

- Updated `RemoteAuthCredentialStore` to track broker usage accounts across snapshots and streams before account-pool filtering.
- Replaced `#countUsageAccounts` with dynamic tracking methods `#replaceBrokerUsageAccounts`, `#upsertBrokerUsageAccount`, and `#removeBrokerUsageAccount`.
- Refactored `AuthStorage.#fetchUsageCached` to accept an options object for `timeoutMs` and `forceRefresh`.
- Updated dockerignore patterns to exclude `**/.venv/` and ensure depth-agnostic `.env` secret exclusion.
This commit is contained in:
can1357
2026-08-14 07:23:02 +02:00
parent 7f56412423
commit a821f4316a
6 changed files with 73 additions and 27 deletions
+6 -2
View File
@@ -74,5 +74,9 @@ out.jsonl
out.html out.html
pi-*.html pi-*.html
# Secrets. Should never be in an image regardless. # Host virtualenvs — the image installs its own interpreter deps.
.env **/.venv/
# Secrets. Should never be in an image regardless. Depth-agnostic: a bare
# `.env` misses nested ones such as `python/robomp/.env`.
**/.env
+6 -2
View File
@@ -75,5 +75,9 @@ out.jsonl
out.html out.html
pi-*.html pi-*.html
# Secrets. Should never be in the image regardless. # Host virtualenvs — the image installs its own interpreter deps.
.env **/.venv/
# Secrets. Should never be in the image regardless. Depth-agnostic: a bare
# `.env` misses `python/robomp/.env`, which `COPY . /pi/` would bake in.
**/.env
+6 -2
View File
@@ -78,8 +78,12 @@ out.jsonl
out.html out.html
pi-*.html pi-*.html
# Secrets. Should never be in the image regardless. # Host virtualenvs — the image installs its own interpreter deps.
.env **/.venv/
# Secrets. Should never be in the image regardless. Depth-agnostic: a bare
# `.env` misses `python/robomp/.env`, which sits next to the copied src tree.
**/.env
# Robomp-only excludes. Natives + wheel + python + bun + rustup all come # Robomp-only excludes. Natives + wheel + python + bun + rustup all come
# from PI_BASE; the web-builder stage only needs root manifests + the # from PI_BASE; the web-builder stage only needs root manifests + the
+1 -1
View File
@@ -4,7 +4,7 @@
### Fixed ### Fixed
- Fixed `omp usage invalidate` to discard stale OAuth and API-key usage snapshots, then force a cache-bypassing, per-provider serialized refresh so upgraded subscriptions do not silently retain pre-change quota data. - Fixed `omp usage invalidate` to discard stale OAuth and API-key usage snapshots, then force a cache-bypassing, per-provider serialized refresh with a broker request budget sized for the full unfiltered account batch, so upgraded subscriptions do not silently retain pre-change quota data.
- Fixed quota reporting and Cookie capture guidance for China (Beijing) Alibaba Token Plan credentials ([#8509](https://github.com/can1357/oh-my-pi/issues/8509)). - Fixed quota reporting and Cookie capture guidance for China (Beijing) Alibaba Token Plan credentials ([#8509](https://github.com/can1357/oh-my-pi/issues/8509)).
## [17.3.3] - 2026-08-14 ## [17.3.3] - 2026-08-14
+44 -12
View File
@@ -253,7 +253,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
#usageInflight?: Promise<UsageReport[] | null>; #usageInflight?: Promise<UsageReport[] | null>;
#credentialBlockReconcileAfter: Map<string, number> = new Map(); #credentialBlockReconcileAfter: Map<string, number> = new Map();
#usageCacheEpoch = 0; #usageCacheEpoch = 0;
#maxUsageAccountsPerProvider = 1; /** Raw broker credentials retained to size aggregate usage requests before account-pool filtering. */
#brokerUsageProviderByCredentialId = new Map<number, Provider>();
#brokerUsageAccountCounts = new Map<Provider, number>();
/** Per-snapshot lookup of oauth credentials by provider; rebuilt when `#snapshot` is replaced. */ /** Per-snapshot lookup of oauth credentials by provider; rebuilt when `#snapshot` is replaced. */
#usageFilterLookup?: { snapshot: SnapshotResponse; byProvider: Map<Provider, OAuthCredential[]> }; #usageFilterLookup?: { snapshot: SnapshotResponse; byProvider: Map<Provider, OAuthCredential[]> };
/** Memoized `#filterUsageReports` output, keyed on (input identity, lookup identity). */ /** Memoized `#filterUsageReports` output, keyed on (input identity, lookup identity). */
@@ -297,7 +299,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
#applySnapshot(snapshot: SnapshotResponse, generation: number, protectNewBlocks = true): void { #applySnapshot(snapshot: SnapshotResponse, generation: number, protectNewBlocks = true): void {
const nowMs = Date.now(); const nowMs = Date.now();
this.#maxUsageAccountsPerProvider = this.#countUsageAccounts(snapshot.credentials); this.#replaceBrokerUsageAccounts(snapshot.credentials);
const previousCredentials = this.#snapshot.credentials; const previousCredentials = this.#snapshot.credentials;
const credentials = snapshot.credentials const credentials = snapshot.credentials
.filter(entry => isCredentialInAccountPool(entry, this.#accountPool)) .filter(entry => isCredentialInAccountPool(entry, this.#accountPool))
@@ -429,8 +431,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
generation: number, generation: number,
serverNowMs: number, serverNowMs: number,
): void { ): void {
this.#upsertBrokerUsageAccount(entry);
if (!isCredentialInAccountPool(entry, this.#accountPool)) { if (!isCredentialInAccountPool(entry, this.#accountPool)) {
this.#removeStreamCredential(entry.id, refresher, generation, serverNowMs); this.#removeStreamCredential(entry.id, refresher, generation, serverNowMs, { retainBrokerUsageAccount: true });
return; return;
} }
const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now()); const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now());
@@ -448,7 +451,14 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
this.#snapshotReceivedAt = Date.now(); this.#snapshotReceivedAt = Date.now();
} }
#removeStreamCredential(id: number, refresher: RefresherSchedule, generation: number, serverNowMs: number): void { #removeStreamCredential(
id: number,
refresher: RefresherSchedule,
generation: number,
serverNowMs: number,
options?: { retainBrokerUsageAccount?: boolean },
): void {
if (!options?.retainBrokerUsageAccount) this.#removeBrokerUsageAccount(id);
const removed = this.#snapshot.credentials.find(entry => entry.id === id); const removed = this.#snapshot.credentials.find(entry => entry.id === id);
if (removed?.blocks && removed.blocks.length > 0) this.#invalidateUsageCache(); if (removed?.blocks && removed.blocks.length > 0) this.#invalidateUsageCache();
const credentials = this.#snapshot.credentials.filter(entry => entry.id !== id); const credentials = this.#snapshot.credentials.filter(entry => entry.id !== id);
@@ -1068,14 +1078,36 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
}); });
} }
#countUsageAccounts(entries: readonly SnapshotEntry[]): number { #replaceBrokerUsageAccounts(entries: readonly SnapshotEntry[]): void {
const counts = new Map<Provider, number>(); this.#brokerUsageProviderByCredentialId.clear();
let maximum = 1; this.#brokerUsageAccountCounts.clear();
for (const entry of entries) { for (const entry of entries) this.#upsertBrokerUsageAccount(entry);
const count = (counts.get(entry.provider) ?? 0) + 1; }
counts.set(entry.provider, count);
maximum = Math.max(maximum, count); #upsertBrokerUsageAccount(entry: Pick<SnapshotEntry, "id" | "provider">): void {
const previous = this.#brokerUsageProviderByCredentialId.get(entry.id);
if (previous === entry.provider) return;
if (previous !== undefined) {
const count = this.#brokerUsageAccountCounts.get(previous) ?? 0;
if (count <= 1) this.#brokerUsageAccountCounts.delete(previous);
else this.#brokerUsageAccountCounts.set(previous, count - 1);
} }
this.#brokerUsageProviderByCredentialId.set(entry.id, entry.provider);
this.#brokerUsageAccountCounts.set(entry.provider, (this.#brokerUsageAccountCounts.get(entry.provider) ?? 0) + 1);
}
#removeBrokerUsageAccount(id: number): void {
const provider = this.#brokerUsageProviderByCredentialId.get(id);
if (provider === undefined) return;
this.#brokerUsageProviderByCredentialId.delete(id);
const count = this.#brokerUsageAccountCounts.get(provider) ?? 0;
if (count <= 1) this.#brokerUsageAccountCounts.delete(provider);
else this.#brokerUsageAccountCounts.set(provider, count - 1);
}
#maxBrokerUsageAccounts(): number {
let maximum = 1;
for (const count of this.#brokerUsageAccountCounts.values()) maximum = Math.max(maximum, count);
return maximum; return maximum;
} }
@@ -1087,7 +1119,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
if (this.#usageInflight) return this.#usageInflight; if (this.#usageInflight) return this.#usageInflight;
const epoch = this.#usageCacheEpoch; const epoch = this.#usageCacheEpoch;
const inflight = this.#client const inflight = this.#client
.fetchUsage({ maxAccountsPerProvider: this.#maxUsageAccountsPerProvider }) .fetchUsage({ maxAccountsPerProvider: this.#maxBrokerUsageAccounts() })
.then(body => { .then(body => {
if (epoch !== this.#usageCacheEpoch) return this.#loadUsageReports(); if (epoch !== this.#usageCacheEpoch) return this.#loadUsageReports();
this.#usageCache = { reports: body.reports, fetchedAt: Date.now() }; this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
+10 -8
View File
@@ -3274,9 +3274,10 @@ export class AuthStorage {
async #fetchUsageCached( async #fetchUsageCached(
request: UsageRequestDescriptor, request: UsageRequestDescriptor,
timeoutMs?: number, options: { timeoutMs?: number; forceRefresh?: boolean } = {},
forceRefresh = false,
): Promise<UsageReport | null> { ): Promise<UsageReport | null> {
const timeoutMs = options.timeoutMs;
const forceRefresh = options.forceRefresh ?? false;
const cacheKey = this.#buildUsageReportCacheKey(request); const cacheKey = this.#buildUsageReportCacheKey(request);
const now = Date.now(); const now = Date.now();
const cached = forceRefresh ? undefined : this.#usageCache.get<UsageReport | null>(cacheKey); const cached = forceRefresh ? undefined : this.#usageCache.get<UsageReport | null>(cacheKey);
@@ -3797,10 +3798,9 @@ export class AuthStorage {
if (!resolvedApiKey) return null; if (!resolvedApiKey) return null;
usageCredential.apiKey = resolvedApiKey; usageCredential.apiKey = resolvedApiKey;
} }
return this.#fetchUsageCached( return this.#fetchUsageCached(this.#buildUsageRequest(provider, usageCredential, options?.baseUrl), {
this.#buildUsageRequest(provider, usageCredential, options?.baseUrl), timeoutMs: options?.timeoutMs ?? this.#usageRequestTimeoutMs,
options?.timeoutMs ?? this.#usageRequestTimeoutMs, });
);
} }
/** /**
@@ -4007,10 +4007,12 @@ export class AuthStorage {
requests.map(request => { requests.map(request => {
const forceRefresh = serializedProviders.has(request.provider); const forceRefresh = serializedProviders.has(request.provider);
if (!forceRefresh) { if (!forceRefresh) {
return this.#fetchUsageCached(request, this.#usageRequestTimeoutMs); return this.#fetchUsageCached(request, { timeoutMs: this.#usageRequestTimeoutMs });
} }
const tail = tails.get(request.provider) ?? Promise.resolve(); const tail = tails.get(request.provider) ?? Promise.resolve();
const current = tail.then(() => this.#fetchUsageCached(request, this.#usageRequestTimeoutMs, true)); const current = tail.then(() =>
this.#fetchUsageCached(request, { timeoutMs: this.#usageRequestTimeoutMs, forceRefresh: true }),
);
tails.set( tails.set(
request.provider, request.provider,
current.then( current.then(