@oh-my-pi/pi-ai 18.4.0 → 18.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +12 -0
- package/dist/types/auth/oauth.d.ts +2 -2
- package/dist/types/auth/refresh.d.ts +3 -3
- package/dist/types/auth/store.d.ts +4 -2
- package/dist/types/auth/types.d.ts +20 -3
- package/dist/types/auth/usage-cache.d.ts +9 -2
- package/dist/types/auth/usage.d.ts +6 -5
- package/dist/types/auth-broker/client.d.ts +2 -2
- package/dist/types/auth-broker/remote-store.d.ts +2 -2
- package/dist/types/error/flags.d.ts +9 -2
- package/dist/types/error/rate-limit.d.ts +3 -2
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -18
- package/dist/types/stream.d.ts +7 -0
- package/dist/types/usage/cursor.d.ts +3 -1
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/cascade.ts +1 -0
- package/src/auth/oauth.ts +4 -3
- package/src/auth/refresh.ts +105 -16
- package/src/auth/resets.ts +6 -2
- package/src/auth/select.ts +11 -1
- package/src/auth/store.ts +4 -0
- package/src/auth/types.ts +22 -2
- package/src/auth/usage-cache.ts +12 -8
- package/src/auth/usage.ts +45 -19
- package/src/auth-broker/client.ts +8 -3
- package/src/auth-broker/remote-store.ts +4 -2
- package/src/auth-broker/server.ts +7 -1
- package/src/auth-gateway/dispatch.ts +1 -0
- package/src/auth-retry.ts +5 -1
- package/src/error/flags.ts +10 -3
- package/src/error/rate-limit.ts +23 -3
- package/src/providers/anthropic.ts +62 -53
- package/src/providers/cowork-fetch.ts +45 -3
- package/src/providers/cursor/exec-modern.ts +1 -1
- package/src/providers/cursor-pi-args.ts +13 -18
- package/src/providers/cursor.ts +179 -57
- package/src/providers/devin.ts +43 -13
- package/src/providers/openai-completions.ts +122 -30
- package/src/stream.ts +77 -16
- package/src/usage/claude.ts +5 -0
- package/src/usage/cursor.ts +43 -1
- package/src/usage/devin.ts +8 -7
- package/src/usage/registry.ts +2 -1
- package/src/usage/zai.ts +20 -0
- package/src/usage.ts +6 -0
package/src/stream.ts
CHANGED
|
@@ -175,6 +175,9 @@ const PROVIDER_INFLIGHT_HEARTBEAT_MS = 5_000;
|
|
|
175
175
|
const PROVIDER_INFLIGHT_SIGNAL_FALLBACK_MS = 250;
|
|
176
176
|
const PROVIDER_INFLIGHT_HEARTBEAT_FLUSH_TIMEOUT_MS = 1_000;
|
|
177
177
|
const PROVIDER_INFLIGHT_RELEASE_TIMEOUT_MS = 5_000;
|
|
178
|
+
const PROVIDER_INFLIGHT_LOCK_RETRY_INITIAL_MS = 25;
|
|
179
|
+
const PROVIDER_INFLIGHT_LOCK_RETRY_MAX_MS = 250;
|
|
180
|
+
const PROVIDER_INFLIGHT_LOCK_RETRY_BUDGET_MS = 3_000;
|
|
178
181
|
|
|
179
182
|
let configuredProviderMaxInFlightRequests: Record<string, number> = {};
|
|
180
183
|
let providerInFlightRootOverride: string | undefined;
|
|
@@ -187,6 +190,9 @@ let providerInFlightLeaseRemoverOverride: ((leasePath: string) => Promise<void>)
|
|
|
187
190
|
let providerInFlightWaitObserverOverride: ((provider: string) => void) | undefined;
|
|
188
191
|
let providerInFlightLockCreatedObserverOverride: ((lockDir: string) => Promise<void>) | undefined;
|
|
189
192
|
let providerInFlightLockIdentifiedObserverOverride: ((lockDir: string) => Promise<void>) | undefined;
|
|
193
|
+
let providerInFlightLockMkdirOverride: ((lockDir: string) => Promise<void>) | undefined;
|
|
194
|
+
let providerInFlightLockPlatformOverride: NodeJS.Platform | undefined;
|
|
195
|
+
let providerInFlightLockRetryTimings: { budgetMs?: number; initialDelayMs?: number; maxDelayMs?: number } | undefined;
|
|
190
196
|
|
|
191
197
|
export function configureProviderMaxInFlightRequests(limits: Record<string, number> | undefined): void {
|
|
192
198
|
configuredProviderMaxInFlightRequests = limits ?? {};
|
|
@@ -351,6 +357,42 @@ async function releaseProviderInFlightLockDirIfSame(
|
|
|
351
357
|
} catch {}
|
|
352
358
|
}
|
|
353
359
|
|
|
360
|
+
// On Windows, mkdir on a lock directory another process is concurrently deleting
|
|
361
|
+
// (delete-pending while a watcher, antivirus, or indexer still holds it) fails
|
|
362
|
+
// with ERROR_ACCESS_DENIED — mapped to EPERM/EACCES — instead of EEXIST, so
|
|
363
|
+
// ordinary contention looks like a permission error. On win32 only, back off and
|
|
364
|
+
// retry briefly; if the failure persists past the budget, rethrow the original
|
|
365
|
+
// error so a real permission problem still surfaces. Every other outcome
|
|
366
|
+
// (success, EEXIST, any other code) exits immediately, keeping non-win32 and
|
|
367
|
+
// non-transient behavior unchanged.
|
|
368
|
+
function isTransientProviderInFlightLockMkdirError(error: NodeJS.ErrnoException): boolean {
|
|
369
|
+
if ((providerInFlightLockPlatformOverride ?? process.platform) !== "win32") return false;
|
|
370
|
+
return error.code === "EPERM" || error.code === "EACCES";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function mkdirProviderInFlightLockDir(lockDir: string, signal?: AbortSignal): Promise<void> {
|
|
374
|
+
const mkdir = providerInFlightLockMkdirOverride ?? ((dir: string) => fs.mkdir(dir));
|
|
375
|
+
let failures = 0;
|
|
376
|
+
let since = Date.now();
|
|
377
|
+
while (true) {
|
|
378
|
+
try {
|
|
379
|
+
await mkdir(lockDir);
|
|
380
|
+
return;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (!isTransientProviderInFlightLockMkdirError(error as NodeJS.ErrnoException)) throw error;
|
|
383
|
+
const now = Date.now();
|
|
384
|
+
if (failures === 0) since = now;
|
|
385
|
+
const budgetMs = providerInFlightLockRetryTimings?.budgetMs ?? PROVIDER_INFLIGHT_LOCK_RETRY_BUDGET_MS;
|
|
386
|
+
if (now - since >= budgetMs) throw error;
|
|
387
|
+
const initialMs = providerInFlightLockRetryTimings?.initialDelayMs ?? PROVIDER_INFLIGHT_LOCK_RETRY_INITIAL_MS;
|
|
388
|
+
const maxMs = providerInFlightLockRetryTimings?.maxDelayMs ?? PROVIDER_INFLIGHT_LOCK_RETRY_MAX_MS;
|
|
389
|
+
const delayMs = Math.min(maxMs, initialMs * 2 ** failures);
|
|
390
|
+
failures++;
|
|
391
|
+
await untilAborted(signal, Bun.sleep(delayMs));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
354
396
|
async function acquireProviderInFlightLock(provider: string, signal?: AbortSignal): Promise<() => Promise<void>> {
|
|
355
397
|
const lockDir = providerInFlightLockDir(provider);
|
|
356
398
|
await fs.mkdir(path.dirname(lockDir), { recursive: true });
|
|
@@ -358,7 +400,7 @@ async function acquireProviderInFlightLock(provider: string, signal?: AbortSigna
|
|
|
358
400
|
while (true) {
|
|
359
401
|
if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
360
402
|
try {
|
|
361
|
-
await
|
|
403
|
+
await mkdirProviderInFlightLockDir(lockDir, signal);
|
|
362
404
|
await providerInFlightLockCreatedObserverOverride?.(lockDir);
|
|
363
405
|
let lockIdentity: ProviderInFlightLockIdentity;
|
|
364
406
|
try {
|
|
@@ -632,6 +674,15 @@ export const __providerInFlightForTesting = {
|
|
|
632
674
|
setLockIdentifiedObserver(observer: ((lockDir: string) => Promise<void>) | undefined): void {
|
|
633
675
|
providerInFlightLockIdentifiedObserverOverride = observer;
|
|
634
676
|
},
|
|
677
|
+
setLockMkdirOverride(mkdir: ((lockDir: string) => Promise<void>) | undefined): void {
|
|
678
|
+
providerInFlightLockMkdirOverride = mkdir;
|
|
679
|
+
},
|
|
680
|
+
setLockPlatformOverride(platform: NodeJS.Platform | undefined): void {
|
|
681
|
+
providerInFlightLockPlatformOverride = platform;
|
|
682
|
+
},
|
|
683
|
+
setLockRetryTimings(timings: { budgetMs?: number; initialDelayMs?: number; maxDelayMs?: number } | undefined): void {
|
|
684
|
+
providerInFlightLockRetryTimings = timings;
|
|
685
|
+
},
|
|
635
686
|
providerDir(provider: string): string {
|
|
636
687
|
return providerInFlightDir(provider);
|
|
637
688
|
},
|
|
@@ -1881,29 +1932,39 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1881
1932
|
}
|
|
1882
1933
|
|
|
1883
1934
|
if (ANTHROPIC_USE_INTERLEAVED_THINKING) {
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
maxTokens
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1935
|
+
if (
|
|
1936
|
+
model.maxTokens !== null &&
|
|
1937
|
+
model.maxTokens !== undefined &&
|
|
1938
|
+
model.maxTokens < thinkingBudget + OUTPUT_FALLBACK_BUFFER
|
|
1939
|
+
) {
|
|
1940
|
+
thinkingBudget = model.maxTokens - OUTPUT_FALLBACK_BUFFER;
|
|
1941
|
+
}
|
|
1942
|
+
if (thinkingBudget >= ANTHROPIC_THINKING.minimal) {
|
|
1943
|
+
return castApi<"anthropic-messages">({
|
|
1944
|
+
...base,
|
|
1945
|
+
maxTokens: maxTokensWithThinking,
|
|
1946
|
+
requestModelId: resolveWireModelId(model, reasoning),
|
|
1947
|
+
thinkingEnabled: true,
|
|
1948
|
+
thinkingBudgetTokens: thinkingBudget,
|
|
1949
|
+
effort,
|
|
1950
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1951
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1952
|
+
serviceTier: options?.serviceTier,
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1895
1955
|
}
|
|
1896
1956
|
|
|
1897
1957
|
// Caller's maxTokens is desired output, so add thinking budget on top. With no caller/model cap, use a finite total fallback.
|
|
1898
1958
|
const maxTokens = maxTokensWithThinkingBudget(base.maxTokens, model.maxTokens, thinkingBudget);
|
|
1899
1959
|
|
|
1900
|
-
//
|
|
1901
|
-
|
|
1902
|
-
|
|
1960
|
+
// Keep the provider's output buffer after thinking, reducing the
|
|
1961
|
+
// budget before its wire-level clamp could fall below the API minimum.
|
|
1962
|
+
if (maxTokens < thinkingBudget + OUTPUT_FALLBACK_BUFFER) {
|
|
1963
|
+
thinkingBudget = maxTokens - OUTPUT_FALLBACK_BUFFER;
|
|
1903
1964
|
}
|
|
1904
1965
|
|
|
1905
1966
|
// If thinking budget is too low, disable thinking
|
|
1906
|
-
if (thinkingBudget
|
|
1967
|
+
if (thinkingBudget < ANTHROPIC_THINKING.minimal) {
|
|
1907
1968
|
return castApi<"anthropic-messages">({
|
|
1908
1969
|
...base,
|
|
1909
1970
|
requestModelId: resolveWireModelId(model, undefined),
|
package/src/usage/claude.ts
CHANGED
|
@@ -702,6 +702,11 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
702
702
|
const { orgId, baseUrl: _baseUrl, report: _report, ...resetCredits } = resetCreditList;
|
|
703
703
|
result.resetCredits = resetCredits;
|
|
704
704
|
if (orgId) result.metadata = { ...result.metadata, orgId };
|
|
705
|
+
} else if (ctx.previousReport?.resetCredits) {
|
|
706
|
+
// `null` means the reset probe failed (timeout, 429, malformed body), not
|
|
707
|
+
// that the account has no saved resets. Keep the block this credential
|
|
708
|
+
// last reported instead of dropping banked resets from the report.
|
|
709
|
+
result.resetCredits = ctx.previousReport.resetCredits;
|
|
705
710
|
}
|
|
706
711
|
result.metadata = {
|
|
707
712
|
...result.metadata,
|
package/src/usage/cursor.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { quotaTierFor } from "@oh-my-pi/pi-catalog/compat/behavior";
|
|
1
2
|
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
2
3
|
import { extractCursorAccessTokenUserId } from "../registry/oauth/cursor";
|
|
3
4
|
import type {
|
|
5
|
+
CredentialRankingContext,
|
|
6
|
+
CredentialRankingStrategy,
|
|
4
7
|
UsageAmount,
|
|
5
8
|
UsageFetchContext,
|
|
6
9
|
UsageFetchParams,
|
|
@@ -10,7 +13,7 @@ import type {
|
|
|
10
13
|
UsageWindow,
|
|
11
14
|
} from "../usage";
|
|
12
15
|
import { isRecord } from "../utils";
|
|
13
|
-
import { parseIsoTimestamp, usageStatus } from "./shared";
|
|
16
|
+
import { DAY_MS, parseIsoTimestamp, usageStatus } from "./shared";
|
|
14
17
|
|
|
15
18
|
function parseTimestamp(value: unknown): number | undefined {
|
|
16
19
|
const numeric = toNumber(value);
|
|
@@ -352,6 +355,45 @@ export function parseCursorUsage(payload: unknown, fetchedAt = Date.now()): Usag
|
|
|
352
355
|
};
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
const CURSOR_MODELS_LIMIT_ID = "cursor:usd:individual-auto";
|
|
359
|
+
const OTHER_MODELS_LIMIT_ID = "cursor:usd:individual-api";
|
|
360
|
+
|
|
361
|
+
function scopeCursorLimitsForModel(report: UsageReport, context: CredentialRankingContext | undefined): UsageLimit[] {
|
|
362
|
+
const splitLimits = report.limits.filter(
|
|
363
|
+
limit => limit.id === CURSOR_MODELS_LIMIT_ID || limit.id === OTHER_MODELS_LIMIT_ID,
|
|
364
|
+
);
|
|
365
|
+
if (splitLimits.length > 0) {
|
|
366
|
+
const modelId = context?.modelId;
|
|
367
|
+
if (!modelId) return [];
|
|
368
|
+
const tier = quotaTierFor("cursor", modelId);
|
|
369
|
+
const limitId = tier === "auto" ? CURSOR_MODELS_LIMIT_ID : tier === "api" ? OTHER_MODELS_LIMIT_ID : undefined;
|
|
370
|
+
return limitId ? splitLimits.filter(limit => limit.id === limitId) : [];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const combinedLimits = report.limits.filter(
|
|
374
|
+
limit => limit.id === "cursor:usd:individual-plan" || limit.id === "cursor:usd:individual-overall",
|
|
375
|
+
);
|
|
376
|
+
return combinedLimits.length > 0 ? combinedLimits : report.limits;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Routes Cursor credential ranking and reserve checks through the requested model's billing pool. */
|
|
380
|
+
export const cursorRankingStrategy: CredentialRankingStrategy = {
|
|
381
|
+
findWindowLimits(report, context) {
|
|
382
|
+
return { secondary: scopeCursorLimitsForModel(report, context)[0] };
|
|
383
|
+
},
|
|
384
|
+
scopeLimits: scopeCursorLimitsForModel,
|
|
385
|
+
scopeLimitsForReserve: scopeCursorLimitsForModel,
|
|
386
|
+
// Back off per billing pool so an exhausted Other Models pool does not
|
|
387
|
+
// block Grok/Composer on the same account (and vice versa).
|
|
388
|
+
blockScope(context) {
|
|
389
|
+
return context?.modelId ? `pool:${quotaTierFor("cursor", context.modelId)}` : undefined;
|
|
390
|
+
},
|
|
391
|
+
windowDefaults: {
|
|
392
|
+
primaryMs: 30 * DAY_MS,
|
|
393
|
+
secondaryMs: 30 * DAY_MS,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
|
|
355
397
|
export const cursorUsageProvider: UsageProvider = {
|
|
356
398
|
id: "cursor",
|
|
357
399
|
supports(params: UsageFetchParams): boolean {
|
package/src/usage/devin.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type Timestamp,
|
|
22
22
|
} from "@oh-my-pi/pi-catalog/discovery/devin-proto";
|
|
23
23
|
import { create, toBinary } from "@oh-my-pi/pi-catalog/discovery/protobuf";
|
|
24
|
-
import { DEVIN_DEFAULT_BASE_URL, devinCliMetadata,
|
|
24
|
+
import { DEVIN_DEFAULT_BASE_URL, devinCliMetadata, devinWireMetadata } from "@oh-my-pi/pi-catalog/wire/devin";
|
|
25
25
|
import { decodeDevinUnaryMessage } from "@oh-my-pi/pi-catalog/wire/devin-proto";
|
|
26
26
|
import type {
|
|
27
27
|
UsageAmount,
|
|
@@ -48,11 +48,11 @@ function timestampMs(timestamp: Timestamp): number {
|
|
|
48
48
|
return Number(timestamp.seconds) * 1_000 + timestamp.nanos / 1_000_000;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
function
|
|
51
|
+
/** Credential bytes exactly as their storage kind requires on the Cascade wire. */
|
|
52
|
+
function devinCredential(credential: UsageCredential): string | undefined {
|
|
53
53
|
const raw = credential.type === "oauth" ? credential.accessToken : credential.apiKey;
|
|
54
54
|
const token = raw?.trim();
|
|
55
|
-
return token
|
|
55
|
+
return token || undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** `TEAMS_TIER_DEVIN_PRO` → `Devin Pro`, for plans that ship no `plan_name`. */
|
|
@@ -261,13 +261,14 @@ function buildDevinReport(response: GetUserStatusResponse): UsageReport | null {
|
|
|
261
261
|
|
|
262
262
|
async function fetchDevinUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
|
263
263
|
if (params.provider !== PROVIDER) return null;
|
|
264
|
-
const token =
|
|
264
|
+
const token = devinCredential(params.credential);
|
|
265
265
|
if (!token) return null;
|
|
266
266
|
const baseUrl = (params.baseUrl ?? DEVIN_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
267
267
|
|
|
268
|
+
const metadata = params.credential.type === "oauth" ? devinCliMetadata(token) : devinWireMetadata(token);
|
|
268
269
|
try {
|
|
269
270
|
const request = create(GetUserStatusRequestSchema, {
|
|
270
|
-
metadata: create(MetadataSchema,
|
|
271
|
+
metadata: create(MetadataSchema, metadata),
|
|
271
272
|
});
|
|
272
273
|
const response = await ctx.fetch(`${baseUrl}${GET_USER_STATUS_PATH}`, {
|
|
273
274
|
method: "POST",
|
|
@@ -305,6 +306,6 @@ async function fetchDevinUsage(params: UsageFetchParams, ctx: UsageFetchContext)
|
|
|
305
306
|
export const devinUsageProvider: UsageProvider = {
|
|
306
307
|
id: PROVIDER,
|
|
307
308
|
fetchUsage: fetchDevinUsage,
|
|
308
|
-
supports: params => params.provider === PROVIDER &&
|
|
309
|
+
supports: params => params.provider === PROVIDER && devinCredential(params.credential) !== undefined,
|
|
309
310
|
validatesCredentials: true,
|
|
310
311
|
};
|
package/src/usage/registry.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { alibabaTokenPlanRankingStrategy, alibabaTokenPlanUsageProvider } from "
|
|
|
4
4
|
import { charmHyperUsageProvider } from "./charm-hyper";
|
|
5
5
|
import { claudeRankingStrategy, claudeUsageProvider } from "./claude";
|
|
6
6
|
import { clinePassUsageProvider } from "./cline-pass";
|
|
7
|
-
import { cursorUsageProvider } from "./cursor";
|
|
7
|
+
import { cursorRankingStrategy, cursorUsageProvider } from "./cursor";
|
|
8
8
|
import { devinUsageProvider } from "./devin";
|
|
9
9
|
import { googleGeminiCliUsageProvider } from "./gemini";
|
|
10
10
|
import { githubCopilotUsageProvider } from "./github-copilot";
|
|
@@ -60,6 +60,7 @@ const DEFAULT_RANKING_STRATEGIES = new Map<Provider, CredentialRankingStrategy>(
|
|
|
60
60
|
["alibaba-token-plan", alibabaTokenPlanRankingStrategy],
|
|
61
61
|
["openai-codex", codexRankingStrategy],
|
|
62
62
|
["anthropic", claudeRankingStrategy],
|
|
63
|
+
["cursor", cursorRankingStrategy],
|
|
63
64
|
["google-antigravity", antigravityRankingStrategy],
|
|
64
65
|
["kimi-code", kimiRankingStrategy],
|
|
65
66
|
["zai", zaiRankingStrategy],
|
package/src/usage/zai.ts
CHANGED
|
@@ -390,6 +390,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
390
390
|
return report;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
/** Fetches the account-wide quota windows for ZAI credentials. */
|
|
393
394
|
export const zaiUsageProvider: UsageProvider = {
|
|
394
395
|
id: "zai",
|
|
395
396
|
cacheVersion: 2,
|
|
@@ -399,7 +400,10 @@ export const zaiUsageProvider: UsageProvider = {
|
|
|
399
400
|
(params.credential.type === "oauth" ? Boolean(params.credential.accessToken) : Boolean(params.credential.apiKey)),
|
|
400
401
|
};
|
|
401
402
|
|
|
403
|
+
/** Ranks ZAI credentials and identifies quota blocks a live report can heal. */
|
|
402
404
|
export const zaiRankingStrategy: CredentialRankingStrategy = {
|
|
405
|
+
// Pre-scoping ZAI quota blocks are unscoped; let the usage preflight probe them.
|
|
406
|
+
healsGlobalBlocks: true,
|
|
403
407
|
findWindowLimits(report) {
|
|
404
408
|
const ranked = rankZaiRequestLimits(report);
|
|
405
409
|
return { primary: ranked[0], secondary: ranked[1] };
|
|
@@ -408,6 +412,22 @@ export const zaiRankingStrategy: CredentialRankingStrategy = {
|
|
|
408
412
|
const limits = getZaiCredentialLimits(report);
|
|
409
413
|
return limits;
|
|
410
414
|
},
|
|
415
|
+
// All GLM requests share the account's quota pool; keep reactive blocks
|
|
416
|
+
// separate from provider-wide failures that a usage report cannot heal.
|
|
417
|
+
blockScope() {
|
|
418
|
+
return "credits";
|
|
419
|
+
},
|
|
420
|
+
healableBlockScopes(report) {
|
|
421
|
+
const limits = getZaiCredentialLimits(report);
|
|
422
|
+
// A feature-only report cannot prove the shared credit/token pool recovered.
|
|
423
|
+
if (!limits.some(limit => limit.id.startsWith("zai:credits:") || limit.id.startsWith("zai:tokens:"))) {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
return [
|
|
427
|
+
{ blockScope: "credits", limits },
|
|
428
|
+
{ blockScope: "", limits },
|
|
429
|
+
];
|
|
430
|
+
},
|
|
411
431
|
windowDefaults: {
|
|
412
432
|
primaryMs: 5 * HOUR_MS,
|
|
413
433
|
secondaryMs: WEEK_MS,
|
package/src/usage.ts
CHANGED
|
@@ -396,6 +396,12 @@ export interface UsageFetchContext {
|
|
|
396
396
|
fetch: FetchImpl;
|
|
397
397
|
logger?: UsageLogger;
|
|
398
398
|
retryWait?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
399
|
+
/**
|
|
400
|
+
* Last report cached for this exact credential cache key, when one exists.
|
|
401
|
+
* Lets a fetcher keep a field it could not re-read this time (a failed
|
|
402
|
+
* secondary probe) instead of reporting it as absent.
|
|
403
|
+
*/
|
|
404
|
+
previousReport?: UsageReport;
|
|
399
405
|
}
|
|
400
406
|
|
|
401
407
|
/** Provider implementation for fetching usage information. */
|