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,313 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import {
|
|
6
|
+
SqliteClaudeMessageResponseRepository,
|
|
7
|
+
generateUlid,
|
|
8
|
+
} from './SqliteClaudeMessageResponseRepository';
|
|
9
|
+
import { ClaudeMessageResponse } from '../../domain/entities/ClaudeMessageResponse';
|
|
10
|
+
|
|
11
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
12
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
const requireRecord = (value: unknown): Record<string, unknown> => {
|
|
15
|
+
if (!isRecord(value))
|
|
16
|
+
throw new Error(`Expected record, got: ${String(value)}`);
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const requireRecordArray = (value: unknown[]): Array<Record<string, unknown>> =>
|
|
21
|
+
value.map(requireRecord);
|
|
22
|
+
|
|
23
|
+
const buildTestResponse = (
|
|
24
|
+
overrides: Partial<ClaudeMessageResponse> = {},
|
|
25
|
+
): ClaudeMessageResponse => ({
|
|
26
|
+
id: generateUlid(),
|
|
27
|
+
observedAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
28
|
+
tokenName: 'hashed-token-abc',
|
|
29
|
+
externalClaudeMessageId: 'msg_01XFDUDYJgAACTvykiHMn8xy',
|
|
30
|
+
externalClaudeRequestId: 'req_01234',
|
|
31
|
+
httpStatus: 200,
|
|
32
|
+
model: 'claude-sonnet-4-5',
|
|
33
|
+
role: 'assistant',
|
|
34
|
+
stopReason: 'end_turn',
|
|
35
|
+
stopSequence: null,
|
|
36
|
+
inputTokens: 100,
|
|
37
|
+
outputTokens: 50,
|
|
38
|
+
cacheCreationInputTokens: null,
|
|
39
|
+
cacheReadInputTokens: null,
|
|
40
|
+
ephemeral5mInputTokens: null,
|
|
41
|
+
ephemeral1hInputTokens: null,
|
|
42
|
+
serviceTier: 'standard',
|
|
43
|
+
inferenceGeo: null,
|
|
44
|
+
errorType: null,
|
|
45
|
+
errorMessage: null,
|
|
46
|
+
anthropicRatelimitUnifiedStatus: 'active',
|
|
47
|
+
anthropicRatelimitUnified5hStatus: 'active',
|
|
48
|
+
anthropicRatelimitUnified5hUtilization: 42.5,
|
|
49
|
+
anthropicRatelimitUnified5hReset: 1705316200,
|
|
50
|
+
anthropicRatelimitUnified7dStatus: 'active',
|
|
51
|
+
anthropicRatelimitUnified7dUtilization: 10.0,
|
|
52
|
+
anthropicRatelimitUnified7dReset: 1705920000,
|
|
53
|
+
retryAfter: null,
|
|
54
|
+
anthropicOrganizationId: 'org-abc123',
|
|
55
|
+
...overrides,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('generateUlid', () => {
|
|
59
|
+
it('generates a 26-character uppercase alphanumeric string', () => {
|
|
60
|
+
const ulid = generateUlid();
|
|
61
|
+
expect(ulid).toHaveLength(26);
|
|
62
|
+
expect(/^[0-9A-HJKMNP-TV-Z]{26}$/.test(ulid)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('generates unique values across multiple calls', () => {
|
|
66
|
+
const ids = new Set(Array.from({ length: 100 }, () => generateUlid()));
|
|
67
|
+
expect(ids.size).toBe(100);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('generates a value whose first 10 characters encode a timestamp no later than the current time', () => {
|
|
71
|
+
const before = Date.now();
|
|
72
|
+
const ulid = generateUlid();
|
|
73
|
+
const after = Date.now();
|
|
74
|
+
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
75
|
+
const timeChars = ulid.slice(0, 10);
|
|
76
|
+
let decoded = 0;
|
|
77
|
+
for (const char of timeChars) {
|
|
78
|
+
decoded = decoded * 32 + ENCODING.indexOf(char);
|
|
79
|
+
}
|
|
80
|
+
expect(decoded).toBeGreaterThanOrEqual(before);
|
|
81
|
+
expect(decoded).toBeLessThanOrEqual(after);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('SqliteClaudeMessageResponseRepository', () => {
|
|
86
|
+
let tmpDir: string;
|
|
87
|
+
let dbPath: string;
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-msg-repo-test-'));
|
|
91
|
+
dbPath = path.join(tmpDir, 'claude_message_response.db');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('constructor', () => {
|
|
99
|
+
it('creates the database file when it does not exist', () => {
|
|
100
|
+
new SqliteClaudeMessageResponseRepository(dbPath);
|
|
101
|
+
expect(fs.existsSync(dbPath)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('creates the parent directory when it does not exist', () => {
|
|
105
|
+
const nestedDbPath = path.join(tmpDir, 'nested', 'dir', 'test.db');
|
|
106
|
+
new SqliteClaudeMessageResponseRepository(nestedDbPath);
|
|
107
|
+
expect(fs.existsSync(nestedDbPath)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('creates the claude_message_response table on first open', () => {
|
|
111
|
+
new SqliteClaudeMessageResponseRepository(dbPath);
|
|
112
|
+
const db = new Database(dbPath);
|
|
113
|
+
const row = db
|
|
114
|
+
.prepare(
|
|
115
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='claude_message_response'",
|
|
116
|
+
)
|
|
117
|
+
.get();
|
|
118
|
+
db.close();
|
|
119
|
+
expect(row).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not fail when reopened against an existing database', () => {
|
|
123
|
+
new SqliteClaudeMessageResponseRepository(dbPath);
|
|
124
|
+
expect(() => {
|
|
125
|
+
new SqliteClaudeMessageResponseRepository(dbPath);
|
|
126
|
+
}).not.toThrow();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('append', () => {
|
|
131
|
+
it('inserts a row with all fields set', () => {
|
|
132
|
+
const repo = new SqliteClaudeMessageResponseRepository(dbPath);
|
|
133
|
+
const response = buildTestResponse();
|
|
134
|
+
|
|
135
|
+
repo.append(response);
|
|
136
|
+
|
|
137
|
+
const db = new Database(dbPath);
|
|
138
|
+
const row = requireRecord(
|
|
139
|
+
db
|
|
140
|
+
.prepare('SELECT * FROM claude_message_response WHERE id = ?')
|
|
141
|
+
.get(response.id),
|
|
142
|
+
);
|
|
143
|
+
db.close();
|
|
144
|
+
|
|
145
|
+
expect(row['id']).toBe(response.id);
|
|
146
|
+
expect(row['observed_at']).toBe(response.observedAt.getTime());
|
|
147
|
+
expect(row['token_name']).toBe(response.tokenName);
|
|
148
|
+
expect(row['external_claude_message_id']).toBe(
|
|
149
|
+
response.externalClaudeMessageId,
|
|
150
|
+
);
|
|
151
|
+
expect(row['external_claude_request_id']).toBe(
|
|
152
|
+
response.externalClaudeRequestId,
|
|
153
|
+
);
|
|
154
|
+
expect(row['http_status']).toBe(response.httpStatus);
|
|
155
|
+
expect(row['model']).toBe(response.model);
|
|
156
|
+
expect(row['role']).toBe(response.role);
|
|
157
|
+
expect(row['stop_reason']).toBe(response.stopReason);
|
|
158
|
+
expect(row['stop_sequence']).toBeNull();
|
|
159
|
+
expect(row['input_tokens']).toBe(response.inputTokens);
|
|
160
|
+
expect(row['output_tokens']).toBe(response.outputTokens);
|
|
161
|
+
expect(row['cache_creation_input_tokens']).toBeNull();
|
|
162
|
+
expect(row['cache_read_input_tokens']).toBeNull();
|
|
163
|
+
expect(row['ephemeral_5m_input_tokens']).toBeNull();
|
|
164
|
+
expect(row['ephemeral_1h_input_tokens']).toBeNull();
|
|
165
|
+
expect(row['service_tier']).toBe(response.serviceTier);
|
|
166
|
+
expect(row['inference_geo']).toBeNull();
|
|
167
|
+
expect(row['error_type']).toBeNull();
|
|
168
|
+
expect(row['error_message']).toBeNull();
|
|
169
|
+
expect(row['anthropic_ratelimit_unified_status']).toBe(
|
|
170
|
+
response.anthropicRatelimitUnifiedStatus,
|
|
171
|
+
);
|
|
172
|
+
expect(row['anthropic_ratelimit_unified_5h_status']).toBe(
|
|
173
|
+
response.anthropicRatelimitUnified5hStatus,
|
|
174
|
+
);
|
|
175
|
+
expect(row['anthropic_ratelimit_unified_5h_utilization']).toBe(
|
|
176
|
+
response.anthropicRatelimitUnified5hUtilization,
|
|
177
|
+
);
|
|
178
|
+
expect(row['anthropic_ratelimit_unified_5h_reset']).toBe(
|
|
179
|
+
response.anthropicRatelimitUnified5hReset,
|
|
180
|
+
);
|
|
181
|
+
expect(row['anthropic_ratelimit_unified_7d_status']).toBe(
|
|
182
|
+
response.anthropicRatelimitUnified7dStatus,
|
|
183
|
+
);
|
|
184
|
+
expect(row['anthropic_ratelimit_unified_7d_utilization']).toBe(
|
|
185
|
+
response.anthropicRatelimitUnified7dUtilization,
|
|
186
|
+
);
|
|
187
|
+
expect(row['anthropic_ratelimit_unified_7d_reset']).toBe(
|
|
188
|
+
response.anthropicRatelimitUnified7dReset,
|
|
189
|
+
);
|
|
190
|
+
expect(row['retry_after']).toBeNull();
|
|
191
|
+
expect(row['anthropic_organization_id']).toBe(
|
|
192
|
+
response.anthropicOrganizationId,
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('inserts a row where all nullable fields are null', () => {
|
|
197
|
+
const repo = new SqliteClaudeMessageResponseRepository(dbPath);
|
|
198
|
+
const response = buildTestResponse({
|
|
199
|
+
externalClaudeMessageId: null,
|
|
200
|
+
externalClaudeRequestId: null,
|
|
201
|
+
model: null,
|
|
202
|
+
role: null,
|
|
203
|
+
stopReason: null,
|
|
204
|
+
stopSequence: null,
|
|
205
|
+
inputTokens: null,
|
|
206
|
+
outputTokens: null,
|
|
207
|
+
cacheCreationInputTokens: null,
|
|
208
|
+
cacheReadInputTokens: null,
|
|
209
|
+
ephemeral5mInputTokens: null,
|
|
210
|
+
ephemeral1hInputTokens: null,
|
|
211
|
+
serviceTier: null,
|
|
212
|
+
inferenceGeo: null,
|
|
213
|
+
errorType: null,
|
|
214
|
+
errorMessage: null,
|
|
215
|
+
anthropicRatelimitUnifiedStatus: null,
|
|
216
|
+
anthropicRatelimitUnified5hStatus: null,
|
|
217
|
+
anthropicRatelimitUnified5hUtilization: null,
|
|
218
|
+
anthropicRatelimitUnified5hReset: null,
|
|
219
|
+
anthropicRatelimitUnified7dStatus: null,
|
|
220
|
+
anthropicRatelimitUnified7dUtilization: null,
|
|
221
|
+
anthropicRatelimitUnified7dReset: null,
|
|
222
|
+
retryAfter: null,
|
|
223
|
+
anthropicOrganizationId: null,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
repo.append(response);
|
|
227
|
+
|
|
228
|
+
const db = new Database(dbPath);
|
|
229
|
+
const row = requireRecord(
|
|
230
|
+
db
|
|
231
|
+
.prepare('SELECT * FROM claude_message_response WHERE id = ?')
|
|
232
|
+
.get(response.id),
|
|
233
|
+
);
|
|
234
|
+
db.close();
|
|
235
|
+
|
|
236
|
+
expect(row['id']).toBe(response.id);
|
|
237
|
+
expect(row['http_status']).toBe(response.httpStatus);
|
|
238
|
+
expect(row['external_claude_message_id']).toBeNull();
|
|
239
|
+
expect(row['model']).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('inserts an error response with error fields set', () => {
|
|
243
|
+
const repo = new SqliteClaudeMessageResponseRepository(dbPath);
|
|
244
|
+
const response = buildTestResponse({
|
|
245
|
+
httpStatus: 429,
|
|
246
|
+
model: null,
|
|
247
|
+
role: null,
|
|
248
|
+
stopReason: null,
|
|
249
|
+
inputTokens: null,
|
|
250
|
+
outputTokens: null,
|
|
251
|
+
errorType: 'rate_limit_error',
|
|
252
|
+
errorMessage: 'Too many requests',
|
|
253
|
+
retryAfter: 30,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
repo.append(response);
|
|
257
|
+
|
|
258
|
+
const db = new Database(dbPath);
|
|
259
|
+
const row = requireRecord(
|
|
260
|
+
db
|
|
261
|
+
.prepare('SELECT * FROM claude_message_response WHERE id = ?')
|
|
262
|
+
.get(response.id),
|
|
263
|
+
);
|
|
264
|
+
db.close();
|
|
265
|
+
|
|
266
|
+
expect(row['http_status']).toBe(429);
|
|
267
|
+
expect(row['error_type']).toBe('rate_limit_error');
|
|
268
|
+
expect(row['error_message']).toBe('Too many requests');
|
|
269
|
+
expect(row['retry_after']).toBe(30);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('inserts multiple rows appending each independently', () => {
|
|
273
|
+
const repo = new SqliteClaudeMessageResponseRepository(dbPath);
|
|
274
|
+
const responses = [
|
|
275
|
+
buildTestResponse({ httpStatus: 200 }),
|
|
276
|
+
buildTestResponse({ httpStatus: 429 }),
|
|
277
|
+
buildTestResponse({ httpStatus: 200 }),
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
for (const response of responses) {
|
|
281
|
+
repo.append(response);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const db = new Database(dbPath);
|
|
285
|
+
const rows = requireRecordArray(
|
|
286
|
+
db.prepare('SELECT id FROM claude_message_response').all(),
|
|
287
|
+
);
|
|
288
|
+
db.close();
|
|
289
|
+
|
|
290
|
+
expect(rows).toHaveLength(3);
|
|
291
|
+
const rowIds = rows.map((r) => r['id']).sort();
|
|
292
|
+
const responseIds = responses.map((r) => r.id).sort();
|
|
293
|
+
expect(rowIds).toEqual(responseIds);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('persists rows that survive a repository close and reopen', () => {
|
|
297
|
+
const firstRepo = new SqliteClaudeMessageResponseRepository(dbPath);
|
|
298
|
+
const response = buildTestResponse();
|
|
299
|
+
firstRepo.append(response);
|
|
300
|
+
|
|
301
|
+
const secondRepo = new SqliteClaudeMessageResponseRepository(dbPath);
|
|
302
|
+
|
|
303
|
+
const db = new Database(dbPath);
|
|
304
|
+
const countRow = requireRecord(
|
|
305
|
+
db.prepare('SELECT COUNT(*) as cnt FROM claude_message_response').get(),
|
|
306
|
+
);
|
|
307
|
+
db.close();
|
|
308
|
+
|
|
309
|
+
expect(countRow['cnt']).toBe(1);
|
|
310
|
+
expect(secondRepo).toBeDefined();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { ClaudeMessageResponse } from '../../domain/entities/ClaudeMessageResponse';
|
|
6
|
+
import { ClaudeMessageResponseRepository } from '../../domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository';
|
|
7
|
+
|
|
8
|
+
const ULID_ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
9
|
+
|
|
10
|
+
const encodeUlidTime = (ms: number): string => {
|
|
11
|
+
let str = '';
|
|
12
|
+
for (let i = 9; i >= 0; i--) {
|
|
13
|
+
str = ULID_ENCODING[ms % 32] + str;
|
|
14
|
+
ms = Math.floor(ms / 32);
|
|
15
|
+
}
|
|
16
|
+
return str;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const encodeUlidRandom = (): string => {
|
|
20
|
+
const bytes = crypto.randomBytes(10);
|
|
21
|
+
let bits = 0;
|
|
22
|
+
let bitLen = 0;
|
|
23
|
+
let str = '';
|
|
24
|
+
for (let i = 0; i < bytes.length && str.length < 16; i++) {
|
|
25
|
+
bits = (bits << 8) | bytes[i];
|
|
26
|
+
bitLen += 8;
|
|
27
|
+
while (bitLen >= 5 && str.length < 16) {
|
|
28
|
+
str += ULID_ENCODING[(bits >>> (bitLen - 5)) & 0x1f];
|
|
29
|
+
bitLen -= 5;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return str;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const generateUlid = (): string =>
|
|
36
|
+
encodeUlidTime(Date.now()) + encodeUlidRandom();
|
|
37
|
+
|
|
38
|
+
const CREATE_TABLE_SQL = `
|
|
39
|
+
CREATE TABLE IF NOT EXISTS claude_message_response (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
observed_at INTEGER NOT NULL,
|
|
42
|
+
token_name TEXT NOT NULL,
|
|
43
|
+
external_claude_message_id TEXT,
|
|
44
|
+
external_claude_request_id TEXT,
|
|
45
|
+
http_status INTEGER NOT NULL,
|
|
46
|
+
model TEXT,
|
|
47
|
+
role TEXT,
|
|
48
|
+
stop_reason TEXT,
|
|
49
|
+
stop_sequence TEXT,
|
|
50
|
+
input_tokens INTEGER,
|
|
51
|
+
output_tokens INTEGER,
|
|
52
|
+
cache_creation_input_tokens INTEGER,
|
|
53
|
+
cache_read_input_tokens INTEGER,
|
|
54
|
+
ephemeral_5m_input_tokens INTEGER,
|
|
55
|
+
ephemeral_1h_input_tokens INTEGER,
|
|
56
|
+
service_tier TEXT,
|
|
57
|
+
inference_geo TEXT,
|
|
58
|
+
error_type TEXT,
|
|
59
|
+
error_message TEXT,
|
|
60
|
+
anthropic_ratelimit_unified_status TEXT,
|
|
61
|
+
anthropic_ratelimit_unified_5h_status TEXT,
|
|
62
|
+
anthropic_ratelimit_unified_5h_utilization REAL,
|
|
63
|
+
anthropic_ratelimit_unified_5h_reset REAL,
|
|
64
|
+
anthropic_ratelimit_unified_7d_status TEXT,
|
|
65
|
+
anthropic_ratelimit_unified_7d_utilization REAL,
|
|
66
|
+
anthropic_ratelimit_unified_7d_reset REAL,
|
|
67
|
+
retry_after REAL,
|
|
68
|
+
anthropic_organization_id TEXT
|
|
69
|
+
) STRICT
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const INSERT_SQL = `
|
|
73
|
+
INSERT INTO claude_message_response (
|
|
74
|
+
id, observed_at, token_name,
|
|
75
|
+
external_claude_message_id, external_claude_request_id,
|
|
76
|
+
http_status, model, role, stop_reason, stop_sequence,
|
|
77
|
+
input_tokens, output_tokens,
|
|
78
|
+
cache_creation_input_tokens, cache_read_input_tokens,
|
|
79
|
+
ephemeral_5m_input_tokens, ephemeral_1h_input_tokens,
|
|
80
|
+
service_tier, inference_geo,
|
|
81
|
+
error_type, error_message,
|
|
82
|
+
anthropic_ratelimit_unified_status,
|
|
83
|
+
anthropic_ratelimit_unified_5h_status,
|
|
84
|
+
anthropic_ratelimit_unified_5h_utilization,
|
|
85
|
+
anthropic_ratelimit_unified_5h_reset,
|
|
86
|
+
anthropic_ratelimit_unified_7d_status,
|
|
87
|
+
anthropic_ratelimit_unified_7d_utilization,
|
|
88
|
+
anthropic_ratelimit_unified_7d_reset,
|
|
89
|
+
retry_after, anthropic_organization_id
|
|
90
|
+
) VALUES (
|
|
91
|
+
@id, @observedAt, @tokenName,
|
|
92
|
+
@externalClaudeMessageId, @externalClaudeRequestId,
|
|
93
|
+
@httpStatus, @model, @role, @stopReason, @stopSequence,
|
|
94
|
+
@inputTokens, @outputTokens,
|
|
95
|
+
@cacheCreationInputTokens, @cacheReadInputTokens,
|
|
96
|
+
@ephemeral5mInputTokens, @ephemeral1hInputTokens,
|
|
97
|
+
@serviceTier, @inferenceGeo,
|
|
98
|
+
@errorType, @errorMessage,
|
|
99
|
+
@anthropicRatelimitUnifiedStatus,
|
|
100
|
+
@anthropicRatelimitUnified5hStatus,
|
|
101
|
+
@anthropicRatelimitUnified5hUtilization,
|
|
102
|
+
@anthropicRatelimitUnified5hReset,
|
|
103
|
+
@anthropicRatelimitUnified7dStatus,
|
|
104
|
+
@anthropicRatelimitUnified7dUtilization,
|
|
105
|
+
@anthropicRatelimitUnified7dReset,
|
|
106
|
+
@retryAfter, @anthropicOrganizationId
|
|
107
|
+
)
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
export class SqliteClaudeMessageResponseRepository implements ClaudeMessageResponseRepository {
|
|
111
|
+
private readonly db: Database.Database;
|
|
112
|
+
private readonly insert: Database.Statement;
|
|
113
|
+
|
|
114
|
+
constructor(dbPath: string) {
|
|
115
|
+
const dir = path.dirname(dbPath);
|
|
116
|
+
if (!fs.existsSync(dir)) {
|
|
117
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
this.db = new Database(dbPath);
|
|
120
|
+
this.db.pragma('journal_mode = WAL');
|
|
121
|
+
this.db.exec(CREATE_TABLE_SQL);
|
|
122
|
+
this.insert = this.db.prepare(INSERT_SQL);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
append = (response: ClaudeMessageResponse): void => {
|
|
126
|
+
this.insert.run({
|
|
127
|
+
id: response.id,
|
|
128
|
+
observedAt: response.observedAt.getTime(),
|
|
129
|
+
tokenName: response.tokenName,
|
|
130
|
+
externalClaudeMessageId: response.externalClaudeMessageId,
|
|
131
|
+
externalClaudeRequestId: response.externalClaudeRequestId,
|
|
132
|
+
httpStatus: response.httpStatus,
|
|
133
|
+
model: response.model,
|
|
134
|
+
role: response.role,
|
|
135
|
+
stopReason: response.stopReason,
|
|
136
|
+
stopSequence: response.stopSequence,
|
|
137
|
+
inputTokens: response.inputTokens,
|
|
138
|
+
outputTokens: response.outputTokens,
|
|
139
|
+
cacheCreationInputTokens: response.cacheCreationInputTokens,
|
|
140
|
+
cacheReadInputTokens: response.cacheReadInputTokens,
|
|
141
|
+
ephemeral5mInputTokens: response.ephemeral5mInputTokens,
|
|
142
|
+
ephemeral1hInputTokens: response.ephemeral1hInputTokens,
|
|
143
|
+
serviceTier: response.serviceTier,
|
|
144
|
+
inferenceGeo: response.inferenceGeo,
|
|
145
|
+
errorType: response.errorType,
|
|
146
|
+
errorMessage: response.errorMessage,
|
|
147
|
+
anthropicRatelimitUnifiedStatus: response.anthropicRatelimitUnifiedStatus,
|
|
148
|
+
anthropicRatelimitUnified5hStatus:
|
|
149
|
+
response.anthropicRatelimitUnified5hStatus,
|
|
150
|
+
anthropicRatelimitUnified5hUtilization:
|
|
151
|
+
response.anthropicRatelimitUnified5hUtilization,
|
|
152
|
+
anthropicRatelimitUnified5hReset:
|
|
153
|
+
response.anthropicRatelimitUnified5hReset,
|
|
154
|
+
anthropicRatelimitUnified7dStatus:
|
|
155
|
+
response.anthropicRatelimitUnified7dStatus,
|
|
156
|
+
anthropicRatelimitUnified7dUtilization:
|
|
157
|
+
response.anthropicRatelimitUnified7dUtilization,
|
|
158
|
+
anthropicRatelimitUnified7dReset:
|
|
159
|
+
response.anthropicRatelimitUnified7dReset,
|
|
160
|
+
retryAfter: response.retryAfter,
|
|
161
|
+
anthropicOrganizationId: response.anthropicOrganizationId,
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ClaudeMessageResponse = {
|
|
2
|
+
id: string;
|
|
3
|
+
observedAt: Date;
|
|
4
|
+
tokenName: string;
|
|
5
|
+
externalClaudeMessageId: string | null;
|
|
6
|
+
externalClaudeRequestId: string | null;
|
|
7
|
+
httpStatus: number;
|
|
8
|
+
model: string | null;
|
|
9
|
+
role: string | null;
|
|
10
|
+
stopReason: string | null;
|
|
11
|
+
stopSequence: string | null;
|
|
12
|
+
inputTokens: number | null;
|
|
13
|
+
outputTokens: number | null;
|
|
14
|
+
cacheCreationInputTokens: number | null;
|
|
15
|
+
cacheReadInputTokens: number | null;
|
|
16
|
+
ephemeral5mInputTokens: number | null;
|
|
17
|
+
ephemeral1hInputTokens: number | null;
|
|
18
|
+
serviceTier: string | null;
|
|
19
|
+
inferenceGeo: string | null;
|
|
20
|
+
errorType: string | null;
|
|
21
|
+
errorMessage: string | null;
|
|
22
|
+
anthropicRatelimitUnifiedStatus: string | null;
|
|
23
|
+
anthropicRatelimitUnified5hStatus: string | null;
|
|
24
|
+
anthropicRatelimitUnified5hUtilization: number | null;
|
|
25
|
+
anthropicRatelimitUnified5hReset: number | null;
|
|
26
|
+
anthropicRatelimitUnified7dStatus: string | null;
|
|
27
|
+
anthropicRatelimitUnified7dUtilization: number | null;
|
|
28
|
+
anthropicRatelimitUnified7dReset: number | null;
|
|
29
|
+
retryAfter: number | null;
|
|
30
|
+
anthropicOrganizationId: string | null;
|
|
31
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { ClaudeMessageResponse } from '../../domain/entities/ClaudeMessageResponse';
|
|
3
|
+
export declare const parseClaudeMessageResponse: (tokenName: string, httpStatus: number, headers: http.IncomingHttpHeaders, body: string) => ClaudeMessageResponse;
|
|
4
|
+
//# sourceMappingURL=ClaudeMessageResponseParser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaudeMessageResponseParser.d.ts","sourceRoot":"","sources":["../../../src/adapter/proxy/ClaudeMessageResponseParser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AAiFpF,eAAO,MAAM,0BAA0B,GACrC,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,SAAS,IAAI,CAAC,mBAAmB,EACjC,MAAM,MAAM,KACX,qBA4FF,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { ClaudeMessageResponseRepository } from '../../domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository';
|
|
1
2
|
declare const extractToken: (authorization: string | string[] | undefined) => string | null;
|
|
2
|
-
declare const startProxy: (port: number) => void;
|
|
3
|
+
declare const startProxy: (port: number, claudeMessageResponseRepository?: ClaudeMessageResponseRepository | null) => void;
|
|
3
4
|
export { startProxy, extractToken };
|
|
4
5
|
//# sourceMappingURL=proxyEntry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxyEntry.d.ts","sourceRoot":"","sources":["../../../src/adapter/proxy/proxyEntry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"proxyEntry.d.ts","sourceRoot":"","sources":["../../../src/adapter/proxy/proxyEntry.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,+BAA+B,EAAE,MAAM,0EAA0E,CAAC;AAU3H,QAAA,MAAM,YAAY,GAChB,eAAe,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,KAC3C,MAAM,GAAG,IAQX,CAAC;AAEF,QAAA,MAAM,UAAU,GACd,MAAM,MAAM,EACZ,kCAAiC,+BAA+B,GAAG,IAAW,KAC7E,IA0EF,CAAC;AAQF,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ClaudeMessageResponse } from '../../domain/entities/ClaudeMessageResponse';
|
|
2
|
+
import { ClaudeMessageResponseRepository } from '../../domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository';
|
|
3
|
+
export declare const generateUlid: () => string;
|
|
4
|
+
export declare class SqliteClaudeMessageResponseRepository implements ClaudeMessageResponseRepository {
|
|
5
|
+
private readonly db;
|
|
6
|
+
private readonly insert;
|
|
7
|
+
constructor(dbPath: string);
|
|
8
|
+
append: (response: ClaudeMessageResponse) => void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=SqliteClaudeMessageResponseRepository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SqliteClaudeMessageResponseRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/SqliteClaudeMessageResponseRepository.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AACpF,OAAO,EAAE,+BAA+B,EAAE,MAAM,0EAA0E,CAAC;AA6B3H,eAAO,MAAM,YAAY,QAAO,MACiB,CAAC;AA0ElD,qBAAa,qCAAsC,YAAW,+BAA+B;IAC3F,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;gBAEhC,MAAM,EAAE,MAAM;IAW1B,MAAM,GAAI,UAAU,qBAAqB,KAAG,IAAI,CAsC9C;CACH"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ClaudeMessageResponse = {
|
|
2
|
+
id: string;
|
|
3
|
+
observedAt: Date;
|
|
4
|
+
tokenName: string;
|
|
5
|
+
externalClaudeMessageId: string | null;
|
|
6
|
+
externalClaudeRequestId: string | null;
|
|
7
|
+
httpStatus: number;
|
|
8
|
+
model: string | null;
|
|
9
|
+
role: string | null;
|
|
10
|
+
stopReason: string | null;
|
|
11
|
+
stopSequence: string | null;
|
|
12
|
+
inputTokens: number | null;
|
|
13
|
+
outputTokens: number | null;
|
|
14
|
+
cacheCreationInputTokens: number | null;
|
|
15
|
+
cacheReadInputTokens: number | null;
|
|
16
|
+
ephemeral5mInputTokens: number | null;
|
|
17
|
+
ephemeral1hInputTokens: number | null;
|
|
18
|
+
serviceTier: string | null;
|
|
19
|
+
inferenceGeo: string | null;
|
|
20
|
+
errorType: string | null;
|
|
21
|
+
errorMessage: string | null;
|
|
22
|
+
anthropicRatelimitUnifiedStatus: string | null;
|
|
23
|
+
anthropicRatelimitUnified5hStatus: string | null;
|
|
24
|
+
anthropicRatelimitUnified5hUtilization: number | null;
|
|
25
|
+
anthropicRatelimitUnified5hReset: number | null;
|
|
26
|
+
anthropicRatelimitUnified7dStatus: string | null;
|
|
27
|
+
anthropicRatelimitUnified7dUtilization: number | null;
|
|
28
|
+
anthropicRatelimitUnified7dReset: number | null;
|
|
29
|
+
retryAfter: number | null;
|
|
30
|
+
anthropicOrganizationId: string | null;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=ClaudeMessageResponse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaudeMessageResponse.d.ts","sourceRoot":"","sources":["../../../src/domain/entities/ClaudeMessageResponse.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,IAAI,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,+BAA+B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,iCAAiC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjD,sCAAsC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtD,gCAAgC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,iCAAiC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjD,sCAAsC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtD,gCAAgC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaudeMessageResponseRepository.d.ts","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ClaudeMessageResponseRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAE7E,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,IAAI,CAAC;CACnD"}
|