aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-11-01 18:44:26 +0100
committerAgando <roduccomm@gmail.com>2026-11-01 18:44:26 +0100
commit0f02d398c86640547f58e0717735d45489abb766 (patch)
tree449f54160fb6b415cfccf8ee3220fcd58e1a16ec
parentd352c464fd9f821542f58b6510a835efa591360b (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
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js51
-rw-r--r--modules/core/module-billing.js2
-rw-r--r--modules/core/module-loader.js2
4 files changed, 43 insertions, 18 deletions
diff --git a/main.html b/main.html
index 6b1aed4..a785d7c 100644
--- a/main.html
+++ b/main.html
@@ -1349,13 +1349,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.135"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.136"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=v4.6.135"></script>
+<script src="modules/core/module-loader.js?v=v4.6.136"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.135"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.136"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
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) {
diff --git a/modules/core/module-billing.js b/modules/core/module-billing.js
index b353e79..87f8dc3 100644
--- a/modules/core/module-billing.js
+++ b/modules/core/module-billing.js
@@ -142,7 +142,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) {
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 52cee36..0cc4dd2 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.135';
+ const LATEST_VER = 'v4.6.136';
const CACHE = '?v=' + LATEST_VER;
// Exposed so ext modules can register their own URL with the same cache-busting version
// instead of an unversioned path the browser may already have cached (shell.js did).