github-issue-tower-defence-management 1.58.2 → 1.58.3
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 +9 -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 +1 -3
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/proxy/RateLimitCache.js +94 -3
- 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/domain/usecases/StartPreparationUseCase.js +34 -35
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +0 -3
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -3
- package/src/adapter/proxy/RateLimitCache.test.ts +190 -0
- package/src/adapter/proxy/RateLimitCache.ts +104 -2
- 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/domain/entities/ClaudeTokenUsage.ts +7 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1212 -887
- package/src/domain/usecases/StartPreparationUseCase.ts +47 -57
- 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/domain/entities/ClaudeTokenUsage.d.ts +6 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +3 -3
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
|
@@ -34,7 +34,6 @@ import { AssignNoAssigneeIssueToManagerUseCase } from '../../../domain/usecases/
|
|
|
34
34
|
import { UpdateIssueStatusByLabelUseCase } from '../../../domain/usecases/UpdateIssueStatusByLabelUseCase';
|
|
35
35
|
import { StartPreparationUseCase } from '../../../domain/usecases/StartPreparationUseCase';
|
|
36
36
|
import { NodeLocalCommandRunner } from '../../repositories/NodeLocalCommandRunner';
|
|
37
|
-
import { OauthAPIProxyClaudeRepository } from '../../repositories/OauthAPIProxyClaudeRepository';
|
|
38
37
|
import { ProxyClaudeTokenUsageRepository } from '../../repositories/ProxyClaudeTokenUsageRepository';
|
|
39
38
|
import { RevertOrphanedPreparationUseCase } from '../../../domain/usecases/RevertOrphanedPreparationUseCase';
|
|
40
39
|
import { RevertNotReadyAwaitingQualityCheckUseCase } from '../../../domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase';
|
|
@@ -221,14 +220,12 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
221
220
|
issueRepository,
|
|
222
221
|
);
|
|
223
222
|
const nodeLocalCommandRunner = new NodeLocalCommandRunner();
|
|
224
|
-
const claudeRepository = new OauthAPIProxyClaudeRepository();
|
|
225
223
|
const claudeTokenUsageRepository = new ProxyClaudeTokenUsageRepository(
|
|
226
224
|
mergedInput.claudeCodeOauthTokenListJsonPath ?? null,
|
|
227
225
|
);
|
|
228
226
|
const startPreparationUseCase = new StartPreparationUseCase(
|
|
229
227
|
projectRepository,
|
|
230
228
|
issueRepository,
|
|
231
|
-
claudeRepository,
|
|
232
229
|
nodeLocalCommandRunner,
|
|
233
230
|
claudeTokenUsageRepository,
|
|
234
231
|
);
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
cacheDir,
|
|
6
6
|
cachePathForToken,
|
|
7
7
|
hashToken,
|
|
8
|
+
parseModelRateLimitsFromBody,
|
|
8
9
|
readRateLimit,
|
|
10
|
+
writeModelRateLimit,
|
|
9
11
|
writeRateLimit,
|
|
10
12
|
} from './RateLimitCache';
|
|
11
13
|
|
|
@@ -84,6 +86,7 @@ describe('RateLimitCache', () => {
|
|
|
84
86
|
expect(snapshot.sevenDayUtilization).toBe(17);
|
|
85
87
|
expect(snapshot.sevenDayReset).toBe(1700100000);
|
|
86
88
|
expect(snapshot.blocked).toBe(false);
|
|
89
|
+
expect(snapshot.rejected).toBe(false);
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
it('should mark snapshot as blocked when status header is blocked', () => {
|
|
@@ -101,6 +104,79 @@ describe('RateLimitCache', () => {
|
|
|
101
104
|
expect(snapshot?.blocked).toBe(true);
|
|
102
105
|
});
|
|
103
106
|
|
|
107
|
+
it('should mark snapshot as rejected when unified status header is rejected', () => {
|
|
108
|
+
const token = 'rejected-unified-token';
|
|
109
|
+
writeRateLimit(token, {
|
|
110
|
+
'anthropic-ratelimit-unified-status': 'rejected',
|
|
111
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
112
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
113
|
+
'anthropic-ratelimit-unified-5h-utilization': '100',
|
|
114
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
115
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
116
|
+
'anthropic-ratelimit-unified-7d-utilization': '99',
|
|
117
|
+
});
|
|
118
|
+
const snapshot = readRateLimit(token);
|
|
119
|
+
expect(snapshot?.rejected).toBe(true);
|
|
120
|
+
expect(snapshot?.blocked).toBe(false);
|
|
121
|
+
expect(snapshot?.unifiedRejected).toBe(true);
|
|
122
|
+
expect(snapshot?.fiveHourRejected).toBe(false);
|
|
123
|
+
expect(snapshot?.sevenDayRejected).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should mark snapshot as rejected when 5h status header is rejected', () => {
|
|
127
|
+
const token = 'rejected-5h-token';
|
|
128
|
+
writeRateLimit(token, {
|
|
129
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
130
|
+
'anthropic-ratelimit-unified-5h-status': 'rejected',
|
|
131
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
132
|
+
'anthropic-ratelimit-unified-5h-utilization': '100',
|
|
133
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
134
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
135
|
+
'anthropic-ratelimit-unified-7d-utilization': '99',
|
|
136
|
+
});
|
|
137
|
+
const snapshot = readRateLimit(token);
|
|
138
|
+
expect(snapshot?.rejected).toBe(true);
|
|
139
|
+
expect(snapshot?.fiveHourRejected).toBe(true);
|
|
140
|
+
expect(snapshot?.sevenDayRejected).toBe(false);
|
|
141
|
+
expect(snapshot?.unifiedRejected).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should mark snapshot as rejected when 7d status header is rejected', () => {
|
|
145
|
+
const token = 'rejected-7d-token';
|
|
146
|
+
writeRateLimit(token, {
|
|
147
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
148
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
149
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
150
|
+
'anthropic-ratelimit-unified-5h-utilization': '50',
|
|
151
|
+
'anthropic-ratelimit-unified-7d-status': 'rejected',
|
|
152
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
153
|
+
'anthropic-ratelimit-unified-7d-utilization': '100',
|
|
154
|
+
});
|
|
155
|
+
const snapshot = readRateLimit(token);
|
|
156
|
+
expect(snapshot?.rejected).toBe(true);
|
|
157
|
+
expect(snapshot?.sevenDayRejected).toBe(true);
|
|
158
|
+
expect(snapshot?.fiveHourRejected).toBe(false);
|
|
159
|
+
expect(snapshot?.unifiedRejected).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should not mark snapshot as rejected when no status header is rejected', () => {
|
|
163
|
+
const token = 'allowed-token';
|
|
164
|
+
writeRateLimit(token, {
|
|
165
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
166
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
167
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
168
|
+
'anthropic-ratelimit-unified-5h-utilization': '50',
|
|
169
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
170
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
171
|
+
'anthropic-ratelimit-unified-7d-utilization': '40',
|
|
172
|
+
});
|
|
173
|
+
const snapshot = readRateLimit(token);
|
|
174
|
+
expect(snapshot?.rejected).toBe(false);
|
|
175
|
+
expect(snapshot?.unifiedRejected).toBe(false);
|
|
176
|
+
expect(snapshot?.fiveHourRejected).toBe(false);
|
|
177
|
+
expect(snapshot?.sevenDayRejected).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
104
180
|
it('should return null when file does not exist', () => {
|
|
105
181
|
expect(readRateLimit('never-written-token')).toBeNull();
|
|
106
182
|
});
|
|
@@ -127,5 +203,119 @@ describe('RateLimitCache', () => {
|
|
|
127
203
|
expect(snapshot?.fiveHourUtilization).toBe(55);
|
|
128
204
|
expect(snapshot?.fiveHourReset).toBe(1700000000);
|
|
129
205
|
});
|
|
206
|
+
|
|
207
|
+
it('should default modelWeeklyLimits to an empty object when none are recorded', () => {
|
|
208
|
+
const token = 'no-model-limits-token';
|
|
209
|
+
writeRateLimit(token, {
|
|
210
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
211
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
212
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
213
|
+
'anthropic-ratelimit-unified-5h-utilization': '10',
|
|
214
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
215
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
216
|
+
'anthropic-ratelimit-unified-7d-utilization': '5',
|
|
217
|
+
});
|
|
218
|
+
const snapshot = readRateLimit(token);
|
|
219
|
+
expect(snapshot?.modelWeeklyLimits).toEqual({});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('parseModelRateLimitsFromBody', () => {
|
|
224
|
+
it('should extract a rejected seven_day_sonnet limit from a rate_limit event body', () => {
|
|
225
|
+
const body =
|
|
226
|
+
'event: error\n' +
|
|
227
|
+
'data: {"status":"rejected","resetsAt":1779642000,"rateLimitType":"seven_day_sonnet","overageStatus":"rejected"}\n\n';
|
|
228
|
+
expect(parseModelRateLimitsFromBody(body)).toEqual({
|
|
229
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should extract multiple distinct rate limit types', () => {
|
|
234
|
+
const body =
|
|
235
|
+
'data: {"status":"rejected","resetsAt":1779642000,"rateLimitType":"seven_day_sonnet"}\n' +
|
|
236
|
+
'data: {"status":"allowed","resetsAt":1779700000,"rateLimitType":"seven_day"}\n';
|
|
237
|
+
expect(parseModelRateLimitsFromBody(body)).toEqual({
|
|
238
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
239
|
+
seven_day: { rejected: false, resetsAt: 1779700000 },
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should return an empty object when no rate limit information is present', () => {
|
|
244
|
+
const body =
|
|
245
|
+
'event: message_start\n' +
|
|
246
|
+
'data: {"type":"message_start","message":{"id":"msg_1"}}\n\n';
|
|
247
|
+
expect(parseModelRateLimitsFromBody(body)).toEqual({});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should ignore entries that lack a numeric resetsAt', () => {
|
|
251
|
+
const body =
|
|
252
|
+
'data: {"status":"rejected","rateLimitType":"seven_day_sonnet"}\n';
|
|
253
|
+
expect(parseModelRateLimitsFromBody(body)).toEqual({});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('writeModelRateLimit', () => {
|
|
258
|
+
it('should persist model weekly limits readable by readRateLimit', () => {
|
|
259
|
+
const token = 'model-limit-token';
|
|
260
|
+
writeRateLimit(token, {
|
|
261
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
262
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
263
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
264
|
+
'anthropic-ratelimit-unified-5h-utilization': '10',
|
|
265
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
266
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
267
|
+
'anthropic-ratelimit-unified-7d-utilization': '5',
|
|
268
|
+
});
|
|
269
|
+
writeModelRateLimit(token, {
|
|
270
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
271
|
+
});
|
|
272
|
+
const snapshot = readRateLimit(token);
|
|
273
|
+
expect(snapshot?.modelWeeklyLimits).toEqual({
|
|
274
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
275
|
+
});
|
|
276
|
+
expect(snapshot?.fiveHourUtilization).toBe(10);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should preserve previously recorded model limits when headers are rewritten', () => {
|
|
280
|
+
const token = 'preserve-token';
|
|
281
|
+
writeModelRateLimit(token, {
|
|
282
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
283
|
+
});
|
|
284
|
+
writeRateLimit(token, {
|
|
285
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
286
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
287
|
+
'anthropic-ratelimit-unified-5h-reset': '1700000000',
|
|
288
|
+
'anthropic-ratelimit-unified-5h-utilization': '20',
|
|
289
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
290
|
+
'anthropic-ratelimit-unified-7d-reset': '1700100000',
|
|
291
|
+
'anthropic-ratelimit-unified-7d-utilization': '5',
|
|
292
|
+
});
|
|
293
|
+
const snapshot = readRateLimit(token);
|
|
294
|
+
expect(snapshot?.modelWeeklyLimits).toEqual({
|
|
295
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
296
|
+
});
|
|
297
|
+
expect(snapshot?.fiveHourUtilization).toBe(20);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should merge new model limits with existing ones', () => {
|
|
301
|
+
const token = 'merge-token';
|
|
302
|
+
writeModelRateLimit(token, {
|
|
303
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
304
|
+
});
|
|
305
|
+
writeModelRateLimit(token, {
|
|
306
|
+
seven_day: { rejected: false, resetsAt: 1779700000 },
|
|
307
|
+
});
|
|
308
|
+
const snapshot = readRateLimit(token);
|
|
309
|
+
expect(snapshot?.modelWeeklyLimits).toEqual({
|
|
310
|
+
seven_day_sonnet: { rejected: true, resetsAt: 1779642000 },
|
|
311
|
+
seven_day: { rejected: false, resetsAt: 1779700000 },
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should not write a file when there are no limits to record', () => {
|
|
316
|
+
const token = 'empty-limits-token';
|
|
317
|
+
writeModelRateLimit(token, {});
|
|
318
|
+
expect(readRateLimit(token)).toBeNull();
|
|
319
|
+
});
|
|
130
320
|
});
|
|
131
321
|
});
|
|
@@ -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,6 +80,8 @@ 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);
|
|
42
85
|
const payload = {
|
|
43
86
|
ts: Date.now() / 1000,
|
|
44
87
|
headers: {
|
|
@@ -64,19 +107,70 @@ export const writeRateLimit = (
|
|
|
64
107
|
'anthropic-ratelimit-unified-7d-utilization',
|
|
65
108
|
),
|
|
66
109
|
},
|
|
110
|
+
modelWeeklyLimits: readModelWeeklyLimits(existing),
|
|
67
111
|
};
|
|
112
|
+
fs.writeFileSync(filePath, JSON.stringify(payload));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const writeModelRateLimit = (
|
|
116
|
+
token: string,
|
|
117
|
+
limits: Record<string, ModelWeeklyLimit>,
|
|
118
|
+
): void => {
|
|
119
|
+
const limitTypes = Object.keys(limits);
|
|
120
|
+
if (limitTypes.length === 0) return;
|
|
121
|
+
const dir = cacheDir();
|
|
122
|
+
if (!fs.existsSync(dir)) {
|
|
123
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
124
|
+
}
|
|
68
125
|
const filePath = path.join(dir, `${hashToken(token)}.json`);
|
|
126
|
+
const existing = readPayload(filePath);
|
|
127
|
+
const merged = {
|
|
128
|
+
...readModelWeeklyLimits(existing),
|
|
129
|
+
...limits,
|
|
130
|
+
};
|
|
131
|
+
const payload = {
|
|
132
|
+
...existing,
|
|
133
|
+
modelWeeklyLimits: merged,
|
|
134
|
+
};
|
|
69
135
|
fs.writeFileSync(filePath, JSON.stringify(payload));
|
|
70
136
|
};
|
|
71
137
|
|
|
138
|
+
export const parseModelRateLimitsFromBody = (
|
|
139
|
+
body: string,
|
|
140
|
+
): Record<string, ModelWeeklyLimit> => {
|
|
141
|
+
const result: Record<string, ModelWeeklyLimit> = {};
|
|
142
|
+
const matches = body.match(
|
|
143
|
+
/\{[^{}]*"rateLimitType"[^{}]*\}|\{[^{}]*"resetsAt"[^{}]*"rateLimitType"[^{}]*\}/g,
|
|
144
|
+
);
|
|
145
|
+
if (matches === null) return result;
|
|
146
|
+
for (const candidate of matches) {
|
|
147
|
+
let parsed: unknown;
|
|
148
|
+
try {
|
|
149
|
+
parsed = JSON.parse(candidate);
|
|
150
|
+
} catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!isRecord(parsed)) continue;
|
|
154
|
+
const rateLimitType = parsed.rateLimitType;
|
|
155
|
+
const status = parsed.status;
|
|
156
|
+
const resetsAt = parsed.resetsAt;
|
|
157
|
+
if (typeof rateLimitType !== 'string') continue;
|
|
158
|
+
if (typeof status !== 'string') continue;
|
|
159
|
+
if (typeof resetsAt !== 'number') continue;
|
|
160
|
+
result[rateLimitType] = {
|
|
161
|
+
rejected: status === 'rejected',
|
|
162
|
+
resetsAt,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
|
|
72
168
|
export const readRateLimit = (token: string): RateLimitSnapshot | null => {
|
|
73
169
|
const filePath = cachePathForToken(token);
|
|
74
170
|
if (!fs.existsSync(filePath)) return null;
|
|
75
171
|
try {
|
|
76
172
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
77
173
|
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
174
|
if (!isRecord(parsed)) return null;
|
|
81
175
|
const headersUnknown = parsed.headers;
|
|
82
176
|
const headers: Record<string, string> = {};
|
|
@@ -96,6 +190,9 @@ export const readRateLimit = (token: string): RateLimitSnapshot | null => {
|
|
|
96
190
|
const status = headers['anthropic-ratelimit-unified-status'];
|
|
97
191
|
const fiveHourStatus = headers['anthropic-ratelimit-unified-5h-status'];
|
|
98
192
|
const sevenDayStatus = headers['anthropic-ratelimit-unified-7d-status'];
|
|
193
|
+
const unifiedRejected = status === 'rejected';
|
|
194
|
+
const fiveHourRejected = fiveHourStatus === 'rejected';
|
|
195
|
+
const sevenDayRejected = sevenDayStatus === 'rejected';
|
|
99
196
|
return {
|
|
100
197
|
fiveHourUtilization: num('anthropic-ratelimit-unified-5h-utilization'),
|
|
101
198
|
fiveHourReset: num('anthropic-ratelimit-unified-5h-reset'),
|
|
@@ -105,6 +202,11 @@ export const readRateLimit = (token: string): RateLimitSnapshot | null => {
|
|
|
105
202
|
status === 'blocked' ||
|
|
106
203
|
fiveHourStatus === 'blocked' ||
|
|
107
204
|
sevenDayStatus === 'blocked',
|
|
205
|
+
rejected: unifiedRejected || fiveHourRejected || sevenDayRejected,
|
|
206
|
+
unifiedRejected,
|
|
207
|
+
fiveHourRejected,
|
|
208
|
+
sevenDayRejected,
|
|
209
|
+
modelWeeklyLimits: readModelWeeklyLimits(parsed),
|
|
108
210
|
};
|
|
109
211
|
} catch {
|
|
110
212
|
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,
|