github-issue-tower-defence-management 1.59.0 → 1.60.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/.github/workflows/create-pr.yml +1 -1
- package/.github/workflows/test.yml +4 -0
- package/.github/workflows/umino-project.yml +3 -3
- package/CHANGELOG.md +7 -0
- package/bin/adapter/proxy/ClaudeMessageResponseParser.js +123 -0
- package/bin/adapter/proxy/ClaudeMessageResponseParser.js.map +1 -0
- package/bin/adapter/proxy/proxyEntry.js +16 -3
- package/bin/adapter/proxy/proxyEntry.js.map +1 -1
- package/bin/adapter/repositories/SqliteClaudeMessageResponseRepository.js +186 -0
- package/bin/adapter/repositories/SqliteClaudeMessageResponseRepository.js.map +1 -0
- package/bin/domain/entities/ClaudeMessageResponse.js +3 -0
- package/bin/domain/entities/ClaudeMessageResponse.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository.js.map +1 -0
- package/package.json +3 -1
- package/src/adapter/proxy/ClaudeMessageResponseParser.test.ts +211 -0
- package/src/adapter/proxy/ClaudeMessageResponseParser.ts +180 -0
- package/src/adapter/proxy/proxyEntry.ts +28 -3
- package/src/adapter/repositories/SqliteClaudeMessageResponseRepository.test.ts +313 -0
- package/src/adapter/repositories/SqliteClaudeMessageResponseRepository.ts +164 -0
- package/src/domain/entities/ClaudeMessageResponse.ts +31 -0
- package/src/domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository.ts +5 -0
- package/types/adapter/proxy/ClaudeMessageResponseParser.d.ts +4 -0
- package/types/adapter/proxy/ClaudeMessageResponseParser.d.ts.map +1 -0
- package/types/adapter/proxy/proxyEntry.d.ts +2 -1
- package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
- package/types/adapter/repositories/SqliteClaudeMessageResponseRepository.d.ts +10 -0
- package/types/adapter/repositories/SqliteClaudeMessageResponseRepository.d.ts.map +1 -0
- package/types/domain/entities/ClaudeMessageResponse.d.ts +32 -0
- package/types/domain/entities/ClaudeMessageResponse.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository.d.ts.map +1 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { parseClaudeMessageResponse } from './ClaudeMessageResponseParser';
|
|
2
|
+
|
|
3
|
+
const SUCCESS_BODY = JSON.stringify({
|
|
4
|
+
id: 'msg_01XFDUDYJgAACTvykiHMn8xy',
|
|
5
|
+
type: 'message',
|
|
6
|
+
role: 'assistant',
|
|
7
|
+
model: 'claude-sonnet-4-5',
|
|
8
|
+
stop_reason: 'end_turn',
|
|
9
|
+
stop_sequence: null,
|
|
10
|
+
usage: {
|
|
11
|
+
input_tokens: 100,
|
|
12
|
+
output_tokens: 50,
|
|
13
|
+
cache_creation_input_tokens: null,
|
|
14
|
+
cache_read_input_tokens: null,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const ERROR_BODY = JSON.stringify({
|
|
19
|
+
type: 'error',
|
|
20
|
+
error: {
|
|
21
|
+
type: 'rate_limit_error',
|
|
22
|
+
message: 'Too many requests',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const SUCCESS_HEADERS = {
|
|
27
|
+
'x-request-id': 'req_01234',
|
|
28
|
+
'anthropic-ratelimit-unified-status': 'active',
|
|
29
|
+
'anthropic-ratelimit-unified-5h-status': 'active',
|
|
30
|
+
'anthropic-ratelimit-unified-5h-utilization': '42.5',
|
|
31
|
+
'anthropic-ratelimit-unified-5h-reset': '1705316200',
|
|
32
|
+
'anthropic-ratelimit-unified-7d-status': 'active',
|
|
33
|
+
'anthropic-ratelimit-unified-7d-utilization': '10.0',
|
|
34
|
+
'anthropic-ratelimit-unified-7d-reset': '1705920000',
|
|
35
|
+
'anthropic-organization-id': 'org-abc123',
|
|
36
|
+
'anthropic-service-tier': 'standard',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe('parseClaudeMessageResponse', () => {
|
|
40
|
+
it('parses a successful 200 response with all fields', () => {
|
|
41
|
+
const result = parseClaudeMessageResponse(
|
|
42
|
+
'hashed-token',
|
|
43
|
+
200,
|
|
44
|
+
SUCCESS_HEADERS,
|
|
45
|
+
SUCCESS_BODY,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(result.tokenName).toBe('hashed-token');
|
|
49
|
+
expect(result.httpStatus).toBe(200);
|
|
50
|
+
expect(result.externalClaudeMessageId).toBe('msg_01XFDUDYJgAACTvykiHMn8xy');
|
|
51
|
+
expect(result.externalClaudeRequestId).toBe('req_01234');
|
|
52
|
+
expect(result.model).toBe('claude-sonnet-4-5');
|
|
53
|
+
expect(result.role).toBe('assistant');
|
|
54
|
+
expect(result.stopReason).toBe('end_turn');
|
|
55
|
+
expect(result.stopSequence).toBeNull();
|
|
56
|
+
expect(result.inputTokens).toBe(100);
|
|
57
|
+
expect(result.outputTokens).toBe(50);
|
|
58
|
+
expect(result.cacheCreationInputTokens).toBeNull();
|
|
59
|
+
expect(result.cacheReadInputTokens).toBeNull();
|
|
60
|
+
expect(result.serviceTier).toBe('standard');
|
|
61
|
+
expect(result.errorType).toBeNull();
|
|
62
|
+
expect(result.errorMessage).toBeNull();
|
|
63
|
+
expect(result.anthropicRatelimitUnifiedStatus).toBe('active');
|
|
64
|
+
expect(result.anthropicRatelimitUnified5hStatus).toBe('active');
|
|
65
|
+
expect(result.anthropicRatelimitUnified5hUtilization).toBe(42.5);
|
|
66
|
+
expect(result.anthropicRatelimitUnified5hReset).toBe(1705316200);
|
|
67
|
+
expect(result.anthropicRatelimitUnified7dStatus).toBe('active');
|
|
68
|
+
expect(result.anthropicRatelimitUnified7dUtilization).toBe(10.0);
|
|
69
|
+
expect(result.anthropicRatelimitUnified7dReset).toBe(1705920000);
|
|
70
|
+
expect(result.retryAfter).toBeNull();
|
|
71
|
+
expect(result.anthropicOrganizationId).toBe('org-abc123');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('assigns a non-empty ULID as id', () => {
|
|
75
|
+
const result = parseClaudeMessageResponse('tok', 200, {}, '{}');
|
|
76
|
+
expect(result.id).toHaveLength(26);
|
|
77
|
+
expect(/^[0-9A-HJKMNP-TV-Z]{26}$/.test(result.id)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('sets observedAt to a recent Date', () => {
|
|
81
|
+
const before = Date.now();
|
|
82
|
+
const result = parseClaudeMessageResponse('tok', 200, {}, '{}');
|
|
83
|
+
const after = Date.now();
|
|
84
|
+
expect(result.observedAt.getTime()).toBeGreaterThanOrEqual(before);
|
|
85
|
+
expect(result.observedAt.getTime()).toBeLessThanOrEqual(after);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('parses an error response body', () => {
|
|
89
|
+
const result = parseClaudeMessageResponse(
|
|
90
|
+
'hashed-token',
|
|
91
|
+
429,
|
|
92
|
+
{ 'retry-after': '30' },
|
|
93
|
+
ERROR_BODY,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(result.httpStatus).toBe(429);
|
|
97
|
+
expect(result.errorType).toBe('rate_limit_error');
|
|
98
|
+
expect(result.errorMessage).toBe('Too many requests');
|
|
99
|
+
expect(result.retryAfter).toBe(30);
|
|
100
|
+
expect(result.model).toBeNull();
|
|
101
|
+
expect(result.role).toBeNull();
|
|
102
|
+
expect(result.stopReason).toBeNull();
|
|
103
|
+
expect(result.inputTokens).toBeNull();
|
|
104
|
+
expect(result.outputTokens).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns null for missing body fields when body is empty object', () => {
|
|
108
|
+
const result = parseClaudeMessageResponse('tok', 200, {}, '{}');
|
|
109
|
+
|
|
110
|
+
expect(result.externalClaudeMessageId).toBeNull();
|
|
111
|
+
expect(result.model).toBeNull();
|
|
112
|
+
expect(result.role).toBeNull();
|
|
113
|
+
expect(result.stopReason).toBeNull();
|
|
114
|
+
expect(result.stopSequence).toBeNull();
|
|
115
|
+
expect(result.inputTokens).toBeNull();
|
|
116
|
+
expect(result.outputTokens).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('returns null for rate limit headers when they are absent', () => {
|
|
120
|
+
const result = parseClaudeMessageResponse('tok', 200, {}, '{}');
|
|
121
|
+
|
|
122
|
+
expect(result.anthropicRatelimitUnifiedStatus).toBeNull();
|
|
123
|
+
expect(result.anthropicRatelimitUnified5hStatus).toBeNull();
|
|
124
|
+
expect(result.anthropicRatelimitUnified5hUtilization).toBeNull();
|
|
125
|
+
expect(result.anthropicRatelimitUnified5hReset).toBeNull();
|
|
126
|
+
expect(result.anthropicOrganizationId).toBeNull();
|
|
127
|
+
expect(result.retryAfter).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('handles non-JSON body gracefully by leaving body fields null', () => {
|
|
131
|
+
const result = parseClaudeMessageResponse(
|
|
132
|
+
'tok',
|
|
133
|
+
200,
|
|
134
|
+
{},
|
|
135
|
+
'this is not json',
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(result.externalClaudeMessageId).toBeNull();
|
|
139
|
+
expect(result.model).toBeNull();
|
|
140
|
+
expect(result.inputTokens).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('handles empty body string gracefully', () => {
|
|
144
|
+
const result = parseClaudeMessageResponse('tok', 200, {}, '');
|
|
145
|
+
|
|
146
|
+
expect(result.externalClaudeMessageId).toBeNull();
|
|
147
|
+
expect(result.model).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('picks the first value when a header is an array', () => {
|
|
151
|
+
const result = parseClaudeMessageResponse(
|
|
152
|
+
'tok',
|
|
153
|
+
200,
|
|
154
|
+
{ 'x-request-id': ['req-first', 'req-second'] },
|
|
155
|
+
'{}',
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.externalClaudeRequestId).toBe('req-first');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('parses cache token fields from usage', () => {
|
|
162
|
+
const body = JSON.stringify({
|
|
163
|
+
id: 'msg_cache',
|
|
164
|
+
role: 'assistant',
|
|
165
|
+
model: 'claude-sonnet-4-5',
|
|
166
|
+
stop_reason: 'end_turn',
|
|
167
|
+
usage: {
|
|
168
|
+
input_tokens: 200,
|
|
169
|
+
output_tokens: 80,
|
|
170
|
+
cache_creation_input_tokens: 150,
|
|
171
|
+
cache_read_input_tokens: 50,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = parseClaudeMessageResponse('tok', 200, {}, body);
|
|
176
|
+
|
|
177
|
+
expect(result.cacheCreationInputTokens).toBe(150);
|
|
178
|
+
expect(result.cacheReadInputTokens).toBe(50);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('parses ephemeral token fields from usage', () => {
|
|
182
|
+
const body = JSON.stringify({
|
|
183
|
+
id: 'msg_ephemeral',
|
|
184
|
+
role: 'assistant',
|
|
185
|
+
model: 'claude-sonnet-4-5',
|
|
186
|
+
stop_reason: 'end_turn',
|
|
187
|
+
usage: {
|
|
188
|
+
input_tokens: 300,
|
|
189
|
+
output_tokens: 120,
|
|
190
|
+
ephemeral_5m_input_tokens: 100,
|
|
191
|
+
ephemeral_1h_input_tokens: 200,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = parseClaudeMessageResponse('tok', 200, {}, body);
|
|
196
|
+
|
|
197
|
+
expect(result.ephemeral5mInputTokens).toBe(100);
|
|
198
|
+
expect(result.ephemeral1hInputTokens).toBe(200);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('parses a non-finite utilization header as null', () => {
|
|
202
|
+
const result = parseClaudeMessageResponse(
|
|
203
|
+
'tok',
|
|
204
|
+
200,
|
|
205
|
+
{ 'anthropic-ratelimit-unified-5h-utilization': 'not-a-number' },
|
|
206
|
+
'{}',
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(result.anthropicRatelimitUnified5hUtilization).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { ClaudeMessageResponse } from '../../domain/entities/ClaudeMessageResponse';
|
|
3
|
+
import { generateUlid } from '../repositories/SqliteClaudeMessageResponseRepository';
|
|
4
|
+
|
|
5
|
+
const pickHeader = (
|
|
6
|
+
headers: http.IncomingHttpHeaders,
|
|
7
|
+
key: string,
|
|
8
|
+
): string | null => {
|
|
9
|
+
const value = headers[key];
|
|
10
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
11
|
+
return value ?? null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const parseNullableFloat = (value: string | null): number | null => {
|
|
15
|
+
if (value === null) return null;
|
|
16
|
+
const parsed = parseFloat(value);
|
|
17
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const parseNullableInt = (value: unknown): number | null => {
|
|
21
|
+
if (value === null || value === undefined) return null;
|
|
22
|
+
const parsed =
|
|
23
|
+
typeof value === 'number' ? value : parseInt(String(value), 10);
|
|
24
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
28
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
|
|
30
|
+
const extractUsage = (
|
|
31
|
+
body: Record<string, unknown>,
|
|
32
|
+
): {
|
|
33
|
+
inputTokens: number | null;
|
|
34
|
+
outputTokens: number | null;
|
|
35
|
+
cacheCreationInputTokens: number | null;
|
|
36
|
+
cacheReadInputTokens: number | null;
|
|
37
|
+
ephemeral5mInputTokens: number | null;
|
|
38
|
+
ephemeral1hInputTokens: number | null;
|
|
39
|
+
} => {
|
|
40
|
+
const usage = body['usage'];
|
|
41
|
+
if (!isRecord(usage)) {
|
|
42
|
+
return {
|
|
43
|
+
inputTokens: null,
|
|
44
|
+
outputTokens: null,
|
|
45
|
+
cacheCreationInputTokens: null,
|
|
46
|
+
cacheReadInputTokens: null,
|
|
47
|
+
ephemeral5mInputTokens: null,
|
|
48
|
+
ephemeral1hInputTokens: null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
inputTokens: parseNullableInt(usage['input_tokens']),
|
|
53
|
+
outputTokens: parseNullableInt(usage['output_tokens']),
|
|
54
|
+
cacheCreationInputTokens: parseNullableInt(
|
|
55
|
+
usage['cache_creation_input_tokens'],
|
|
56
|
+
),
|
|
57
|
+
cacheReadInputTokens: parseNullableInt(usage['cache_read_input_tokens']),
|
|
58
|
+
ephemeral5mInputTokens: parseNullableInt(
|
|
59
|
+
usage['ephemeral_5m_input_tokens'],
|
|
60
|
+
),
|
|
61
|
+
ephemeral1hInputTokens: parseNullableInt(
|
|
62
|
+
usage['ephemeral_1h_input_tokens'],
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const extractRole = (body: Record<string, unknown>): string | null => {
|
|
68
|
+
const role = body['role'];
|
|
69
|
+
return typeof role === 'string' ? role : null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const extractFirstContentRole = (
|
|
73
|
+
body: Record<string, unknown>,
|
|
74
|
+
): string | null => {
|
|
75
|
+
const content = body['content'];
|
|
76
|
+
if (!Array.isArray(content)) return null;
|
|
77
|
+
const first: unknown = content[0];
|
|
78
|
+
if (!isRecord(first)) return null;
|
|
79
|
+
const role = first['role'];
|
|
80
|
+
return typeof role === 'string' ? role : null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const parseClaudeMessageResponse = (
|
|
84
|
+
tokenName: string,
|
|
85
|
+
httpStatus: number,
|
|
86
|
+
headers: http.IncomingHttpHeaders,
|
|
87
|
+
body: string,
|
|
88
|
+
): ClaudeMessageResponse => {
|
|
89
|
+
let parsedBody: Record<string, unknown> = {};
|
|
90
|
+
try {
|
|
91
|
+
const parsed: unknown = JSON.parse(body);
|
|
92
|
+
if (isRecord(parsed)) {
|
|
93
|
+
parsedBody = parsed;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
parsedBody = {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const errorObj = parsedBody['error'];
|
|
100
|
+
const errorRecord = isRecord(errorObj) ? errorObj : null;
|
|
101
|
+
const errorType =
|
|
102
|
+
errorRecord !== null && typeof errorRecord['type'] === 'string'
|
|
103
|
+
? errorRecord['type']
|
|
104
|
+
: null;
|
|
105
|
+
const errorMessage =
|
|
106
|
+
errorRecord !== null && typeof errorRecord['message'] === 'string'
|
|
107
|
+
? errorRecord['message']
|
|
108
|
+
: null;
|
|
109
|
+
|
|
110
|
+
const externalClaudeMessageId =
|
|
111
|
+
typeof parsedBody['id'] === 'string' ? parsedBody['id'] : null;
|
|
112
|
+
|
|
113
|
+
const model =
|
|
114
|
+
typeof parsedBody['model'] === 'string' ? parsedBody['model'] : null;
|
|
115
|
+
|
|
116
|
+
const role = extractRole(parsedBody) ?? extractFirstContentRole(parsedBody);
|
|
117
|
+
|
|
118
|
+
const stopReason =
|
|
119
|
+
typeof parsedBody['stop_reason'] === 'string'
|
|
120
|
+
? parsedBody['stop_reason']
|
|
121
|
+
: null;
|
|
122
|
+
const stopSequence =
|
|
123
|
+
typeof parsedBody['stop_sequence'] === 'string'
|
|
124
|
+
? parsedBody['stop_sequence']
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
const usage = extractUsage(parsedBody);
|
|
128
|
+
|
|
129
|
+
const retryAfterRaw = pickHeader(headers, 'retry-after');
|
|
130
|
+
const retryAfter = parseNullableFloat(retryAfterRaw);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
id: generateUlid(),
|
|
134
|
+
observedAt: new Date(),
|
|
135
|
+
tokenName,
|
|
136
|
+
externalClaudeMessageId,
|
|
137
|
+
externalClaudeRequestId: pickHeader(headers, 'x-request-id'),
|
|
138
|
+
httpStatus,
|
|
139
|
+
model,
|
|
140
|
+
role,
|
|
141
|
+
stopReason,
|
|
142
|
+
stopSequence,
|
|
143
|
+
inputTokens: usage.inputTokens,
|
|
144
|
+
outputTokens: usage.outputTokens,
|
|
145
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
|
146
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
147
|
+
ephemeral5mInputTokens: usage.ephemeral5mInputTokens,
|
|
148
|
+
ephemeral1hInputTokens: usage.ephemeral1hInputTokens,
|
|
149
|
+
serviceTier: pickHeader(headers, 'anthropic-service-tier'),
|
|
150
|
+
inferenceGeo: pickHeader(headers, 'anthropic-inference-geo'),
|
|
151
|
+
errorType,
|
|
152
|
+
errorMessage,
|
|
153
|
+
anthropicRatelimitUnifiedStatus: pickHeader(
|
|
154
|
+
headers,
|
|
155
|
+
'anthropic-ratelimit-unified-status',
|
|
156
|
+
),
|
|
157
|
+
anthropicRatelimitUnified5hStatus: pickHeader(
|
|
158
|
+
headers,
|
|
159
|
+
'anthropic-ratelimit-unified-5h-status',
|
|
160
|
+
),
|
|
161
|
+
anthropicRatelimitUnified5hUtilization: parseNullableFloat(
|
|
162
|
+
pickHeader(headers, 'anthropic-ratelimit-unified-5h-utilization'),
|
|
163
|
+
),
|
|
164
|
+
anthropicRatelimitUnified5hReset: parseNullableFloat(
|
|
165
|
+
pickHeader(headers, 'anthropic-ratelimit-unified-5h-reset'),
|
|
166
|
+
),
|
|
167
|
+
anthropicRatelimitUnified7dStatus: pickHeader(
|
|
168
|
+
headers,
|
|
169
|
+
'anthropic-ratelimit-unified-7d-status',
|
|
170
|
+
),
|
|
171
|
+
anthropicRatelimitUnified7dUtilization: parseNullableFloat(
|
|
172
|
+
pickHeader(headers, 'anthropic-ratelimit-unified-7d-utilization'),
|
|
173
|
+
),
|
|
174
|
+
anthropicRatelimitUnified7dReset: parseNullableFloat(
|
|
175
|
+
pickHeader(headers, 'anthropic-ratelimit-unified-7d-reset'),
|
|
176
|
+
),
|
|
177
|
+
retryAfter,
|
|
178
|
+
anthropicOrganizationId: pickHeader(headers, 'anthropic-organization-id'),
|
|
179
|
+
};
|
|
180
|
+
};
|
|
@@ -2,10 +2,14 @@ import * as http from 'http';
|
|
|
2
2
|
import * as https from 'https';
|
|
3
3
|
import {
|
|
4
4
|
PROXY_PORT,
|
|
5
|
+
hashToken,
|
|
5
6
|
parseModelRateLimitsFromBody,
|
|
6
7
|
writeModelRateLimit,
|
|
7
8
|
writeRateLimit,
|
|
8
9
|
} from './RateLimitCache';
|
|
10
|
+
import { ClaudeMessageResponseRepository } from '../../domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository';
|
|
11
|
+
import { parseClaudeMessageResponse } from './ClaudeMessageResponseParser';
|
|
12
|
+
import { SqliteClaudeMessageResponseRepository } from '../repositories/SqliteClaudeMessageResponseRepository';
|
|
9
13
|
|
|
10
14
|
const UPSTREAM_HOST = 'api.anthropic.com';
|
|
11
15
|
|
|
@@ -25,7 +29,10 @@ const extractToken = (
|
|
|
25
29
|
return token.length > 0 ? token : null;
|
|
26
30
|
};
|
|
27
31
|
|
|
28
|
-
const startProxy = (
|
|
32
|
+
const startProxy = (
|
|
33
|
+
port: number,
|
|
34
|
+
claudeMessageResponseRepository: ClaudeMessageResponseRepository | null = null,
|
|
35
|
+
): void => {
|
|
29
36
|
const server = http.createServer((clientRequest, clientResponse) => {
|
|
30
37
|
const token = extractToken(clientRequest.headers['authorization']);
|
|
31
38
|
const upstreamHeaders: Record<string, string | string[] | undefined> = {
|
|
@@ -55,13 +62,29 @@ const startProxy = (port: number): void => {
|
|
|
55
62
|
inspectedBytes += chunk.length;
|
|
56
63
|
});
|
|
57
64
|
upstreamResponse.on('end', () => {
|
|
65
|
+
const body = Buffer.concat(inspectedChunks).toString('utf8');
|
|
58
66
|
try {
|
|
59
|
-
const body = Buffer.concat(inspectedChunks).toString('utf8');
|
|
60
67
|
const limits = parseModelRateLimitsFromBody(body);
|
|
61
68
|
writeModelRateLimit(token, limits);
|
|
62
69
|
} catch (error) {
|
|
63
70
|
console.error('Failed to write model rate limit cache:', error);
|
|
64
71
|
}
|
|
72
|
+
if (claudeMessageResponseRepository !== null) {
|
|
73
|
+
try {
|
|
74
|
+
const response = parseClaudeMessageResponse(
|
|
75
|
+
hashToken(token),
|
|
76
|
+
upstreamResponse.statusCode ?? 0,
|
|
77
|
+
upstreamResponse.headers,
|
|
78
|
+
body,
|
|
79
|
+
);
|
|
80
|
+
claudeMessageResponseRepository.append(response);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(
|
|
83
|
+
'Failed to record Claude message response:',
|
|
84
|
+
error,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
65
88
|
});
|
|
66
89
|
}
|
|
67
90
|
clientResponse.writeHead(
|
|
@@ -86,7 +109,9 @@ const startProxy = (port: number): void => {
|
|
|
86
109
|
};
|
|
87
110
|
|
|
88
111
|
if (require.main === module) {
|
|
89
|
-
|
|
112
|
+
const dbPath = './db/claude_message_response.db';
|
|
113
|
+
const repository = new SqliteClaudeMessageResponseRepository(dbPath);
|
|
114
|
+
startProxy(PROXY_PORT, repository);
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
export { startProxy, extractToken };
|