MCPcopy Create free account
hub / github.com/aattaran/deepclaude / UsageNormalizer

Class UsageNormalizer

proxy/model-proxy.js:40–91  ·  view source on GitHub ↗

* Transform stream that intercepts SSE events and injects missing `usage` * fields. DeepSeek/OpenRouter may omit `usage` in message_start or * message_delta, which crashes Claude Code ("$.input_tokens" is undefined).

Source from the content-addressed store, hash-verified

38 * message_delta, which crashes Claude Code ("$.input_tokens" is undefined).
39 */
40class UsageNormalizer extends Transform {
41 constructor(onUsage) {
42 super();
43 this._buf = '';
44 this._onUsage = onUsage;
45 this._inputTokens = 0;
46 this._outputTokens = 0;
47 }
48
49 _transform(chunk, _enc, cb) {
50 this._buf += chunk.toString();
51 const parts = this._buf.split('\n\n');
52 this._buf = parts.pop();
53 for (const part of parts) {
54 this.push(this._fix(part) + '\n\n');
55 }
56 cb();
57 }
58
59 _fix(event) {
60 const m = event.match(/^data: (.+)$/m);
61 if (!m) return event;
62 try {
63 const d = JSON.parse(m[1]);
64 let changed = false;
65 if (d.type === 'message_start' && d.message) {
66 if (d.message.usage) {
67 this._inputTokens = d.message.usage.input_tokens || 0;
68 } else {
69 d.message.usage = { input_tokens: 0, output_tokens: 0 };
70 changed = true;
71 }
72 }
73 if (d.type === 'message_delta') {
74 if (d.usage) {
75 this._outputTokens = d.usage.output_tokens || 0;
76 } else {
77 d.usage = { output_tokens: 0 };
78 changed = true;
79 }
80 }
81 if (changed) return event.replace(m[1], () => JSON.stringify(d));
82 } catch { /* not JSON, pass through */ }
83 return event;
84 }
85
86 _flush(cb) {
87 if (this._buf.trim()) this.push(this._fix(this._buf) + '\n\n');
88 if (this._onUsage) this._onUsage(this._inputTokens, this._outputTokens);
89 cb();
90 }
91}
92
93/**
94 * For non-streaming JSON responses, ensure `usage` exists.

Callers

nothing calls this directly

Calls

no outgoing calls

Tested by

no test coverage detected