github-issue-tower-defence-management 1.58.2 → 1.59.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/.gitattributes +2 -0
- package/.prettierignore +1 -0
- package/CHANGELOG.md +21 -0
- package/README.md +3 -3
- package/bin/adapter/entry-points/cli/index.js +1 -3
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/proxy/RateLimitCache.js +104 -12
- package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
- package/bin/adapter/proxy/proxyEntry.js +19 -0
- package/bin/adapter/proxy/proxyEntry.js.map +1 -1
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +33 -2
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
- package/bin/adapter/repositories/ProxyRateLimitCacheRepository.js +92 -0
- package/bin/adapter/repositories/ProxyRateLimitCacheRepository.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +34 -35
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/UpdateRateLimitCacheUseCase.js +18 -0
- package/bin/domain/usecases/UpdateRateLimitCacheUseCase.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/RateLimitCacheRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/RateLimitCacheRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +0 -3
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +9 -3
- package/src/adapter/proxy/RateLimitCache.test.ts +250 -0
- package/src/adapter/proxy/RateLimitCache.ts +114 -25
- package/src/adapter/proxy/proxyEntry.ts +24 -1
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +354 -7
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +42 -2
- package/src/adapter/repositories/ProxyRateLimitCacheRepository.test.ts +101 -0
- package/src/adapter/repositories/ProxyRateLimitCacheRepository.ts +66 -0
- package/src/domain/entities/ClaudeTokenUsage.ts +7 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +63 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +7 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1212 -887
- package/src/domain/usecases/StartPreparationUseCase.ts +47 -57
- package/src/domain/usecases/UpdateRateLimitCacheUseCase.test.ts +81 -0
- package/src/domain/usecases/UpdateRateLimitCacheUseCase.ts +16 -0
- package/src/domain/usecases/adapter-interfaces/RateLimitCacheRepository.ts +9 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/proxy/RateLimitCache.d.ts +11 -0
- package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
- package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyRateLimitCacheRepository.d.ts +10 -0
- package/types/adapter/repositories/ProxyRateLimitCacheRepository.d.ts.map +1 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts +6 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +3 -3
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/UpdateRateLimitCacheUseCase.d.ts +9 -0
- package/types/domain/usecases/UpdateRateLimitCacheUseCase.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/RateLimitCacheRepository.d.ts +9 -0
- package/types/domain/usecases/adapter-interfaces/RateLimitCacheRepository.d.ts.map +1 -0
|
@@ -3,12 +3,22 @@ import * as fs from 'fs';
|
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
|
|
6
|
+
export interface ModelWeeklyLimit {
|
|
7
|
+
rejected: boolean;
|
|
8
|
+
resetsAt: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export interface RateLimitSnapshot {
|
|
7
12
|
fiveHourUtilization: number;
|
|
8
13
|
fiveHourReset: number;
|
|
9
14
|
sevenDayUtilization: number;
|
|
10
15
|
sevenDayReset: number;
|
|
11
16
|
blocked: boolean;
|
|
17
|
+
rejected: boolean;
|
|
18
|
+
unifiedRejected: boolean;
|
|
19
|
+
fiveHourRejected: boolean;
|
|
20
|
+
sevenDayRejected: boolean;
|
|
21
|
+
modelWeeklyLimits: Record<string, ModelWeeklyLimit>;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
export const PROXY_PORT = 8787;
|
|
@@ -26,6 +36,37 @@ export const hashToken = (token: string): string =>
|
|
|
26
36
|
export const cachePathForToken = (token: string): string =>
|
|
27
37
|
path.join(cacheDir(), `${hashToken(token)}.json`);
|
|
28
38
|
|
|
39
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
40
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
41
|
+
|
|
42
|
+
const readPayload = (filePath: string): Record<string, unknown> => {
|
|
43
|
+
if (!fs.existsSync(filePath)) return {};
|
|
44
|
+
try {
|
|
45
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
46
|
+
const parsed: unknown = JSON.parse(raw);
|
|
47
|
+
return isRecord(parsed) ? parsed : {};
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const readModelWeeklyLimits = (
|
|
54
|
+
payload: Record<string, unknown>,
|
|
55
|
+
): Record<string, ModelWeeklyLimit> => {
|
|
56
|
+
const stored = payload.modelWeeklyLimits;
|
|
57
|
+
const result: Record<string, ModelWeeklyLimit> = {};
|
|
58
|
+
if (!isRecord(stored)) return result;
|
|
59
|
+
for (const [limitType, value] of Object.entries(stored)) {
|
|
60
|
+
if (!isRecord(value)) continue;
|
|
61
|
+
const rejected = value.rejected;
|
|
62
|
+
const resetsAt = value.resetsAt;
|
|
63
|
+
if (typeof rejected === 'boolean' && typeof resetsAt === 'number') {
|
|
64
|
+
result[limitType] = { rejected, resetsAt };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
69
|
+
|
|
29
70
|
export const writeRateLimit = (
|
|
30
71
|
token: string,
|
|
31
72
|
headers: Record<string, string | string[] | undefined>,
|
|
@@ -39,44 +80,84 @@ export const writeRateLimit = (
|
|
|
39
80
|
if (Array.isArray(value)) return value[0];
|
|
40
81
|
return value;
|
|
41
82
|
};
|
|
83
|
+
const filePath = path.join(dir, `${hashToken(token)}.json`);
|
|
84
|
+
const existing = readPayload(filePath);
|
|
85
|
+
const rateLimitHeaders: Record<string, string> = {};
|
|
86
|
+
for (const key of Object.keys(headers)) {
|
|
87
|
+
if (key.startsWith('anthropic-ratelimit-')) {
|
|
88
|
+
const value = pick(key);
|
|
89
|
+
if (value !== undefined) {
|
|
90
|
+
rateLimitHeaders[key] = value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
42
94
|
const payload = {
|
|
43
95
|
ts: Date.now() / 1000,
|
|
44
|
-
headers:
|
|
45
|
-
|
|
46
|
-
'anthropic-ratelimit-unified-status',
|
|
47
|
-
),
|
|
48
|
-
'anthropic-ratelimit-unified-5h-status': pick(
|
|
49
|
-
'anthropic-ratelimit-unified-5h-status',
|
|
50
|
-
),
|
|
51
|
-
'anthropic-ratelimit-unified-5h-reset': pick(
|
|
52
|
-
'anthropic-ratelimit-unified-5h-reset',
|
|
53
|
-
),
|
|
54
|
-
'anthropic-ratelimit-unified-5h-utilization': pick(
|
|
55
|
-
'anthropic-ratelimit-unified-5h-utilization',
|
|
56
|
-
),
|
|
57
|
-
'anthropic-ratelimit-unified-7d-status': pick(
|
|
58
|
-
'anthropic-ratelimit-unified-7d-status',
|
|
59
|
-
),
|
|
60
|
-
'anthropic-ratelimit-unified-7d-reset': pick(
|
|
61
|
-
'anthropic-ratelimit-unified-7d-reset',
|
|
62
|
-
),
|
|
63
|
-
'anthropic-ratelimit-unified-7d-utilization': pick(
|
|
64
|
-
'anthropic-ratelimit-unified-7d-utilization',
|
|
65
|
-
),
|
|
66
|
-
},
|
|
96
|
+
headers: rateLimitHeaders,
|
|
97
|
+
modelWeeklyLimits: readModelWeeklyLimits(existing),
|
|
67
98
|
};
|
|
99
|
+
fs.writeFileSync(filePath, JSON.stringify(payload));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const writeModelRateLimit = (
|
|
103
|
+
token: string,
|
|
104
|
+
limits: Record<string, ModelWeeklyLimit>,
|
|
105
|
+
): void => {
|
|
106
|
+
const limitTypes = Object.keys(limits);
|
|
107
|
+
if (limitTypes.length === 0) return;
|
|
108
|
+
const dir = cacheDir();
|
|
109
|
+
if (!fs.existsSync(dir)) {
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
}
|
|
68
112
|
const filePath = path.join(dir, `${hashToken(token)}.json`);
|
|
113
|
+
const existing = readPayload(filePath);
|
|
114
|
+
const merged = {
|
|
115
|
+
...readModelWeeklyLimits(existing),
|
|
116
|
+
...limits,
|
|
117
|
+
};
|
|
118
|
+
const payload = {
|
|
119
|
+
...existing,
|
|
120
|
+
modelWeeklyLimits: merged,
|
|
121
|
+
};
|
|
69
122
|
fs.writeFileSync(filePath, JSON.stringify(payload));
|
|
70
123
|
};
|
|
71
124
|
|
|
125
|
+
export const parseModelRateLimitsFromBody = (
|
|
126
|
+
body: string,
|
|
127
|
+
): Record<string, ModelWeeklyLimit> => {
|
|
128
|
+
const result: Record<string, ModelWeeklyLimit> = {};
|
|
129
|
+
const matches = body.match(
|
|
130
|
+
/\{[^{}]*"rateLimitType"[^{}]*\}|\{[^{}]*"resetsAt"[^{}]*"rateLimitType"[^{}]*\}/g,
|
|
131
|
+
);
|
|
132
|
+
if (matches === null) return result;
|
|
133
|
+
for (const candidate of matches) {
|
|
134
|
+
let parsed: unknown;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(candidate);
|
|
137
|
+
} catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!isRecord(parsed)) continue;
|
|
141
|
+
const rateLimitType = parsed.rateLimitType;
|
|
142
|
+
const status = parsed.status;
|
|
143
|
+
const resetsAt = parsed.resetsAt;
|
|
144
|
+
if (typeof rateLimitType !== 'string') continue;
|
|
145
|
+
if (typeof status !== 'string') continue;
|
|
146
|
+
if (typeof resetsAt !== 'number') continue;
|
|
147
|
+
result[rateLimitType] = {
|
|
148
|
+
rejected: status === 'rejected',
|
|
149
|
+
resetsAt,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
};
|
|
154
|
+
|
|
72
155
|
export const readRateLimit = (token: string): RateLimitSnapshot | null => {
|
|
73
156
|
const filePath = cachePathForToken(token);
|
|
74
157
|
if (!fs.existsSync(filePath)) return null;
|
|
75
158
|
try {
|
|
76
159
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
77
160
|
const parsed: unknown = JSON.parse(raw);
|
|
78
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
79
|
-
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
80
161
|
if (!isRecord(parsed)) return null;
|
|
81
162
|
const headersUnknown = parsed.headers;
|
|
82
163
|
const headers: Record<string, string> = {};
|
|
@@ -96,6 +177,9 @@ export const readRateLimit = (token: string): RateLimitSnapshot | null => {
|
|
|
96
177
|
const status = headers['anthropic-ratelimit-unified-status'];
|
|
97
178
|
const fiveHourStatus = headers['anthropic-ratelimit-unified-5h-status'];
|
|
98
179
|
const sevenDayStatus = headers['anthropic-ratelimit-unified-7d-status'];
|
|
180
|
+
const unifiedRejected = status === 'rejected';
|
|
181
|
+
const fiveHourRejected = fiveHourStatus === 'rejected';
|
|
182
|
+
const sevenDayRejected = sevenDayStatus === 'rejected';
|
|
99
183
|
return {
|
|
100
184
|
fiveHourUtilization: num('anthropic-ratelimit-unified-5h-utilization'),
|
|
101
185
|
fiveHourReset: num('anthropic-ratelimit-unified-5h-reset'),
|
|
@@ -105,6 +189,11 @@ export const readRateLimit = (token: string): RateLimitSnapshot | null => {
|
|
|
105
189
|
status === 'blocked' ||
|
|
106
190
|
fiveHourStatus === 'blocked' ||
|
|
107
191
|
sevenDayStatus === 'blocked',
|
|
192
|
+
rejected: unifiedRejected || fiveHourRejected || sevenDayRejected,
|
|
193
|
+
unifiedRejected,
|
|
194
|
+
fiveHourRejected,
|
|
195
|
+
sevenDayRejected,
|
|
196
|
+
modelWeeklyLimits: readModelWeeklyLimits(parsed),
|
|
108
197
|
};
|
|
109
198
|
} catch {
|
|
110
199
|
return null;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
2
|
import * as https from 'https';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
PROXY_PORT,
|
|
5
|
+
parseModelRateLimitsFromBody,
|
|
6
|
+
writeModelRateLimit,
|
|
7
|
+
writeRateLimit,
|
|
8
|
+
} from './RateLimitCache';
|
|
4
9
|
|
|
5
10
|
const UPSTREAM_HOST = 'api.anthropic.com';
|
|
6
11
|
|
|
12
|
+
const MAX_INSPECTED_BODY_BYTES = 1024 * 1024;
|
|
13
|
+
|
|
7
14
|
const BEARER_PREFIX = 'bearer ';
|
|
8
15
|
|
|
9
16
|
const extractToken = (
|
|
@@ -40,6 +47,22 @@ const startProxy = (port: number): void => {
|
|
|
40
47
|
} catch (error) {
|
|
41
48
|
console.error('Failed to write rate limit cache:', error);
|
|
42
49
|
}
|
|
50
|
+
const inspectedChunks: Uint8Array[] = [];
|
|
51
|
+
let inspectedBytes = 0;
|
|
52
|
+
upstreamResponse.on('data', (chunk: Buffer) => {
|
|
53
|
+
if (inspectedBytes >= MAX_INSPECTED_BODY_BYTES) return;
|
|
54
|
+
inspectedChunks.push(new Uint8Array(chunk));
|
|
55
|
+
inspectedBytes += chunk.length;
|
|
56
|
+
});
|
|
57
|
+
upstreamResponse.on('end', () => {
|
|
58
|
+
try {
|
|
59
|
+
const body = Buffer.concat(inspectedChunks).toString('utf8');
|
|
60
|
+
const limits = parseModelRateLimitsFromBody(body);
|
|
61
|
+
writeModelRateLimit(token, limits);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Failed to write model rate limit cache:', error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
43
66
|
}
|
|
44
67
|
clientResponse.writeHead(
|
|
45
68
|
upstreamResponse.statusCode ?? 502,
|
|
@@ -65,16 +65,24 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
65
65
|
expect(mockLoadTokens.mock.calls).toEqual([['/tokens.json']]);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
const futureReset = Math.floor(Date.now() / 1000) + 3600;
|
|
69
|
+
const pastReset = Math.floor(Date.now() / 1000) - 3600;
|
|
70
|
+
|
|
68
71
|
it('should map each token to its cached utilization', async () => {
|
|
69
72
|
mockLoadTokens.mockReturnValue(['token-a', 'token-b']);
|
|
70
73
|
mockReadRateLimit.mockImplementation((token: string) => {
|
|
71
74
|
if (token === 'token-a') {
|
|
72
75
|
return {
|
|
73
76
|
fiveHourUtilization: 42,
|
|
74
|
-
fiveHourReset:
|
|
77
|
+
fiveHourReset: futureReset,
|
|
75
78
|
sevenDayUtilization: 0,
|
|
76
|
-
sevenDayReset:
|
|
79
|
+
sevenDayReset: futureReset,
|
|
77
80
|
blocked: false,
|
|
81
|
+
rejected: false,
|
|
82
|
+
unifiedRejected: false,
|
|
83
|
+
fiveHourRejected: false,
|
|
84
|
+
sevenDayRejected: false,
|
|
85
|
+
modelWeeklyLimits: {},
|
|
78
86
|
};
|
|
79
87
|
}
|
|
80
88
|
return null;
|
|
@@ -84,8 +92,20 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
84
92
|
const result = await repository.getAvailableTokenUsages();
|
|
85
93
|
|
|
86
94
|
expect(result).toEqual([
|
|
87
|
-
{
|
|
88
|
-
|
|
95
|
+
{
|
|
96
|
+
token: 'token-a',
|
|
97
|
+
fiveHourUtilization: 42,
|
|
98
|
+
blocked: false,
|
|
99
|
+
rejected: false,
|
|
100
|
+
modelWeeklyLimits: {},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
token: 'token-b',
|
|
104
|
+
fiveHourUtilization: 0,
|
|
105
|
+
blocked: false,
|
|
106
|
+
rejected: false,
|
|
107
|
+
modelWeeklyLimits: {},
|
|
108
|
+
},
|
|
89
109
|
]);
|
|
90
110
|
});
|
|
91
111
|
|
|
@@ -93,17 +113,344 @@ describe('ProxyClaudeTokenUsageRepository', () => {
|
|
|
93
113
|
mockLoadTokens.mockReturnValue(['token-a']);
|
|
94
114
|
mockReadRateLimit.mockReturnValue({
|
|
95
115
|
fiveHourUtilization: 5,
|
|
96
|
-
fiveHourReset:
|
|
116
|
+
fiveHourReset: futureReset,
|
|
97
117
|
sevenDayUtilization: 0,
|
|
98
|
-
sevenDayReset:
|
|
118
|
+
sevenDayReset: futureReset,
|
|
99
119
|
blocked: true,
|
|
120
|
+
rejected: false,
|
|
121
|
+
unifiedRejected: false,
|
|
122
|
+
fiveHourRejected: false,
|
|
123
|
+
sevenDayRejected: false,
|
|
124
|
+
modelWeeklyLimits: {},
|
|
125
|
+
});
|
|
126
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
127
|
+
|
|
128
|
+
const result = await repository.getAvailableTokenUsages();
|
|
129
|
+
|
|
130
|
+
expect(result).toEqual([
|
|
131
|
+
{
|
|
132
|
+
token: 'token-a',
|
|
133
|
+
fiveHourUtilization: 5,
|
|
134
|
+
blocked: true,
|
|
135
|
+
rejected: false,
|
|
136
|
+
modelWeeklyLimits: {},
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should propagate the rejected status from the cache', async () => {
|
|
142
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
143
|
+
mockReadRateLimit.mockReturnValue({
|
|
144
|
+
fiveHourUtilization: 100,
|
|
145
|
+
fiveHourReset: futureReset,
|
|
146
|
+
sevenDayUtilization: 0,
|
|
147
|
+
sevenDayReset: futureReset,
|
|
148
|
+
blocked: false,
|
|
149
|
+
rejected: true,
|
|
150
|
+
unifiedRejected: false,
|
|
151
|
+
fiveHourRejected: true,
|
|
152
|
+
sevenDayRejected: false,
|
|
153
|
+
modelWeeklyLimits: {},
|
|
154
|
+
});
|
|
155
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
156
|
+
|
|
157
|
+
const result = await repository.getAvailableTokenUsages();
|
|
158
|
+
|
|
159
|
+
expect(result).toEqual([
|
|
160
|
+
{
|
|
161
|
+
token: 'token-a',
|
|
162
|
+
fiveHourUtilization: 100,
|
|
163
|
+
blocked: false,
|
|
164
|
+
rejected: true,
|
|
165
|
+
modelWeeklyLimits: {},
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should normalize fiveHourUtilization to 0 when the 5h reset has passed', async () => {
|
|
171
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
172
|
+
mockReadRateLimit.mockReturnValue({
|
|
173
|
+
fiveHourUtilization: 100,
|
|
174
|
+
fiveHourReset: pastReset,
|
|
175
|
+
sevenDayUtilization: 30,
|
|
176
|
+
sevenDayReset: futureReset,
|
|
177
|
+
blocked: false,
|
|
178
|
+
rejected: false,
|
|
179
|
+
unifiedRejected: false,
|
|
180
|
+
fiveHourRejected: false,
|
|
181
|
+
sevenDayRejected: false,
|
|
182
|
+
modelWeeklyLimits: {},
|
|
183
|
+
});
|
|
184
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
185
|
+
|
|
186
|
+
const result = await repository.getAvailableTokenUsages();
|
|
187
|
+
|
|
188
|
+
expect(result).toEqual([
|
|
189
|
+
{
|
|
190
|
+
token: 'token-a',
|
|
191
|
+
fiveHourUtilization: 0,
|
|
192
|
+
blocked: false,
|
|
193
|
+
rejected: false,
|
|
194
|
+
modelWeeklyLimits: {},
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should keep fiveHourUtilization when the 5h reset is in the future', async () => {
|
|
200
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
201
|
+
mockReadRateLimit.mockReturnValue({
|
|
202
|
+
fiveHourUtilization: 95,
|
|
203
|
+
fiveHourReset: futureReset,
|
|
204
|
+
sevenDayUtilization: 0,
|
|
205
|
+
sevenDayReset: futureReset,
|
|
206
|
+
blocked: false,
|
|
207
|
+
rejected: false,
|
|
208
|
+
unifiedRejected: false,
|
|
209
|
+
fiveHourRejected: false,
|
|
210
|
+
sevenDayRejected: false,
|
|
211
|
+
modelWeeklyLimits: {},
|
|
212
|
+
});
|
|
213
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
214
|
+
|
|
215
|
+
const result = await repository.getAvailableTokenUsages();
|
|
216
|
+
|
|
217
|
+
expect(result).toEqual([
|
|
218
|
+
{
|
|
219
|
+
token: 'token-a',
|
|
220
|
+
fiveHourUtilization: 95,
|
|
221
|
+
blocked: false,
|
|
222
|
+
rejected: false,
|
|
223
|
+
modelWeeklyLimits: {},
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should clear a 5h-origin rejection once the 5h reset has passed', async () => {
|
|
229
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
230
|
+
mockReadRateLimit.mockReturnValue({
|
|
231
|
+
fiveHourUtilization: 100,
|
|
232
|
+
fiveHourReset: pastReset,
|
|
233
|
+
sevenDayUtilization: 0,
|
|
234
|
+
sevenDayReset: futureReset,
|
|
235
|
+
blocked: false,
|
|
236
|
+
rejected: true,
|
|
237
|
+
unifiedRejected: false,
|
|
238
|
+
fiveHourRejected: true,
|
|
239
|
+
sevenDayRejected: false,
|
|
240
|
+
modelWeeklyLimits: {},
|
|
241
|
+
});
|
|
242
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
243
|
+
|
|
244
|
+
const result = await repository.getAvailableTokenUsages();
|
|
245
|
+
|
|
246
|
+
expect(result).toEqual([
|
|
247
|
+
{
|
|
248
|
+
token: 'token-a',
|
|
249
|
+
fiveHourUtilization: 0,
|
|
250
|
+
blocked: false,
|
|
251
|
+
rejected: false,
|
|
252
|
+
modelWeeklyLimits: {},
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should clear a 7d-origin rejection once the 7d reset has passed', async () => {
|
|
258
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
259
|
+
mockReadRateLimit.mockReturnValue({
|
|
260
|
+
fiveHourUtilization: 10,
|
|
261
|
+
fiveHourReset: futureReset,
|
|
262
|
+
sevenDayUtilization: 100,
|
|
263
|
+
sevenDayReset: pastReset,
|
|
264
|
+
blocked: false,
|
|
265
|
+
rejected: true,
|
|
266
|
+
unifiedRejected: false,
|
|
267
|
+
fiveHourRejected: false,
|
|
268
|
+
sevenDayRejected: true,
|
|
269
|
+
modelWeeklyLimits: {},
|
|
270
|
+
});
|
|
271
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
272
|
+
|
|
273
|
+
const result = await repository.getAvailableTokenUsages();
|
|
274
|
+
|
|
275
|
+
expect(result).toEqual([
|
|
276
|
+
{
|
|
277
|
+
token: 'token-a',
|
|
278
|
+
fiveHourUtilization: 10,
|
|
279
|
+
blocked: false,
|
|
280
|
+
rejected: false,
|
|
281
|
+
modelWeeklyLimits: {},
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should keep a 5h-origin rejection while the 5h reset is in the future', async () => {
|
|
287
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
288
|
+
mockReadRateLimit.mockReturnValue({
|
|
289
|
+
fiveHourUtilization: 100,
|
|
290
|
+
fiveHourReset: futureReset,
|
|
291
|
+
sevenDayUtilization: 0,
|
|
292
|
+
sevenDayReset: futureReset,
|
|
293
|
+
blocked: false,
|
|
294
|
+
rejected: true,
|
|
295
|
+
unifiedRejected: false,
|
|
296
|
+
fiveHourRejected: true,
|
|
297
|
+
sevenDayRejected: false,
|
|
298
|
+
modelWeeklyLimits: {},
|
|
299
|
+
});
|
|
300
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
301
|
+
|
|
302
|
+
const result = await repository.getAvailableTokenUsages();
|
|
303
|
+
|
|
304
|
+
expect(result).toEqual([
|
|
305
|
+
{
|
|
306
|
+
token: 'token-a',
|
|
307
|
+
fiveHourUtilization: 100,
|
|
308
|
+
blocked: false,
|
|
309
|
+
rejected: true,
|
|
310
|
+
modelWeeklyLimits: {},
|
|
311
|
+
},
|
|
312
|
+
]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should keep a still-active 7d rejection after the 5h reset has passed', async () => {
|
|
316
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
317
|
+
mockReadRateLimit.mockReturnValue({
|
|
318
|
+
fiveHourUtilization: 100,
|
|
319
|
+
fiveHourReset: pastReset,
|
|
320
|
+
sevenDayUtilization: 100,
|
|
321
|
+
sevenDayReset: futureReset,
|
|
322
|
+
blocked: false,
|
|
323
|
+
rejected: true,
|
|
324
|
+
unifiedRejected: false,
|
|
325
|
+
fiveHourRejected: false,
|
|
326
|
+
sevenDayRejected: true,
|
|
327
|
+
modelWeeklyLimits: {},
|
|
328
|
+
});
|
|
329
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
330
|
+
|
|
331
|
+
const result = await repository.getAvailableTokenUsages();
|
|
332
|
+
|
|
333
|
+
expect(result).toEqual([
|
|
334
|
+
{
|
|
335
|
+
token: 'token-a',
|
|
336
|
+
fiveHourUtilization: 0,
|
|
337
|
+
blocked: false,
|
|
338
|
+
rejected: true,
|
|
339
|
+
modelWeeklyLimits: {},
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should clear a unified rejection once the 5h reset has passed', async () => {
|
|
345
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
346
|
+
mockReadRateLimit.mockReturnValue({
|
|
347
|
+
fiveHourUtilization: 100,
|
|
348
|
+
fiveHourReset: pastReset,
|
|
349
|
+
sevenDayUtilization: 0,
|
|
350
|
+
sevenDayReset: futureReset,
|
|
351
|
+
blocked: false,
|
|
352
|
+
rejected: true,
|
|
353
|
+
unifiedRejected: true,
|
|
354
|
+
fiveHourRejected: false,
|
|
355
|
+
sevenDayRejected: false,
|
|
356
|
+
modelWeeklyLimits: {},
|
|
357
|
+
});
|
|
358
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
359
|
+
|
|
360
|
+
const result = await repository.getAvailableTokenUsages();
|
|
361
|
+
|
|
362
|
+
expect(result).toEqual([
|
|
363
|
+
{
|
|
364
|
+
token: 'token-a',
|
|
365
|
+
fiveHourUtilization: 0,
|
|
366
|
+
blocked: false,
|
|
367
|
+
rejected: false,
|
|
368
|
+
modelWeeklyLimits: {},
|
|
369
|
+
},
|
|
370
|
+
]);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should default rejected to false when no snapshot exists', async () => {
|
|
374
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
375
|
+
mockReadRateLimit.mockReturnValue(null);
|
|
376
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
377
|
+
|
|
378
|
+
const result = await repository.getAvailableTokenUsages();
|
|
379
|
+
|
|
380
|
+
expect(result).toEqual([
|
|
381
|
+
{
|
|
382
|
+
token: 'token-a',
|
|
383
|
+
fiveHourUtilization: 0,
|
|
384
|
+
blocked: false,
|
|
385
|
+
rejected: false,
|
|
386
|
+
modelWeeklyLimits: {},
|
|
387
|
+
},
|
|
388
|
+
]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should keep a model weekly rejection while its reset is in the future', async () => {
|
|
392
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
393
|
+
mockReadRateLimit.mockReturnValue({
|
|
394
|
+
fiveHourUtilization: 5,
|
|
395
|
+
fiveHourReset: futureReset,
|
|
396
|
+
sevenDayUtilization: 10,
|
|
397
|
+
sevenDayReset: futureReset,
|
|
398
|
+
blocked: false,
|
|
399
|
+
rejected: false,
|
|
400
|
+
unifiedRejected: false,
|
|
401
|
+
fiveHourRejected: false,
|
|
402
|
+
sevenDayRejected: false,
|
|
403
|
+
modelWeeklyLimits: {
|
|
404
|
+
seven_day_sonnet: { rejected: true, resetsAt: futureReset },
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
408
|
+
|
|
409
|
+
const result = await repository.getAvailableTokenUsages();
|
|
410
|
+
|
|
411
|
+
expect(result).toEqual([
|
|
412
|
+
{
|
|
413
|
+
token: 'token-a',
|
|
414
|
+
fiveHourUtilization: 5,
|
|
415
|
+
blocked: false,
|
|
416
|
+
rejected: false,
|
|
417
|
+
modelWeeklyLimits: {
|
|
418
|
+
seven_day_sonnet: { rejected: true, resetsAt: futureReset },
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('should clear a model weekly rejection once its reset has passed', async () => {
|
|
425
|
+
mockLoadTokens.mockReturnValue(['token-a']);
|
|
426
|
+
mockReadRateLimit.mockReturnValue({
|
|
427
|
+
fiveHourUtilization: 5,
|
|
428
|
+
fiveHourReset: futureReset,
|
|
429
|
+
sevenDayUtilization: 10,
|
|
430
|
+
sevenDayReset: futureReset,
|
|
431
|
+
blocked: false,
|
|
432
|
+
rejected: false,
|
|
433
|
+
unifiedRejected: false,
|
|
434
|
+
fiveHourRejected: false,
|
|
435
|
+
sevenDayRejected: false,
|
|
436
|
+
modelWeeklyLimits: {
|
|
437
|
+
seven_day_sonnet: { rejected: true, resetsAt: pastReset },
|
|
438
|
+
},
|
|
100
439
|
});
|
|
101
440
|
const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
|
|
102
441
|
|
|
103
442
|
const result = await repository.getAvailableTokenUsages();
|
|
104
443
|
|
|
105
444
|
expect(result).toEqual([
|
|
106
|
-
{
|
|
445
|
+
{
|
|
446
|
+
token: 'token-a',
|
|
447
|
+
fiveHourUtilization: 5,
|
|
448
|
+
blocked: false,
|
|
449
|
+
rejected: false,
|
|
450
|
+
modelWeeklyLimits: {
|
|
451
|
+
seven_day_sonnet: { rejected: false, resetsAt: pastReset },
|
|
452
|
+
},
|
|
453
|
+
},
|
|
107
454
|
]);
|
|
108
455
|
});
|
|
109
456
|
});
|
|
@@ -22,12 +22,52 @@ export class ProxyClaudeTokenUsageRepository implements ClaudeTokenUsageReposito
|
|
|
22
22
|
if (tokens === null) {
|
|
23
23
|
return [];
|
|
24
24
|
}
|
|
25
|
+
const nowEpochSeconds = Date.now() / 1000;
|
|
25
26
|
return tokens.map((token) => {
|
|
26
27
|
const snapshot = readRateLimit(token);
|
|
28
|
+
if (snapshot === null) {
|
|
29
|
+
return {
|
|
30
|
+
token,
|
|
31
|
+
fiveHourUtilization: 0,
|
|
32
|
+
blocked: false,
|
|
33
|
+
rejected: false,
|
|
34
|
+
modelWeeklyLimits: {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const fiveHourExpired = nowEpochSeconds > snapshot.fiveHourReset;
|
|
38
|
+
const sevenDayExpired = nowEpochSeconds > snapshot.sevenDayReset;
|
|
39
|
+
const fiveHourUtilization = fiveHourExpired
|
|
40
|
+
? 0
|
|
41
|
+
: snapshot.fiveHourUtilization;
|
|
42
|
+
const fiveHourRejectionActive =
|
|
43
|
+
snapshot.fiveHourRejected && !fiveHourExpired;
|
|
44
|
+
const sevenDayRejectionActive =
|
|
45
|
+
snapshot.sevenDayRejected && !sevenDayExpired;
|
|
46
|
+
const unifiedRejectionActive =
|
|
47
|
+
snapshot.unifiedRejected && !fiveHourExpired;
|
|
48
|
+
const rejected =
|
|
49
|
+
unifiedRejectionActive ||
|
|
50
|
+
fiveHourRejectionActive ||
|
|
51
|
+
sevenDayRejectionActive;
|
|
52
|
+
const modelWeeklyLimits: Record<
|
|
53
|
+
string,
|
|
54
|
+
{ rejected: boolean; resetsAt: number }
|
|
55
|
+
> = {};
|
|
56
|
+
for (const [limitType, limit] of Object.entries(
|
|
57
|
+
snapshot.modelWeeklyLimits,
|
|
58
|
+
)) {
|
|
59
|
+
const expired = nowEpochSeconds > limit.resetsAt;
|
|
60
|
+
modelWeeklyLimits[limitType] = {
|
|
61
|
+
rejected: limit.rejected && !expired,
|
|
62
|
+
resetsAt: limit.resetsAt,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
27
65
|
return {
|
|
28
66
|
token,
|
|
29
|
-
fiveHourUtilization
|
|
30
|
-
blocked: snapshot
|
|
67
|
+
fiveHourUtilization,
|
|
68
|
+
blocked: snapshot.blocked,
|
|
69
|
+
rejected,
|
|
70
|
+
modelWeeklyLimits,
|
|
31
71
|
};
|
|
32
72
|
});
|
|
33
73
|
};
|