Skip to content

Commit ba4f454

Browse files
authored
Merge pull request #17 from levelcodeai/feat/editor-running-spend
feat(ai): show running $ spend — per-run cost + credits left in the response bar
2 parents 7dc2f10 + 27d5682 commit ba4f454

3 files changed

Lines changed: 39 additions & 8 deletions

File tree

extensions/levelcode-ai/agent.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,9 @@ async function runAgent(ctx) {
474474
const perTurnMax = Math.max(1024, Math.min(32768, ctx.maxTokens || 8192));
475475
const OUTPUT_TOKEN_BUDGET = perTurnMax * 8;
476476
let cumulativeOutputTokens = 0;
477+
// [LevelCode] Gateway turns report real money: this run's cost + the wallet balance, both RETAIL
478+
// micro-$ (what the customer paid). BYOK turns never report them and these stay 0/null.
479+
let runCostMicros = 0;
477480

478481
// --- M5 auto-verify loop ------------------------------------------------
479482
// When the model wants to finish AND it edited files this run, check its work before handing back:
@@ -578,7 +581,11 @@ async function runAgent(ctx) {
578581
// Report real context usage (input_tokens = how full the transcript is) so the UI can meter + warn.
579582
if (turn.usage) {
580583
cumulativeOutputTokens += (turn.usage.output_tokens || 0);
581-
dbg('usage', { input: turn.usage.input_tokens, output: turn.usage.output_tokens, cacheRead: turn.usage.cache_read_input_tokens, cumulativeOutput: cumulativeOutputTokens });
584+
// [LevelCode] The Cloud gateway's credits frame (retail micro-$): accumulate what the RUN
585+
// cost and keep the latest remaining balance for the response bar.
586+
if (turn.usage.cost_micros != null) { runCostMicros += turn.usage.cost_micros; }
587+
if (turn.usage.credits_remaining_micros != null) { ctx.credits = turn.usage.credits_remaining_micros; }
588+
dbg('usage', { input: turn.usage.input_tokens, output: turn.usage.output_tokens, cacheRead: turn.usage.cache_read_input_tokens, cumulativeOutput: cumulativeOutputTokens, costMicros: turn.usage.cost_micros, creditsLeftMicros: turn.usage.credits_remaining_micros });
582589
ctx.post({ type: 'contextUsage', input: (turn.usage.input_tokens || 0) + (turn.usage.cache_read_input_tokens || 0) + (turn.usage.cache_creation_input_tokens || 0), output: turn.usage.output_tokens || 0, limit: ctx.contextLimit || 200000, model: ctx.model, system: systemTokensEst, tools: TOOLS_TOKENS_EST });
583590
}
584591

@@ -695,9 +702,14 @@ async function runAgent(ctx) {
695702
}
696703
else { ctx.post({ type: 'agentError', message: msg, code }); reason = 'error'; }
697704
} finally {
698-
dbg('agent.done', { reason, steps: step - 1, edits: ctx.editCount || 0, credits: ctx.credits != null ? ctx.credits : null });
699-
// credits: null until the M10 gateway returns real usage; the response bar shows it when present.
700-
ctx.post({ type: 'agentDone', reason, edits: ctx.editCount || 0, credits: ctx.credits != null ? ctx.credits : null, maxSteps: ctx.maxSteps });
705+
dbg('agent.done', { reason, steps: step - 1, edits: ctx.editCount || 0, costMicros: runCostMicros, creditsLeftMicros: ctx.credits != null ? ctx.credits : null });
706+
// [LevelCode] Gateway runs now carry real money: costMicros = what THIS run cost, credits = the
707+
// remaining balance — both RETAIL micro-$. BYOK runs send neither (null/0) and the bar omits them.
708+
ctx.post({
709+
type: 'agentDone', reason, edits: ctx.editCount || 0, maxSteps: ctx.maxSteps,
710+
costMicros: runCostMicros > 0 ? runCostMicros : null,
711+
credits: ctx.credits != null ? ctx.credits : null
712+
});
701713
}
702714
}
703715

extensions/levelcode-ai/media/chat.html

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,8 +1863,16 @@
18631863
function modelLabel(){
18641864
try { const t = (document.getElementById('model').textContent || 'Auto').replace('▾', '').trim(); return t || 'Auto'; } catch(e){ return 'Auto'; }
18651865
}
1866-
// Response action bar: (Retry) (Copy) (Helpful) (Unhelpful) … model · credits.
1867-
function addAgentDone(reason, edits, credits, maxSteps){
1866+
// [LevelCode] Retail micro-$ → a human figure. Sub-cent turns are common once prompt caching kicks
1867+
// in, and "$0.00" would read as free/broken — so floor them at "<$0.01" instead of rounding away.
1868+
function money(micros){
1869+
const d = Number(micros) / 1e6;
1870+
if (!isFinite(d) || d <= 0) { return '$0.00'; }
1871+
return d < 0.01 ? '<$0.01' : '$' + d.toFixed(2);
1872+
}
1873+
1874+
// Response action bar: (Retry) (Copy) (Helpful) (Unhelpful) … model · $cost · $left.
1875+
function addAgentDone(reason, edits, credits, maxSteps, costMicros){
18681876
const bar = document.createElement('div'); bar.className = 'agentbar';
18691877
// Stamp the model that produced THIS turn, so a later reaction is attributed to it
18701878
// (not whatever the plan is entitled to at click time — models can change mid-session).
@@ -1880,7 +1888,10 @@
18801888
const meta = document.createElement('div'); meta.className = 'agentbar-meta';
18811889
meta.innerHTML = (hint ? '<span class="abhint">' + esc(hint) + '</span>' : '')
18821890
+ '<span class="abmodel" title="Click to change model">' + esc(modelLabel()) + '</span>'
1883-
+ (credits != null ? '<span class="abcredits">· ' + esc(String(credits)) + ' credit' + (Number(credits) === 1 ? '' : 's') + '</span>' : '');
1891+
// Gateway runs report real money (retail micro-$): what this run cost + what's left. BYOK runs
1892+
// report neither, so the bar stays exactly as it was.
1893+
+ (costMicros != null ? '<span class="abcredits" title="What this run cost">· ' + esc(money(costMicros)) + '</span>' : '')
1894+
+ (credits != null ? '<span class="abcredits" title="Credits remaining on your plan">· ' + esc(money(credits)) + ' left</span>' : '');
18841895
bar.append(actions, meta);
18851896
log.appendChild(bar); scrollIfStuck();
18861897

@@ -2288,7 +2299,7 @@
22882299
else if (m.type === 'account'){ renderAccount(m); if (m.open) openAccount(); }
22892300
else if (m.type === 'fileIndex'){ setFileIndex(m.files || []); }
22902301
else if (m.type === 'agentError'){ clearStatus(); finishAgentBubble(); const cap = capReachedInfo(m.message); if (cap){ addUpgradeCard(cap); } else if (isServiceIssue(m)){ addServiceCard(m); } else { add('assistant', '<span class="err">' + esc(m.message) + '</span>'); } }
2291-
else if (m.type === 'agentDone'){ clearStatus(); finishAgentBubble(); addAgentDone(m.reason, m.edits, m.credits, m.maxSteps); setStreaming(false); }
2302+
else if (m.type === 'agentDone'){ clearStatus(); finishAgentBubble(); addAgentDone(m.reason, m.edits, m.credits, m.maxSteps, m.costMicros); setStreaming(false); }
22922303
else if (m.type === 'context'){ selLabel = m.label; renderChips(); }
22932304
else if (m.type === 'clearContext'){ selLabel = null; renderChips(); }
22942305
else if (m.type === 'reset'){

extensions/levelcode-ai/providers/openaiCompat.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,14 @@ async function streamOpenAIAgentTurn(opts) {
207207
if (cached) { usage.cache_read_input_tokens = cached; }
208208
if (ev.usage.completion_tokens) { usage.output_tokens = ev.usage.completion_tokens; }
209209
}
210+
// [LevelCode] The Cloud gateway's final credits frame, emitted just before [DONE]: what THIS turn
211+
// cost and what's left, in retail micro-$ (the same basis as GET /account/models). Namespaced and
212+
// choice-less, so every other OpenAI-shaped provider simply never sends it and this stays inert.
213+
if (ev.levelcode) {
214+
if (ev.levelcode.cost_micros != null) { usage.cost_micros = ev.levelcode.cost_micros; }
215+
if (ev.levelcode.credits_remaining_micros != null) { usage.credits_remaining_micros = ev.levelcode.credits_remaining_micros; }
216+
return;
217+
}
210218
const c = ev.choices && ev.choices[0];
211219
if (!c) { return; }
212220
const d = c.delta || {};

0 commit comments

Comments
 (0)