Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions extensions/levelcode-ai/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ async function runAgent(ctx) {
const perTurnMax = Math.max(1024, Math.min(32768, ctx.maxTokens || 8192));
const OUTPUT_TOKEN_BUDGET = perTurnMax * 8;
let cumulativeOutputTokens = 0;
// [LevelCode] Gateway turns report real money: this run's cost + the wallet balance, both RETAIL
// micro-$ (what the customer paid). BYOK turns never report them and these stay 0/null.
let runCostMicros = 0;

// --- M5 auto-verify loop ------------------------------------------------
// When the model wants to finish AND it edited files this run, check its work before handing back:
Expand Down Expand Up @@ -578,7 +581,11 @@ async function runAgent(ctx) {
// Report real context usage (input_tokens = how full the transcript is) so the UI can meter + warn.
if (turn.usage) {
cumulativeOutputTokens += (turn.usage.output_tokens || 0);
dbg('usage', { input: turn.usage.input_tokens, output: turn.usage.output_tokens, cacheRead: turn.usage.cache_read_input_tokens, cumulativeOutput: cumulativeOutputTokens });
// [LevelCode] The Cloud gateway's credits frame (retail micro-$): accumulate what the RUN
// cost and keep the latest remaining balance for the response bar.
if (turn.usage.cost_micros != null) { runCostMicros += turn.usage.cost_micros; }
if (turn.usage.credits_remaining_micros != null) { ctx.credits = turn.usage.credits_remaining_micros; }
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 });
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 });
}

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

Expand Down
19 changes: 15 additions & 4 deletions extensions/levelcode-ai/media/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -1863,8 +1863,16 @@
function modelLabel(){
try { const t = (document.getElementById('model').textContent || 'Auto').replace('▾', '').trim(); return t || 'Auto'; } catch(e){ return 'Auto'; }
}
// Response action bar: (Retry) (Copy) (Helpful) (Unhelpful) … model · credits.
function addAgentDone(reason, edits, credits, maxSteps){
// [LevelCode] Retail micro-$ → a human figure. Sub-cent turns are common once prompt caching kicks
// in, and "$0.00" would read as free/broken — so floor them at "<$0.01" instead of rounding away.
function money(micros){
const d = Number(micros) / 1e6;
if (!isFinite(d) || d <= 0) { return '$0.00'; }
return d < 0.01 ? '<$0.01' : '$' + d.toFixed(2);
}

// Response action bar: (Retry) (Copy) (Helpful) (Unhelpful) … model · $cost · $left.
function addAgentDone(reason, edits, credits, maxSteps, costMicros){
const bar = document.createElement('div'); bar.className = 'agentbar';
// Stamp the model that produced THIS turn, so a later reaction is attributed to it
// (not whatever the plan is entitled to at click time — models can change mid-session).
Expand All @@ -1880,7 +1888,10 @@
const meta = document.createElement('div'); meta.className = 'agentbar-meta';
meta.innerHTML = (hint ? '<span class="abhint">' + esc(hint) + '</span>' : '')
+ '<span class="abmodel" title="Click to change model">' + esc(modelLabel()) + '</span>'
+ (credits != null ? '<span class="abcredits">· ' + esc(String(credits)) + ' credit' + (Number(credits) === 1 ? '' : 's') + '</span>' : '');
// Gateway runs report real money (retail micro-$): what this run cost + what's left. BYOK runs
// report neither, so the bar stays exactly as it was.
+ (costMicros != null ? '<span class="abcredits" title="What this run cost">· ' + esc(money(costMicros)) + '</span>' : '')
+ (credits != null ? '<span class="abcredits" title="Credits remaining on your plan">· ' + esc(money(credits)) + ' left</span>' : '');
bar.append(actions, meta);
log.appendChild(bar); scrollIfStuck();

Expand Down Expand Up @@ -2288,7 +2299,7 @@
else if (m.type === 'account'){ renderAccount(m); if (m.open) openAccount(); }
else if (m.type === 'fileIndex'){ setFileIndex(m.files || []); }
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>'); } }
else if (m.type === 'agentDone'){ clearStatus(); finishAgentBubble(); addAgentDone(m.reason, m.edits, m.credits, m.maxSteps); setStreaming(false); }
else if (m.type === 'agentDone'){ clearStatus(); finishAgentBubble(); addAgentDone(m.reason, m.edits, m.credits, m.maxSteps, m.costMicros); setStreaming(false); }
else if (m.type === 'context'){ selLabel = m.label; renderChips(); }
else if (m.type === 'clearContext'){ selLabel = null; renderChips(); }
else if (m.type === 'reset'){
Expand Down
8 changes: 8 additions & 0 deletions extensions/levelcode-ai/providers/openaiCompat.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,14 @@ async function streamOpenAIAgentTurn(opts) {
if (cached) { usage.cache_read_input_tokens = cached; }
if (ev.usage.completion_tokens) { usage.output_tokens = ev.usage.completion_tokens; }
}
// [LevelCode] The Cloud gateway's final credits frame, emitted just before [DONE]: what THIS turn
// cost and what's left, in retail micro-$ (the same basis as GET /account/models). Namespaced and
// choice-less, so every other OpenAI-shaped provider simply never sends it and this stays inert.
if (ev.levelcode) {
if (ev.levelcode.cost_micros != null) { usage.cost_micros = ev.levelcode.cost_micros; }
if (ev.levelcode.credits_remaining_micros != null) { usage.credits_remaining_micros = ev.levelcode.credits_remaining_micros; }
return;
}
const c = ev.choices && ev.choices[0];
if (!c) { return; }
const d = c.delta || {};
Expand Down