diff options
Diffstat (limited to 'modules/core/module-billing.js')
| -rw-r--r-- | modules/core/module-billing.js | 33 |
1 files changed, 28 insertions, 5 deletions
diff --git a/modules/core/module-billing.js b/modules/core/module-billing.js index 87f8dc3..4ccf196 100644 --- a/modules/core/module-billing.js +++ b/modules/core/module-billing.js @@ -1,8 +1,10 @@ // Billing / pricing surface for newOS4 (2026-10-28) // // Why: the terminal could show a model's name but never what it costs. Prices and the anonymous -// message allowance are public data, so they need no login — the account BALANCE does, and newOS4 -// has no session layer, so that is reported honestly as "not signed in" rather than guessed. +// message allowance are public data, so they need no login — the account BALANCE does. +// 2026-11-03: newOS4 DOES have a session — the browser sends the HttpOnly cookie and +// parse_bearer_token() reads it — so the balance below is real when logged in. The old comment +// claimed otherwise and made the 401 look expected. // // Commands: cost [on|off], usage class BillingModule { @@ -56,7 +58,9 @@ class BillingModule { } async _loadBalance() { - // Only answers when a session exists; newOS4 currently has none, so this is expected to 401. + // Cookie auth is what makes this work: the browser sends webx11_session_token and + // parse_bearer_token() falls back to it. If this 401s, the user is logged out — say so, + // do not assume newOS4 cannot authenticate (that comment was wrong until 2026-11-03). try { const r = await fetch('/api/billing/subscription', { credentials: 'include' }); if (!r.ok) return null; @@ -67,6 +71,16 @@ class BillingModule { } } + async _loadUsage(days) { + try { + const r = await fetch('/api/billing/usage?days=' + (days || 1), { credentials: 'include' }); + if (!r.ok) return null; + return await r.json(); + } catch (e) { + return null; + } + } + // --- session spend, measured where the tokens actually are ---------------- _hookUsage() { const om = this.terminal && this.terminal.ollamaManager; @@ -109,8 +123,10 @@ class BillingModule { } } + // On by default since 2026-11-03: "the terminal shows tokens but not cost" was the default, + // not a bug — the flag was opt-in and nobody knew the command existed. `cost off` opts out. _costLineOn() { - try { return localStorage.getItem('webx11_cost_line') === '1'; } catch (e) { return false; } + try { return localStorage.getItem('webx11_cost_line') !== '0'; } catch (e) { return true; } } // --- the command ---------------------------------------------------------- @@ -138,13 +154,20 @@ class BillingModule { p('📊 Free allowance: unavailable (billing API did not answer)', 'warning'); } - p(`💰 Balance: ${bal && bal.balance_usd != null ? '$' + Number(bal.balance_usd).toFixed(2) : 'not signed in — newOS4 sends no session token, so a balance cannot be shown here yet'}`, 'info'); + p(`💰 Balance: ${bal && bal.balance_usd != null ? '$' + Number(bal.balance_usd).toFixed(2) : 'not signed in — log in at /spaces/dashboard.html to see your balance'}`, 'info'); if (bal && bal.plan_allowance) { const pa = bal.plan_allowance; p(`🎁 Included usage (${pa.plan}): $${Number(pa.used_usd).toFixed(2)} of $${Number(pa.limit_usd).toFixed(2)} ${pa.period === 'day' ? 'today' : 'this month'} — $${Number(pa.remaining_usd).toFixed(2)} left, then your balance pays`, 'info'); } + const u = await this._loadUsage(1); + if (u && u.totals) { + p(`📅 Today (all newOS4 sessions): ${u.totals.requests} call(s), ` + + `${((u.totals.prompt_tokens + u.totals.completion_tokens) / 1000).toFixed(1)}k tokens, ` + + `≈ $${Number(u.totals.retail_usd).toFixed(2)} retail`, 'info'); + } + if (s && s.msgs) { p(`🧮 This session: ${s.msgs} call(s), ${(s.prompt / 1000).toFixed(1)}k in / ${(s.completion / 1000).toFixed(1)}k out ≈ $${s.cost.toFixed(4)} retail`, 'info'); Object.entries(s.byModel) |