diff options
| author | Agando <roduccomm@gmail.com> | 2026-11-01 18:44:26 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-11-01 18:44:26 +0100 |
| commit | bdd9d1fe8861cbee78b28384713ba150c2baa309 (patch) | |
| tree | 23d76abc67bf7b93218c3b2b4e711b7293aadd64 /modules/core/core-bundle.js | |
| parent | 351d17c01e3e5a67e1bbd9c75671adfaaa820742 (diff) | |
feat(billing): no model-class gate; free allowance becomes $0.10/day
Operator policy 2026-11-01: every subscription may call any model, and the free tier resets daily.
Model gate removed (api/ai-gateway.py)
`plan == 'free' and model_weight > 1 -> 403 "Advanced models require Pro"` ran before any money
logic, so a free-plan account holding $1086 of purchased credit was refused and told to upgrade.
A model tier is not something we sell: the allowance and the USD balance bound spend. A caller who
cannot pay now gets the honest 429 (with period + needed amount). model_weight survives only for
track_usage() analytics.
Free allowance: $0.50/month -> $0.10/day
~$3/month retail = $2.14/month of provider cost at 1.4x markup, ~6x the previous giveaway and
still bounded per member (the 50-slot invite cap bounds the total; at full occupancy ~$107/mo or
$2.14/user at 100% utilisation).
The window is policy, not code:
- admin/data/pricing-policy.json: free_tier_usd=0.1, free_tier_period="day"
- wx11_billing: FREE_TIER_PERIOD, allowance_window(plan) (day for free, month for paid),
period_start(window), spend_usd(session, window) — one definition used by BOTH the gateway and
the billing API (the old month_spend_usd duplicate is what let them drift before)
- gateway + /api/billing/subscription now report the same window; plan_allowance gains `period`
- dashboard.html, newOS4 module-billing, chatstudio account panel say "today" when period=day
- published copy: index.html, pricing.html (card + compare table + how-usage-works), i18n planFree1,
admin/docs/ai-gateway.html
- verify-public-claims.py asserts the number AND the period per plan: a right number on the wrong
window is the same defect as a wrong number
newOS4 bundle rebuilt (module-billing), LATEST_VER v4.6.135 -> v4.6.136, main.html cache keys bumped.
Proof:
- verify-public-claims.py rc=0 (copy == policy == live billing API, including periods)
- /api/billing/subscription: limit_usd 0.1, period "day", remaining 0.1
- gateway: gemma4 200, kimi-k2-thinking 502 (gate gone; fake model => upstream 404)
- smoke-auth PASS, smoke-gateway PASS (limit=$0.1 in the payload check)
- live pages serve "$0.10 of AI usage / day", zero $0.50 left on index/pricing/subscriptions
- allowance math: free cap_cost $0.0714/day, pro $7.7779/mo, team $23.35/mo, ent $77.85/mo
Diffstat (limited to 'modules/core/core-bundle.js')
| -rw-r--r-- | modules/core/core-bundle.js | 51 |
1 files changed, 38 insertions, 13 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 07609e3..e845903 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -1237,6 +1237,31 @@ window.VirtualFileSystem = VirtualFileSystem; // Refactored to support multiple LLM providers with auto-fallback // Class name kept as OllamaManager for backward compatibility class OllamaManager { + + async _httpError(response) { + // Surface what the server actually said. "HTTP 429" tells the user nothing; the body + // carries e.g. "Demo limit reached. Login or sign up for more." (2026-10-28) + let detail = ''; + try { + const text = await response.text(); + if (text) { + try { + const j = JSON.parse(text); + detail = (j && (j.error || j.message)) || ''; + if (j && j.needed_usd != null && j.balance_usd != null) { + detail += ` (balance $${j.balance_usd}, need $${j.needed_usd})`; + } + } catch (_) { + detail = text.slice(0, 200); + } + } + } catch (_) { /* body unreadable or already consumed */ } + if (/^\s*</.test(detail)) detail = ''; // an HTML error page is not a message + const err = new Error(detail ? `${detail} (HTTP ${response.status})` : `HTTP ${response.status}`); + err.status = response.status; + return err; + } + constructor(terminal) { this.terminal = terminal; // --- Backward-compat properties (reflected from active provider) --- @@ -1496,7 +1521,7 @@ class OllamaManager { const response = await fetch(`${ep}/api/tags`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); const data = await response.json(); this.models = data.models || []; this.connected = true; @@ -1533,7 +1558,7 @@ class OllamaManager { const response = await fetch(`${provider.endpoint}/v1/models`, { method: 'GET', headers }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); const data = await response.json(); this.models = (data.data || data.models || []).map(m => ({ name: m.id || m.name, @@ -1571,7 +1596,7 @@ class OllamaManager { const response = await fetch(`${ep}/api/tags`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); const data = await response.json(); this.models = data.models || []; this.connected = true; @@ -1727,7 +1752,7 @@ class OllamaManager { body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled, options: this._ollamaOptions() }), signal }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); if (this.streamEnabled) { const reader = response.body.getReader(); @@ -1784,7 +1809,7 @@ class OllamaManager { }), signal }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); if (this.streamEnabled) { const reader = response.body.getReader(); @@ -1897,7 +1922,7 @@ class OllamaManager { body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled, options: this._ollamaOptions() }), signal }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); let fullResponse = ''; if (this.streamEnabled) { @@ -1960,7 +1985,7 @@ class OllamaManager { }), signal }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); let fullResponse = ''; if (this.streamEnabled) { @@ -2085,7 +2110,7 @@ class OllamaManager { if (response.status === 400) { return await this.chatWithToolsFallback(messages, tools, useModel, signal); } - throw new Error(`HTTP ${response.status}`); + throw await this._httpError(response); } const data = await response.json(); this.terminal.log('INFO', 'Ollama', `chatWithTools round-trip: ${networkTime}s (model: ${useModel}, provider: ${provider.name})`); @@ -2135,7 +2160,7 @@ class OllamaManager { }); const networkTime = Math.round((Date.now() - t0) / 1000); if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + throw await this._httpError(response); } const data = await response.json(); this.terminal.log('INFO', 'Ollama', `chatWithTools round-trip: ${networkTime}s (model: ${useModel}, provider: ${provider.name})`); @@ -2274,7 +2299,7 @@ class OllamaManager { body: JSON.stringify(body), signal }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); const data = await response.json(); const text = data.choices?.[0]?.message?.content || ''; const latency = Date.now() - t0; @@ -2310,7 +2335,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); const data = await response.json(); const text = (data.message?.content || '').trim(); const latency = Date.now() - t0; @@ -2361,7 +2386,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm body: JSON.stringify(body), signal }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw await this._httpError(response); const data = await response.json(); const text = data.message?.content || data.response || ''; const latency = Date.now() - t0; @@ -7817,7 +7842,7 @@ class BillingModule { 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)} this month — $${Number(pa.remaining_usd).toFixed(2)} left, then your balance pays`, 'info'); + 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'); } if (s && s.msgs) { |