Skip to content

Commit d2cf0a4

Browse files
authored
Refactor token usage record/metrics construction into shared token-persistence helpers (#3566)
* Initial plan * refactor token usage record and metrics helpers --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 2163cb8 commit d2cf0a4

4 files changed

Lines changed: 121 additions & 36 deletions

File tree

containers/api-proxy/token-persistence.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,54 @@ function validateTokenUsageRecord(record) {
137137
return true;
138138
}
139139

140+
/**
141+
* Build a token usage record for JSONL persistence.
142+
*
143+
* @param {object} normalized - Normalized usage object from normalizeUsage()
144+
* @param {object} opts
145+
* @param {string} opts.requestId
146+
* @param {string} opts.provider
147+
* @param {string|null} opts.model
148+
* @param {string} opts.reqPath
149+
* @param {number} opts.status
150+
* @param {boolean} opts.streaming
151+
* @param {number} opts.duration
152+
* @param {number} opts.responseBytes
153+
* @returns {object}
154+
*/
155+
function buildTokenUsageRecord(normalized, opts) {
156+
const { requestId, provider, model, reqPath, status, streaming, duration, responseBytes } = opts;
157+
return {
158+
_schema: TOKEN_USAGE_SCHEMA,
159+
timestamp: new Date().toISOString(),
160+
request_id: requestId,
161+
provider,
162+
model: model || 'unknown',
163+
path: reqPath,
164+
status,
165+
streaming,
166+
input_tokens: normalized.input_tokens,
167+
output_tokens: normalized.output_tokens,
168+
cache_read_tokens: normalized.cache_read_tokens,
169+
cache_write_tokens: normalized.cache_write_tokens,
170+
duration_ms: duration,
171+
response_bytes: responseBytes,
172+
};
173+
}
174+
175+
/**
176+
* Increment token usage metrics when a metrics sink is available.
177+
*
178+
* @param {object|null} metricsRef
179+
* @param {string} provider
180+
* @param {object} normalized
181+
*/
182+
function incrementTokenMetrics(metricsRef, provider, normalized) {
183+
if (!metricsRef) return;
184+
metricsRef.increment('input_tokens_total', { provider }, normalized.input_tokens);
185+
metricsRef.increment('output_tokens_total', { provider }, normalized.output_tokens);
186+
}
187+
140188
/**
141189
* Write a token usage record to the JSONL log file.
142190
* Validates the record against the token-usage schema before writing.
@@ -181,6 +229,8 @@ module.exports = {
181229
TOKEN_LOG_FILE,
182230
TOKEN_USAGE_SCHEMA,
183231
diag,
232+
buildTokenUsageRecord,
233+
incrementTokenMetrics,
184234
validateTokenUsageRecord,
185235
writeTokenUsage,
186236
closeLogStream,

containers/api-proxy/token-tracker-http.js

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ const {
2828
extractUsageFromJson,
2929
normalizeUsage,
3030
} = require('./token-parsers');
31-
const { writeTokenUsage, TOKEN_USAGE_SCHEMA, diag } = require('./token-persistence');
31+
const {
32+
writeTokenUsage,
33+
buildTokenUsageRecord,
34+
incrementTokenMetrics,
35+
diag,
36+
} = require('./token-persistence');
3237

3338
// Max response body to buffer for non-streaming usage extraction (5 MB).
3439
// Responses larger than this are still forwarded but usage is not extracted.
@@ -238,28 +243,19 @@ function trackTokenUsage(proxyRes, opts) {
238243
}
239244

240245
// Update metrics
241-
if (metricsRef) {
242-
metricsRef.increment('input_tokens_total', { provider }, normalized.input_tokens);
243-
metricsRef.increment('output_tokens_total', { provider }, normalized.output_tokens);
244-
}
246+
incrementTokenMetrics(metricsRef, provider, normalized);
245247

246248
// Build log record
247-
const record = {
248-
_schema: TOKEN_USAGE_SCHEMA,
249-
timestamp: new Date().toISOString(),
250-
request_id: requestId,
249+
const record = buildTokenUsageRecord(normalized, {
250+
requestId,
251251
provider,
252-
model: model || 'unknown',
253-
path: reqPath,
252+
model,
253+
reqPath,
254254
status: proxyRes.statusCode,
255255
streaming,
256-
input_tokens: normalized.input_tokens,
257-
output_tokens: normalized.output_tokens,
258-
cache_read_tokens: normalized.cache_read_tokens,
259-
cache_write_tokens: normalized.cache_write_tokens,
260-
duration_ms: duration,
261-
response_bytes: totalBytes,
262-
};
256+
duration,
257+
responseBytes: totalBytes,
258+
});
263259

264260
// Include billing/quota info when available (Copilot PRU tracking)
265261
if (initiatorSent) record.x_initiator = initiatorSent;

containers/api-proxy/token-tracker-ws.js

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111

1212
const { logRequest } = require('./logging');
1313
const { extractUsageFromSseLine, normalizeUsage } = require('./token-parsers');
14-
const { writeTokenUsage, TOKEN_USAGE_SCHEMA, diag } = require('./token-persistence');
14+
const {
15+
writeTokenUsage,
16+
buildTokenUsageRecord,
17+
incrementTokenMetrics,
18+
diag,
19+
} = require('./token-persistence');
1520

1621
/**
1722
* Parse WebSocket frames from a buffer (server→client direction, unmasked).
@@ -198,27 +203,18 @@ function trackWebSocketTokenUsage(upstreamSocket, opts) {
198203
}
199204
}
200205

201-
if (metricsRef) {
202-
metricsRef.increment('input_tokens_total', { provider }, normalized.input_tokens);
203-
metricsRef.increment('output_tokens_total', { provider }, normalized.output_tokens);
204-
}
206+
incrementTokenMetrics(metricsRef, provider, normalized);
205207

206-
const record = {
207-
_schema: TOKEN_USAGE_SCHEMA,
208-
timestamp: new Date().toISOString(),
209-
request_id: requestId,
208+
const record = buildTokenUsageRecord(normalized, {
209+
requestId,
210210
provider,
211-
model: streamingModel || 'unknown',
212-
path: reqPath,
211+
model: streamingModel,
212+
reqPath,
213213
status: 101,
214214
streaming: true,
215-
input_tokens: normalized.input_tokens,
216-
output_tokens: normalized.output_tokens,
217-
cache_read_tokens: normalized.cache_read_tokens,
218-
cache_write_tokens: normalized.cache_write_tokens,
219-
duration_ms: duration,
220-
response_bytes: totalBytes - headerBytes,
221-
};
215+
duration,
216+
responseBytes: totalBytes - headerBytes,
217+
});
222218

223219
writeTokenUsage(record);
224220

containers/api-proxy/token-tracker.schema.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
writeTokenUsage,
1414
closeLogStream,
1515
} = require('./token-tracker');
16+
const { buildTokenUsageRecord, incrementTokenMetrics } = require('./token-persistence');
1617
const { EventEmitter } = require('events');
1718

1819
afterAll(async () => {
@@ -96,6 +97,48 @@ describe('validateTokenUsageRecord', () => {
9697
});
9798
});
9899

100+
describe('shared token usage helpers', () => {
101+
test('buildTokenUsageRecord returns schema-compatible record shape', () => {
102+
const record = buildTokenUsageRecord({
103+
input_tokens: 10,
104+
output_tokens: 5,
105+
cache_read_tokens: 2,
106+
cache_write_tokens: 1,
107+
}, {
108+
requestId: 'helper-record-test',
109+
provider: 'openai',
110+
model: null,
111+
reqPath: '/v1/chat/completions',
112+
status: 200,
113+
streaming: false,
114+
duration: 123,
115+
responseBytes: 456,
116+
});
117+
118+
expect(record).toMatchObject({
119+
request_id: 'helper-record-test',
120+
provider: 'openai',
121+
model: 'unknown',
122+
path: '/v1/chat/completions',
123+
status: 200,
124+
streaming: false,
125+
input_tokens: 10,
126+
output_tokens: 5,
127+
cache_read_tokens: 2,
128+
cache_write_tokens: 1,
129+
duration_ms: 123,
130+
response_bytes: 456,
131+
});
132+
expect(validateTokenUsageRecord(record)).toBe(true);
133+
});
134+
135+
test('incrementTokenMetrics is a no-op when metrics sink is missing', () => {
136+
expect(() => {
137+
incrementTokenMetrics(null, 'anthropic', { input_tokens: 1, output_tokens: 2 });
138+
}).not.toThrow();
139+
});
140+
});
141+
99142
// ── JSONL records include _schema field ───────────────────────────────
100143

101144
/**

0 commit comments

Comments
 (0)