feat(billing): bill Cursor executor usage via stream-json token parsing
Memind CI / Test, build, and release guards (push) Failing after 14m36s
Memind CI / Test, build, and release guards (push) Failing after 14m36s
Parse Cursor CLI usage from stream-json output and charge through billSessionUsage so subscription quota is consumed first and wallet overage applies when needed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
function parseStreamJsonLine(line) {
|
||||
const text = String(line ?? '').trim();
|
||||
if (!text) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return parsed && typeof parsed === 'object' ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCursorAgentStreamJsonEvents(stdout) {
|
||||
const events = [];
|
||||
for (const line of String(stdout ?? '').split('\n')) {
|
||||
const parsed = parseStreamJsonLine(line);
|
||||
if (parsed) events.push(parsed);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
export function normalizeCursorAgentUsage(raw) {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const inputTokens = Number(raw.inputTokens ?? raw.input_tokens ?? 0);
|
||||
const outputTokens = Number(raw.outputTokens ?? raw.output_tokens ?? 0);
|
||||
const cacheReadTokens = Number(raw.cacheReadTokens ?? raw.cache_read_tokens ?? 0);
|
||||
const cacheWriteTokens = Number(raw.cacheWriteTokens ?? raw.cache_write_tokens ?? 0);
|
||||
const billableInput = inputTokens + cacheWriteTokens;
|
||||
const billableOutput = outputTokens;
|
||||
if (billableInput <= 0 && billableOutput <= 0) return null;
|
||||
return {
|
||||
inputTokens: billableInput,
|
||||
outputTokens: billableOutput,
|
||||
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : 0,
|
||||
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseCursorAgentUsage(stdout) {
|
||||
const events = parseCursorAgentStreamJsonEvents(stdout);
|
||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||
const event = events[index];
|
||||
if (event?.type !== 'result') continue;
|
||||
const usage = normalizeCursorAgentUsage(event.usage);
|
||||
if (usage) return usage;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractAssistantText(message) {
|
||||
if (!message || typeof message !== 'object') return '';
|
||||
const content = message.content ?? message.message?.content;
|
||||
if (typeof content === 'string') return content.trim();
|
||||
if (!Array.isArray(content)) return '';
|
||||
return content
|
||||
.map((part) => {
|
||||
if (!part || typeof part !== 'object') return '';
|
||||
if (part.type === 'text') return String(part.text ?? '').trim();
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function extractCursorAgentDisplayText(stdout) {
|
||||
const events = parseCursorAgentStreamJsonEvents(stdout);
|
||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||
const event = events[index];
|
||||
if (event?.type === 'result') {
|
||||
const resultText = String(event.result ?? '').trim();
|
||||
if (resultText) return resultText;
|
||||
}
|
||||
if (event?.type === 'assistant') {
|
||||
const assistantText = extractAssistantText(event.message ?? event);
|
||||
if (assistantText) return assistantText;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function buildCursorBillingTokenState(usage, priorBillingState = null) {
|
||||
const normalized = normalizeCursorAgentUsage(usage);
|
||||
if (!normalized) return null;
|
||||
const priorIn = Number(priorBillingState?.lastInputTokens ?? 0);
|
||||
const priorOut = Number(priorBillingState?.lastOutputTokens ?? 0);
|
||||
return {
|
||||
accumulatedInputTokens: priorIn + normalized.inputTokens,
|
||||
accumulatedOutputTokens: priorOut + normalized.outputTokens,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user