@cortexkit/anthropic-auth-core 1.20.0 → 1.21.0
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/README.md +1 -1
- package/dist/accounts.d.ts +24 -14
- package/dist/accounts.js +234 -143
- package/dist/claude-code.d.ts +3 -1
- package/dist/claude-code.js +124 -21
- package/dist/dump.d.ts +1 -0
- package/dist/dump.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/oauth-profile.d.ts +2 -1
- package/dist/oauth-profile.js +10 -4
- package/dist/quota-header-feed.d.ts +40 -0
- package/dist/quota-header-feed.js +248 -0
- package/dist/quota-headers.js +6 -4
- package/dist/quota-manager.d.ts +36 -27
- package/dist/quota-manager.js +375 -144
- package/dist/sticky-routing.d.ts +14 -2
- package/dist/sticky-routing.js +30 -13
- package/package.json +1 -1
package/dist/claude-code.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export type ClaudeCodeIdentity = {
|
|
2
2
|
deviceId: string;
|
|
3
|
+
accountIdentity?: string;
|
|
3
4
|
accountUuid?: string;
|
|
4
5
|
sessionId: string;
|
|
5
6
|
};
|
|
6
7
|
export declare function setBounded<K, V>(map: Map<K, V>, key: K, value: V, limit?: number): void;
|
|
7
8
|
export declare function getClaudeCodeIdentity(seed: string): ClaudeCodeIdentity;
|
|
8
|
-
export declare function
|
|
9
|
+
export declare function resetClaudeCodeIdentityCachesForTest(): void;
|
|
10
|
+
export declare function resolveClaudeCodeIdentity(accessToken: string, model: string | undefined, accountIdentity: string | undefined): Promise<ClaudeCodeIdentity>;
|
|
9
11
|
export declare function buildClaudeCodeMetadataUserId(identity: ClaudeCodeIdentity): string | null;
|
|
10
12
|
export declare function applyClaudeCodeMetadata(body: Record<string, unknown>, identity: ClaudeCodeIdentity): boolean;
|
|
11
13
|
export declare const CLAUDE_CODE_FULL_AGENT_BETAS: readonly ['oauth-2025-04-20', 'interleaved-thinking-2025-05-14', 'thinking-token-count-2026-05-13', 'context-management-2025-06-27', 'prompt-caching-scope-2026-01-05', 'claude-code-20250219', 'advisor-tool-2026-03-01', 'advanced-tool-use-2025-11-20', 'extended-cache-ttl-2025-04-11', 'cache-diagnosis-2026-04-07'];
|
package/dist/claude-code.js
CHANGED
|
@@ -10,6 +10,16 @@ export function setBounded(map, key, value, limit = IDENTITY_CACHE_LIMIT) {
|
|
|
10
10
|
}
|
|
11
11
|
map.set(key, value);
|
|
12
12
|
}
|
|
13
|
+
function clearCachedAccountUuid(key, identity) {
|
|
14
|
+
if (!identity.accountUuid)
|
|
15
|
+
return identity;
|
|
16
|
+
if (identityCache.get(key) !== identity) {
|
|
17
|
+
return { ...identity, accountUuid: undefined };
|
|
18
|
+
}
|
|
19
|
+
const cleared = { ...identity, accountUuid: undefined };
|
|
20
|
+
setBounded(identityCache, key, cleared);
|
|
21
|
+
return cleared;
|
|
22
|
+
}
|
|
13
23
|
export function getClaudeCodeIdentity(seed) {
|
|
14
24
|
const cacheKey = seed || 'anonymous';
|
|
15
25
|
const cached = identityCache.get(cacheKey);
|
|
@@ -26,6 +36,46 @@ const BOOTSTRAP_IDENTITY_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
|
26
36
|
const BOOTSTRAP_IDENTITY_NEGATIVE_TTL_MS = 5 * 60_000;
|
|
27
37
|
const bootstrapFetches = new Map();
|
|
28
38
|
const bootstrapResults = new Map();
|
|
39
|
+
export function resetClaudeCodeIdentityCachesForTest() {
|
|
40
|
+
identityCache.clear();
|
|
41
|
+
bootstrapFetches.clear();
|
|
42
|
+
bootstrapResults.clear();
|
|
43
|
+
}
|
|
44
|
+
function compatibilityCacheKey(accessToken) {
|
|
45
|
+
return `compat:${accessToken || 'anonymous'}`;
|
|
46
|
+
}
|
|
47
|
+
function explicitCacheKey(accountIdentity) {
|
|
48
|
+
return `identity:${accountIdentity}`;
|
|
49
|
+
}
|
|
50
|
+
function accountCacheKey(accountUuid, accountIdentity) {
|
|
51
|
+
return `account:${accountUuid}:${accountIdentity}`;
|
|
52
|
+
}
|
|
53
|
+
function adoptCachedIdentity(cached, accountIdentity, accountUuid) {
|
|
54
|
+
if (!cached)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (cached.accountIdentity !== undefined &&
|
|
57
|
+
cached.accountIdentity !== accountIdentity) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (accountUuid !== undefined &&
|
|
61
|
+
cached.accountUuid !== undefined &&
|
|
62
|
+
cached.accountUuid !== accountUuid) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (cached.accountIdentity === accountIdentity &&
|
|
66
|
+
(accountUuid === undefined || cached.accountUuid === accountUuid)) {
|
|
67
|
+
return cached;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...cached,
|
|
71
|
+
accountIdentity,
|
|
72
|
+
...(accountUuid !== undefined && { accountUuid }),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function cacheAccountIdentity(identity, accountIdentity, accountUuid) {
|
|
76
|
+
setBounded(identityCache, explicitCacheKey(accountIdentity), identity);
|
|
77
|
+
setBounded(identityCache, accountCacheKey(accountUuid, accountIdentity), identity);
|
|
78
|
+
}
|
|
29
79
|
async function fetchClaudeCodeAccountUuid(accessToken, model) {
|
|
30
80
|
if (!accessToken.startsWith('sk-ant-oat'))
|
|
31
81
|
return null;
|
|
@@ -60,47 +110,100 @@ async function fetchClaudeCodeAccountUuid(accessToken, model) {
|
|
|
60
110
|
clearTimeout(timeout);
|
|
61
111
|
}
|
|
62
112
|
}
|
|
63
|
-
export async function resolveClaudeCodeIdentity(accessToken, model) {
|
|
64
|
-
const
|
|
113
|
+
export async function resolveClaudeCodeIdentity(accessToken, model, accountIdentity) {
|
|
114
|
+
const stableAccountIdentity = accountIdentity?.trim() || undefined;
|
|
115
|
+
const cacheKey = stableAccountIdentity
|
|
116
|
+
? explicitCacheKey(stableAccountIdentity)
|
|
117
|
+
: compatibilityCacheKey(accessToken);
|
|
118
|
+
let identity;
|
|
119
|
+
if (stableAccountIdentity) {
|
|
120
|
+
const cachedIdentity = adoptCachedIdentity(identityCache.get(cacheKey) ?? identityCache.get(accessToken), stableAccountIdentity);
|
|
121
|
+
identity = cachedIdentity ?? getClaudeCodeIdentity(cacheKey);
|
|
122
|
+
identity = adoptCachedIdentity(identity, stableAccountIdentity) ?? identity;
|
|
123
|
+
setBounded(identityCache, cacheKey, identity);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
identity =
|
|
127
|
+
identityCache.get(cacheKey) ??
|
|
128
|
+
identityCache.get(accessToken) ??
|
|
129
|
+
getClaudeCodeIdentity(cacheKey);
|
|
130
|
+
setBounded(identityCache, cacheKey, identity);
|
|
131
|
+
setBounded(identityCache, accessToken, identity);
|
|
132
|
+
}
|
|
65
133
|
if (!accessToken.startsWith('sk-ant-oat'))
|
|
66
134
|
return identity;
|
|
67
135
|
const now = Date.now();
|
|
68
|
-
|
|
136
|
+
// A slot-stable identity survives account replacement; bootstrap is the
|
|
137
|
+
// account lookup that must be repeated for each credential presented to it.
|
|
138
|
+
const bootstrapKey = `${cacheKey}:${accessToken}`;
|
|
139
|
+
const cachedResult = bootstrapResults.get(bootstrapKey);
|
|
69
140
|
if (cachedResult && cachedResult.expiresAt > now) {
|
|
70
|
-
if (!cachedResult.accountUuid)
|
|
141
|
+
if (!cachedResult.accountUuid) {
|
|
142
|
+
identity = clearCachedAccountUuid(cacheKey, identity);
|
|
143
|
+
return identity;
|
|
144
|
+
}
|
|
145
|
+
if (!stableAccountIdentity) {
|
|
146
|
+
if (identity.accountUuid === cachedResult.accountUuid)
|
|
147
|
+
return identity;
|
|
148
|
+
identity = { ...identity, accountUuid: cachedResult.accountUuid };
|
|
149
|
+
setBounded(identityCache, bootstrapKey, identity);
|
|
71
150
|
return identity;
|
|
72
|
-
const accountCacheKey = `account:${cachedResult.accountUuid}`;
|
|
73
|
-
const accountIdentity = identityCache.get(accountCacheKey);
|
|
74
|
-
if (accountIdentity) {
|
|
75
|
-
setBounded(identityCache, accessToken, accountIdentity);
|
|
76
|
-
return accountIdentity;
|
|
77
151
|
}
|
|
152
|
+
const cachedAccountIdentity = adoptCachedIdentity(identityCache.get(accountCacheKey(cachedResult.accountUuid, stableAccountIdentity)) ?? identityCache.get(`account:${cachedResult.accountUuid}`), stableAccountIdentity, cachedResult.accountUuid);
|
|
153
|
+
if (cachedAccountIdentity) {
|
|
154
|
+
identity = cachedAccountIdentity;
|
|
155
|
+
cacheAccountIdentity(identity, stableAccountIdentity, cachedResult.accountUuid);
|
|
156
|
+
return identity;
|
|
157
|
+
}
|
|
158
|
+
if (identity.accountUuid === cachedResult.accountUuid)
|
|
159
|
+
return identity;
|
|
160
|
+
identity = {
|
|
161
|
+
...identity,
|
|
162
|
+
accountIdentity: stableAccountIdentity,
|
|
163
|
+
accountUuid: cachedResult.accountUuid,
|
|
164
|
+
};
|
|
165
|
+
cacheAccountIdentity(identity, stableAccountIdentity, cachedResult.accountUuid);
|
|
166
|
+
return identity;
|
|
78
167
|
}
|
|
79
|
-
let fetchPromise = bootstrapFetches.get(
|
|
168
|
+
let fetchPromise = bootstrapFetches.get(bootstrapKey);
|
|
80
169
|
if (!fetchPromise) {
|
|
81
170
|
fetchPromise = fetchClaudeCodeAccountUuid(accessToken, model);
|
|
82
|
-
bootstrapFetches
|
|
171
|
+
setBounded(bootstrapFetches, bootstrapKey, fetchPromise);
|
|
83
172
|
}
|
|
84
173
|
const accountUuid = await fetchPromise.finally(() => {
|
|
85
|
-
bootstrapFetches.
|
|
174
|
+
if (bootstrapFetches.get(bootstrapKey) === fetchPromise) {
|
|
175
|
+
bootstrapFetches.delete(bootstrapKey);
|
|
176
|
+
}
|
|
86
177
|
});
|
|
87
|
-
setBounded(bootstrapResults,
|
|
178
|
+
setBounded(bootstrapResults, bootstrapKey, {
|
|
88
179
|
accountUuid,
|
|
89
180
|
expiresAt: now +
|
|
90
181
|
(accountUuid
|
|
91
182
|
? BOOTSTRAP_IDENTITY_CACHE_TTL_MS
|
|
92
183
|
: BOOTSTRAP_IDENTITY_NEGATIVE_TTL_MS),
|
|
93
184
|
});
|
|
94
|
-
if (!accountUuid)
|
|
185
|
+
if (!accountUuid) {
|
|
186
|
+
identity = clearCachedAccountUuid(cacheKey, identity);
|
|
187
|
+
return identity;
|
|
188
|
+
}
|
|
189
|
+
if (!stableAccountIdentity) {
|
|
190
|
+
identity = { ...identity, accountUuid };
|
|
191
|
+
setBounded(identityCache, bootstrapKey, identity);
|
|
95
192
|
return identity;
|
|
96
|
-
const accountCacheKey = `account:${accountUuid}`;
|
|
97
|
-
const accountIdentity = identityCache.get(accountCacheKey);
|
|
98
|
-
if (accountIdentity) {
|
|
99
|
-
setBounded(identityCache, accessToken, accountIdentity);
|
|
100
|
-
return accountIdentity;
|
|
101
193
|
}
|
|
102
|
-
|
|
103
|
-
|
|
194
|
+
const cachedAccountIdentity = adoptCachedIdentity(identityCache.get(accountCacheKey(accountUuid, stableAccountIdentity)) ??
|
|
195
|
+
identityCache.get(`account:${accountUuid}`), stableAccountIdentity, accountUuid);
|
|
196
|
+
if (cachedAccountIdentity) {
|
|
197
|
+
identity = cachedAccountIdentity;
|
|
198
|
+
cacheAccountIdentity(identity, stableAccountIdentity, accountUuid);
|
|
199
|
+
return identity;
|
|
200
|
+
}
|
|
201
|
+
identity = {
|
|
202
|
+
...identity,
|
|
203
|
+
accountIdentity: stableAccountIdentity,
|
|
204
|
+
accountUuid,
|
|
205
|
+
};
|
|
206
|
+
cacheAccountIdentity(identity, stableAccountIdentity, accountUuid);
|
|
104
207
|
return identity;
|
|
105
208
|
}
|
|
106
209
|
function isRecord(value) {
|
package/dist/dump.d.ts
CHANGED
package/dist/dump.js
CHANGED
|
@@ -592,7 +592,10 @@ export async function dumpResponseArtifact(handle, input) {
|
|
|
592
592
|
!Array.isArray(input.message)
|
|
593
593
|
? input.message
|
|
594
594
|
: {};
|
|
595
|
-
const artifact = {
|
|
595
|
+
const artifact = {
|
|
596
|
+
status: input.status,
|
|
597
|
+
stream_complete: input.complete ?? true,
|
|
598
|
+
};
|
|
596
599
|
if (typeof message.id === 'string' && message.id.length > 0)
|
|
597
600
|
artifact.message_id = message.id;
|
|
598
601
|
if (typeof message.model === 'string' && message.model.length > 0)
|
|
@@ -601,6 +604,8 @@ export async function dumpResponseArtifact(handle, input) {
|
|
|
601
604
|
artifact.usage = message.usage;
|
|
602
605
|
if (Object.hasOwn(message, 'diagnostics'))
|
|
603
606
|
artifact.diagnostics = message.diagnostics;
|
|
607
|
+
if (typeof message.stop_reason === 'string' && message.stop_reason.length > 0)
|
|
608
|
+
artifact.stop_reason = message.stop_reason;
|
|
604
609
|
try {
|
|
605
610
|
await writeDumpFile(handle.responsePath, `${JSON.stringify(artifact, null, 2)}\n`);
|
|
606
611
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from './oauth-profile.ts';
|
|
|
17
17
|
export * from './pkce.ts';
|
|
18
18
|
export * from './prime.ts';
|
|
19
19
|
export * from './provider.ts';
|
|
20
|
+
export * from './quota-header-feed.ts';
|
|
20
21
|
export * from './quota-headers.ts';
|
|
21
22
|
export * from './quota-manager.ts';
|
|
22
23
|
export * from './quotas.ts';
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from './oauth-profile.js';
|
|
|
17
17
|
export * from './pkce.js';
|
|
18
18
|
export * from './prime.js';
|
|
19
19
|
export * from './provider.js';
|
|
20
|
+
export * from './quota-header-feed.js';
|
|
20
21
|
export * from './quota-headers.js';
|
|
21
22
|
export * from './quota-manager.js';
|
|
22
23
|
export * from './quotas.js';
|
package/dist/oauth-profile.d.ts
CHANGED
|
@@ -2,10 +2,11 @@ import type { OAuthAccountProfile } from './accounts.ts';
|
|
|
2
2
|
export declare const PROFILE_TTL_MS: number;
|
|
3
3
|
export declare function fetchOAuthAccountProfile(input: {
|
|
4
4
|
accessToken: string;
|
|
5
|
+
accountIdentity?: string;
|
|
5
6
|
fetchImpl?: typeof fetch;
|
|
6
7
|
now?: () => number;
|
|
7
8
|
signal?: AbortSignal;
|
|
8
9
|
}): Promise<OAuthAccountProfile>;
|
|
9
|
-
export declare function
|
|
10
|
+
export declare function oauthProfileMatchesIdentity(profile: OAuthAccountProfile | undefined, accountIdentity: string | undefined): boolean;
|
|
10
11
|
export declare function oauthProfileIsFresh(profile: OAuthAccountProfile | undefined, now?: number): boolean;
|
|
11
12
|
export declare function formatOAuthAccountTier(profile: OAuthAccountProfile | undefined): string | undefined;
|
package/dist/oauth-profile.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { tokenFingerprint } from './quota-manager.js';
|
|
2
1
|
const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
|
|
3
2
|
export const PROFILE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
4
3
|
export async function fetchOAuthAccountProfile(input) {
|
|
@@ -27,11 +26,18 @@ export async function fetchOAuthAccountProfile(input) {
|
|
|
27
26
|
tier,
|
|
28
27
|
orgType,
|
|
29
28
|
checkedAt: input.now?.() ?? Date.now(),
|
|
30
|
-
|
|
29
|
+
...(input.accountIdentity !== undefined && {
|
|
30
|
+
accountIdentity: input.accountIdentity,
|
|
31
|
+
}),
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
|
-
export function
|
|
34
|
-
|
|
34
|
+
export function oauthProfileMatchesIdentity(profile, accountIdentity) {
|
|
35
|
+
if (!profile)
|
|
36
|
+
return false;
|
|
37
|
+
if (accountIdentity === undefined)
|
|
38
|
+
return true;
|
|
39
|
+
return (profile.accountIdentity === undefined ||
|
|
40
|
+
profile.accountIdentity === accountIdentity);
|
|
35
41
|
}
|
|
36
42
|
export function oauthProfileIsFresh(profile, now = Date.now()) {
|
|
37
43
|
return Boolean(profile &&
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type AccountStorage, type OAuthQuotaSnapshot } from './accounts.ts';
|
|
2
|
+
export declare const QUOTA_HEADER_FEED_SCHEMA_VERSION = 2;
|
|
3
|
+
export declare const QUOTA_HEADER_FEED_LEASE_MS = 180000;
|
|
4
|
+
export type QuotaHeaderFeedIdentity = {
|
|
5
|
+
identity_source: 'credential_id';
|
|
6
|
+
credential_id: string;
|
|
7
|
+
} | {
|
|
8
|
+
identity_source: 'account_ref';
|
|
9
|
+
account_ref: string;
|
|
10
|
+
} | {
|
|
11
|
+
identity_source: 'none';
|
|
12
|
+
};
|
|
13
|
+
export type QuotaHeaderFeedEntry = QuotaHeaderFeedIdentity & {
|
|
14
|
+
schema_version: typeof QUOTA_HEADER_FEED_SCHEMA_VERSION;
|
|
15
|
+
provider: 'anthropic';
|
|
16
|
+
configured_account_count: number;
|
|
17
|
+
observed_at_ms: number;
|
|
18
|
+
quota: Pick<OAuthQuotaSnapshot, 'five_hour' | 'seven_day' | 'bindingWindow' | 'fallbackAdvised' | 'scoped' | 'extraUsage'>;
|
|
19
|
+
};
|
|
20
|
+
export declare function getDefaultQuotaHeaderFeedDirectory(): string;
|
|
21
|
+
export declare function configuredAnthropicOAuthAccountCount(input: {
|
|
22
|
+
storage: AccountStorage | null;
|
|
23
|
+
mainOAuthConfigured: boolean;
|
|
24
|
+
}): number;
|
|
25
|
+
export declare class QuotaHeaderFeedRegistry {
|
|
26
|
+
private readonly options;
|
|
27
|
+
private readonly filePath;
|
|
28
|
+
private writeChain;
|
|
29
|
+
constructor(options?: {
|
|
30
|
+
directory?: string;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
leaseMs?: number;
|
|
33
|
+
instanceId?: string;
|
|
34
|
+
});
|
|
35
|
+
publish(entry: QuotaHeaderFeedEntry & {
|
|
36
|
+
accountKey: string;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
list(): Promise<QuotaHeaderFeedEntry[]>;
|
|
39
|
+
dispose(): Promise<void>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { isOAuthAccount, } from './accounts.js';
|
|
6
|
+
export const QUOTA_HEADER_FEED_SCHEMA_VERSION = 2;
|
|
7
|
+
export const QUOTA_HEADER_FEED_LEASE_MS = 180_000;
|
|
8
|
+
export function getDefaultQuotaHeaderFeedDirectory() {
|
|
9
|
+
return (process.env.OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR ??
|
|
10
|
+
join(tmpdir(), 'opencode-anthropic-auth', 'quota-header-feed'));
|
|
11
|
+
}
|
|
12
|
+
export function configuredAnthropicOAuthAccountCount(input) {
|
|
13
|
+
const fallbackCount = input.storage?.accounts.filter(isOAuthAccount).length ?? 0;
|
|
14
|
+
return fallbackCount + (input.mainOAuthConfigured ? 1 : 0);
|
|
15
|
+
}
|
|
16
|
+
function validIdentity(entry) {
|
|
17
|
+
if (entry.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
|
|
18
|
+
entry.provider !== 'anthropic')
|
|
19
|
+
return false;
|
|
20
|
+
if (!Number.isFinite(entry.configured_account_count) ||
|
|
21
|
+
!Number.isFinite(entry.observed_at_ms))
|
|
22
|
+
return false;
|
|
23
|
+
if (!entry.quota || typeof entry.quota !== 'object')
|
|
24
|
+
return false;
|
|
25
|
+
if (entry.identity_source === 'none') {
|
|
26
|
+
return !('credential_id' in entry) && !('account_ref' in entry);
|
|
27
|
+
}
|
|
28
|
+
if (entry.identity_source === 'credential_id') {
|
|
29
|
+
return (typeof entry.credential_id === 'string' &&
|
|
30
|
+
entry.credential_id.length > 0 &&
|
|
31
|
+
entry.credential_id !== 'main' &&
|
|
32
|
+
!('account_ref' in entry));
|
|
33
|
+
}
|
|
34
|
+
if (entry.identity_source === 'account_ref') {
|
|
35
|
+
return (typeof entry.account_ref === 'string' &&
|
|
36
|
+
entry.account_ref.length > 0 &&
|
|
37
|
+
entry.account_ref !== 'main' &&
|
|
38
|
+
!('credential_id' in entry));
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function validatePublishEntry(entry) {
|
|
43
|
+
if (entry.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
|
|
44
|
+
entry.provider !== 'anthropic' ||
|
|
45
|
+
!validIdentity(entry)) {
|
|
46
|
+
throw new Error('Invalid quota header feed entry');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function finiteNumber(value) {
|
|
50
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
51
|
+
}
|
|
52
|
+
function projectQuotaWindow(value) {
|
|
53
|
+
if (!value || typeof value !== 'object')
|
|
54
|
+
return undefined;
|
|
55
|
+
const candidate = value;
|
|
56
|
+
if (!finiteNumber(candidate.usedPercent) ||
|
|
57
|
+
!finiteNumber(candidate.remainingPercent) ||
|
|
58
|
+
!finiteNumber(candidate.checkedAt)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
usedPercent: candidate.usedPercent,
|
|
63
|
+
remainingPercent: candidate.remainingPercent,
|
|
64
|
+
...(typeof candidate.resetsAt === 'string' && {
|
|
65
|
+
resetsAt: candidate.resetsAt,
|
|
66
|
+
}),
|
|
67
|
+
checkedAt: candidate.checkedAt,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function projectScopedQuotaWindow(value) {
|
|
71
|
+
if (!value || typeof value !== 'object')
|
|
72
|
+
return undefined;
|
|
73
|
+
const candidate = value;
|
|
74
|
+
const window = projectQuotaWindow(candidate);
|
|
75
|
+
if (!window ||
|
|
76
|
+
typeof candidate.id !== 'string' ||
|
|
77
|
+
!candidate.id.trim() ||
|
|
78
|
+
typeof candidate.title !== 'string' ||
|
|
79
|
+
!candidate.title.trim() ||
|
|
80
|
+
typeof candidate.modelName !== 'string' ||
|
|
81
|
+
!candidate.modelName.trim()) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...window,
|
|
86
|
+
id: candidate.id,
|
|
87
|
+
title: candidate.title,
|
|
88
|
+
...(typeof candidate.modelId === 'string' &&
|
|
89
|
+
candidate.modelId.trim() && {
|
|
90
|
+
modelId: candidate.modelId,
|
|
91
|
+
}),
|
|
92
|
+
modelName: candidate.modelName,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function projectQuotaMoney(value) {
|
|
96
|
+
if (!value || typeof value !== 'object')
|
|
97
|
+
return undefined;
|
|
98
|
+
const candidate = value;
|
|
99
|
+
if (!finiteNumber(candidate.amountMinor) ||
|
|
100
|
+
typeof candidate.currency !== 'string' ||
|
|
101
|
+
!candidate.currency.trim() ||
|
|
102
|
+
!finiteNumber(candidate.exponent)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
amountMinor: candidate.amountMinor,
|
|
107
|
+
currency: candidate.currency,
|
|
108
|
+
exponent: candidate.exponent,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function projectExtraUsage(value) {
|
|
112
|
+
if (!value || typeof value !== 'object')
|
|
113
|
+
return undefined;
|
|
114
|
+
const candidate = value;
|
|
115
|
+
const used = projectQuotaMoney(candidate.used);
|
|
116
|
+
const limit = projectQuotaMoney(candidate.limit);
|
|
117
|
+
if (!used || !limit || typeof candidate.exhausted !== 'boolean') {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
used,
|
|
122
|
+
limit,
|
|
123
|
+
...(finiteNumber(candidate.utilizationPercent) && {
|
|
124
|
+
utilizationPercent: candidate.utilizationPercent,
|
|
125
|
+
}),
|
|
126
|
+
...(typeof candidate.severity === 'string' && {
|
|
127
|
+
severity: candidate.severity,
|
|
128
|
+
}),
|
|
129
|
+
exhausted: candidate.exhausted,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function projectQuota(quota) {
|
|
133
|
+
const fiveHour = projectQuotaWindow(quota.five_hour);
|
|
134
|
+
const sevenDay = projectQuotaWindow(quota.seven_day);
|
|
135
|
+
const scoped = Array.isArray(quota.scoped)
|
|
136
|
+
? quota.scoped
|
|
137
|
+
.map(projectScopedQuotaWindow)
|
|
138
|
+
.filter((entry) => entry != null)
|
|
139
|
+
: undefined;
|
|
140
|
+
const extraUsage = projectExtraUsage(quota.extraUsage);
|
|
141
|
+
return {
|
|
142
|
+
...(fiveHour && { five_hour: fiveHour }),
|
|
143
|
+
...(sevenDay && { seven_day: sevenDay }),
|
|
144
|
+
...(typeof quota.bindingWindow === 'string' && {
|
|
145
|
+
bindingWindow: quota.bindingWindow,
|
|
146
|
+
}),
|
|
147
|
+
...(typeof quota.fallbackAdvised === 'boolean' && {
|
|
148
|
+
fallbackAdvised: quota.fallbackAdvised,
|
|
149
|
+
}),
|
|
150
|
+
...(scoped && { scoped }),
|
|
151
|
+
...(extraUsage && { extraUsage }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export class QuotaHeaderFeedRegistry {
|
|
155
|
+
options;
|
|
156
|
+
filePath;
|
|
157
|
+
writeChain = Promise.resolve();
|
|
158
|
+
constructor(options = {}) {
|
|
159
|
+
this.options = options;
|
|
160
|
+
const instanceId = options.instanceId ?? `${process.pid}-${randomUUID()}`;
|
|
161
|
+
this.filePath = join(options.directory ?? getDefaultQuotaHeaderFeedDirectory(), `${instanceId}.json`);
|
|
162
|
+
}
|
|
163
|
+
publish(entry) {
|
|
164
|
+
const { accountKey, quota, ...entryWithoutQuota } = entry;
|
|
165
|
+
const cleanEntry = {
|
|
166
|
+
...entryWithoutQuota,
|
|
167
|
+
quota: projectQuota(quota),
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
validatePublishEntry(cleanEntry);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return Promise.reject(error);
|
|
174
|
+
}
|
|
175
|
+
if (!accountKey)
|
|
176
|
+
return Promise.reject(new Error('Invalid quota header feed account key'));
|
|
177
|
+
this.writeChain = this.writeChain
|
|
178
|
+
.catch(() => { })
|
|
179
|
+
.then(async () => {
|
|
180
|
+
const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
|
|
181
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
182
|
+
await chmod(directory, 0o700);
|
|
183
|
+
let entries = {};
|
|
184
|
+
try {
|
|
185
|
+
const record = JSON.parse(await readFile(this.filePath, 'utf8'));
|
|
186
|
+
if (record.version === QUOTA_HEADER_FEED_SCHEMA_VERSION &&
|
|
187
|
+
record.entries &&
|
|
188
|
+
typeof record.entries === 'object')
|
|
189
|
+
entries = record.entries;
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
entries[accountKey] = cleanEntry;
|
|
193
|
+
const tempPath = `${this.filePath}.${randomUUID()}.tmp`;
|
|
194
|
+
try {
|
|
195
|
+
await writeFile(tempPath, `${JSON.stringify({ version: QUOTA_HEADER_FEED_SCHEMA_VERSION, entries })}\n`, { mode: 0o600 });
|
|
196
|
+
await chmod(tempPath, 0o600);
|
|
197
|
+
await rename(tempPath, this.filePath);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await rm(tempPath, { force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
return this.writeChain;
|
|
204
|
+
}
|
|
205
|
+
async list() {
|
|
206
|
+
await this.writeChain.catch(() => { });
|
|
207
|
+
const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
|
|
208
|
+
const now = this.options.now?.() ?? Date.now();
|
|
209
|
+
const leaseMs = this.options.leaseMs ?? QUOTA_HEADER_FEED_LEASE_MS;
|
|
210
|
+
let names;
|
|
211
|
+
try {
|
|
212
|
+
names = (await readdir(directory)).filter((name) => name.endsWith('.json'));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const newest = new Map();
|
|
218
|
+
await Promise.all(names.map(async (name) => {
|
|
219
|
+
try {
|
|
220
|
+
const record = JSON.parse(await readFile(join(directory, name), 'utf8'));
|
|
221
|
+
if (record.version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
|
|
222
|
+
!record.entries ||
|
|
223
|
+
typeof record.entries !== 'object')
|
|
224
|
+
return;
|
|
225
|
+
for (const [accountKey, candidate] of Object.entries(record.entries)) {
|
|
226
|
+
if (!candidate ||
|
|
227
|
+
typeof candidate !== 'object' ||
|
|
228
|
+
candidate.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
|
|
229
|
+
candidate.provider !== 'anthropic' ||
|
|
230
|
+
!validIdentity(candidate))
|
|
231
|
+
continue;
|
|
232
|
+
const observed = candidate.observed_at_ms;
|
|
233
|
+
if (observed > now || now - observed >= leaseMs)
|
|
234
|
+
continue;
|
|
235
|
+
const existing = newest.get(accountKey);
|
|
236
|
+
if (!existing || observed > existing.observed_at_ms)
|
|
237
|
+
newest.set(accountKey, candidate);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}));
|
|
242
|
+
return [...newest.values()].sort((a, b) => a.observed_at_ms - b.observed_at_ms);
|
|
243
|
+
}
|
|
244
|
+
async dispose() {
|
|
245
|
+
await this.writeChain.catch(() => { });
|
|
246
|
+
await rm(this.filePath, { force: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
package/dist/quota-headers.js
CHANGED
|
@@ -49,11 +49,13 @@ export function normalizeQuotaHeaders(headers, now = Date.now()) {
|
|
|
49
49
|
return snapshot;
|
|
50
50
|
}
|
|
51
51
|
export function mergeHeaderQuotaSnapshot(existing, incoming) {
|
|
52
|
+
const { scoped: existingScoped, extraUsage: existingExtraUsage, ...existingWithoutPollOwnedFields } = existing ?? {};
|
|
53
|
+
const { scoped: _incomingScoped, extraUsage: _incomingExtraUsage, ...incomingWithoutPollOwnedFields } = incoming;
|
|
52
54
|
return {
|
|
53
|
-
...
|
|
54
|
-
...
|
|
55
|
-
|
|
56
|
-
extraUsage:
|
|
55
|
+
...existingWithoutPollOwnedFields,
|
|
56
|
+
...incomingWithoutPollOwnedFields,
|
|
57
|
+
...(Array.isArray(existingScoped) && { scoped: existingScoped }),
|
|
58
|
+
...(existingExtraUsage != null && { extraUsage: existingExtraUsage }),
|
|
57
59
|
bindingWindow: existing?.bindingWindowSource === 'poll'
|
|
58
60
|
? existing.bindingWindow
|
|
59
61
|
: (incoming.bindingWindow ?? existing?.bindingWindow),
|