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.
Files changed (35) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +3 -3
  3. package/bin/adapter/entry-points/cli/index.js +1 -3
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -3
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  7. package/bin/adapter/proxy/RateLimitCache.js +94 -3
  8. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  9. package/bin/adapter/proxy/proxyEntry.js +19 -0
  10. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  11. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +33 -2
  12. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
  13. package/bin/domain/usecases/StartPreparationUseCase.js +34 -35
  14. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/cli/index.ts +0 -3
  17. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -3
  18. package/src/adapter/proxy/RateLimitCache.test.ts +190 -0
  19. package/src/adapter/proxy/RateLimitCache.ts +104 -2
  20. package/src/adapter/proxy/proxyEntry.ts +24 -1
  21. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +354 -7
  22. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +42 -2
  23. package/src/domain/entities/ClaudeTokenUsage.ts +7 -0
  24. package/src/domain/usecases/StartPreparationUseCase.test.ts +1212 -887
  25. package/src/domain/usecases/StartPreparationUseCase.ts +47 -57
  26. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  27. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  28. package/types/adapter/proxy/RateLimitCache.d.ts +11 -0
  29. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  30. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  31. package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
  32. package/types/domain/entities/ClaudeTokenUsage.d.ts +6 -0
  33. package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
  34. package/types/domain/usecases/StartPreparationUseCase.d.ts +3 -3
  35. 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 { PROXY_PORT, writeRateLimit } from './RateLimitCache';
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,