diff options
| author | Agando <roduccomm@gmail.com> | 2026-11-02 20:46:18 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-11-02 20:46:18 +0100 |
| commit | 8d5c4b563da5225c4dbd2a44c7635c49a16d5ab1 (patch) | |
| tree | de05cf1dcda3b45f131e4b1fca85421b49facdd6 /modules/core/ollama-manager.js | |
| parent | 89787327ec5aba15d11ca7bff2b4fb7f0df24a79 (diff) | |
feat(newos4): configurable tool budgets, per-model defaults, real tool telemetry
Closes STEER-20261102-201234-7200 (read_file capped at 6000 while the schema
promised 32000).
Budgets (module-aiassist.js + new modules/core/model-profiles.js):
- one resolver, precedence: call arg -> loop profile -> model profile ->
ai-config -> code default. Keys: read-page-chars, read-result-chars,
tool-context-budget, fetch-chars.
- model classes tiny/small/mid/large keyed by id then by size token; `mid`
reproduces the old numbers so nothing moves for a mid model.
- _clipToolResult now clips the `content` field, never the serialised JSON
envelope: clipping the envelope deleted truncated/next_offset, so a 16k page
lost its continuation pointer (the reported symptom).
- read_file's schema no longer promises a number the model class may not give.
Telemetry:
- _recordToolCall keeps sizes, duration, ok and a 120-char arg preview instead
of the whole payload; ai-stats prints calls / ok% / avg ms / KB / truncation
from localStorage webx11_tool_stats.
Server (api/proxy-api.py):
- /api/extract honours max_chars (default 12000, cap 100000) and returns
content_chars + truncated; the bs4 branch is no longer uncapped while the
regex fallback was silently cut at 10 000.
BEHAVIOUR CHANGE: num_ctx for a model with no explicit setting now follows the
class default (16384 -> 65536 for cloud/large, 8192 for tiny). Cloud models bill
on tokens used, so cost is unchanged; local models get a bigger KV cache.
Two build traps, both now documented in AGENTS.md:
- model-profiles.js must be the FIRST entry in CORE_FILES and must export the
MODEL_PROFILES global (a TDZ typeof throws; without the global every budget
silently falls back to the code default).
- a budget key needs the setter branch AND the constructor loader at
module-aiassist.js:49 - persisting without restoring looks fine until reload.
Verified: test-tool-context-bound 23/23 (RED before: E1-E4 + TypeError on
_toolLimit); live bundle read_page 32000 / result 12000 / budget 48000 /
fetch 12000 and _ctxTokens 65536 for gemma4:31b-cloud; /api/extract 28970 ->
500 chars with truncated true; ai-config write+restore probe 6/6.
Diffstat (limited to 'modules/core/ollama-manager.js')
| -rw-r--r-- | modules/core/ollama-manager.js | 58 |
1 files changed, 45 insertions, 13 deletions
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index 7dcecc3..1201280 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -2,6 +2,31 @@ // 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) --- @@ -61,7 +86,14 @@ class OllamaManager { // just told. Send it on every Ollama request; the OpenAI-compatible branch has its own params. _ctxTokens() { const n = Number(this.numCtx || (typeof localStorage !== 'undefined' ? localStorage.getItem('webx11_ollama_numctx') : 0)); - return Number.isFinite(n) && n >= 512 ? Math.floor(n) : 16384; + if (Number.isFinite(n) && n >= 512) return Math.floor(n); + // No explicit setting: take the model class default (large/cloud 65536, mid 32768, …) + try { + const mp = this.modelProfiles + || (typeof MODEL_PROFILES !== 'undefined' ? MODEL_PROFILES : null); + if (mp && mp.profileFor && this.currentModel) return mp.profileFor(this.currentModel).ctx; + } catch (e) {} + return 16384; } _ollamaOptions(extra) { @@ -261,7 +293,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; @@ -298,7 +330,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, @@ -336,7 +368,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; @@ -492,7 +524,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(); @@ -549,7 +581,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(); @@ -662,7 +694,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) { @@ -725,7 +757,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) { @@ -850,7 +882,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})`); @@ -900,7 +932,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})`); @@ -1039,7 +1071,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; @@ -1075,7 +1107,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; @@ -1126,7 +1158,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; |