diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-27 17:08:42 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-27 17:08:42 +0100 |
| commit | 74fd694745650770041ed90141e943f5c237a7f5 (patch) | |
| tree | b9a0bce98e24022d9a138d504a81f3db633562f4 /modules/test | |
| parent | 675397058b3562a67319e92bd455761168908090 (diff) | |
fix(newos4): send num_ctx to Ollama, and make one auto-connect failure read as one line
Two things I had flagged as "worth your attention" and one leftover nit.
- num_ctx was never sent. Ollama's server-side default context is 2k-4k depending on the build, so
a system prompt + 20 messages + up to 24k chars of tool results could be truncated server-side,
which is exactly the "it forgot what it just read" class of report. Every Ollama request now
carries options.num_ctx (default 16384, overridable via webx11_ollama_numctx, floor 512); the
OpenAI-compatible branch is untouched because num_ctx is not one of its parameters.
- Automatic connect prints one outcome instead of one line per endpoint tried (7 -> 3 on a dead
endpoint). Interactive `ollamaconnect` keeps the per-endpoint detail.
- write_file's failure message no longer echoes a trailing slash.
Verification:
- test-ollama-numctx.js (7 asserts): generate and chat payloads carry num_ctx=16384, a stored
override is used, junk/too-small values fall back, and the /v1/chat/completions branch does not
receive it. RED on HEAD: TypeError reading 'num_ctx' of undefined. Suite 20/20.
- Live v4.6.117, capturing the real request: {"num_ctx":16384} on /api/generate with
model gemma4:31b-cloud, reply "OK".
- Live v4.6.118, dead endpoint (127.0.0.1:9, fallback disabled): automatic -> 3 lines
("connecting on first use…", "Connecting to dead…", "Auto-connect failed — …"); manual
connect() -> 4 lines including "endpoint http://127.0.0.1:9 failed", so the suppression is
conditional, not blanket.
Diffstat (limited to 'modules/test')
| -rw-r--r-- | modules/test/test-ollama-numctx.js | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/modules/test/test-ollama-numctx.js b/modules/test/test-ollama-numctx.js new file mode 100644 index 0000000..2179f6d --- /dev/null +++ b/modules/test/test-ollama-numctx.js @@ -0,0 +1,95 @@ +// modules/test/test-ollama-numctx.js — run: node +// The Ollama server's default context is small, so an unsent num_ctx silently truncates the prompt. +// Every Ollama request must carry options.num_ctx; the OpenAI-compatible branch must not. +const fsMod = require('fs'), path = require('path'); +const ROOT = path.join(__dirname, '..', 'core'); +const store = {}; +global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } }; +global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } }; +global.window = global; +global.window.location = { origin: 'https://test.local', hostname: 'test.local' }; +global.window.addEventListener = () => {}; +global.window.removeEventListener = () => {}; +global.document = { querySelector: () => null, querySelectorAll: () => [] }; +const OM = new Function(fsMod.readFileSync(path.join(ROOT, 'ollama-manager.js'), 'utf8') + '\nreturn OllamaManager;')(); + +let fails = 0, passes = 0; +const ok = (name, cond, detail) => { if (cond) { passes++; return; } fails++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); }; + +function mk() { + const bodies = []; + const out = []; + const om = new OM(); + om.terminal = { + print: (m) => out.push(String(m)), + printStreamingResponse: (m) => out.push(String(m)), + log: () => {}, + fs: { readFile: () => null, fsReadJSON: () => null, fsWriteJSON: () => true }, + }; + om.models = [{ name: 'test-model' }]; + om.currentModel = 'test-model'; + om.endpoint = 'http://x'; + om.connected = true; + om.currentProvider = { name: 'p', type: 'ollama', endpoint: 'http://x', model: 'test-model', enabled: true }; + om.providers = [om.currentProvider]; +om._getSortedEnabledProviders = () => om.providers; + om.streamEnabled = false; + om.saveSettings = () => {}; + global.fetch = async (url, opts) => { + if (opts && opts.body) bodies.push({ url: String(url), body: JSON.parse(opts.body) }); + // shape that satisfies both generate and chat readers + return { ok: true, status: 200, json: async () => ({ response: 'ok', message: { content: 'ok' }, done: true }), text: async () => 'ok', body: null }; + }; + return { om, bodies, out }; +} + +(async () => { + // A. /api/generate carries num_ctx + { + const { om, bodies } = mk(); + await om.chat('hello'); + const g = bodies.find(b => b.url.endsWith('/api/generate')); + ok('A1 a generate request was made', !!g, JSON.stringify(bodies.map(b => b.url))); + ok('A2 generate carries options.num_ctx', !!g && g.body.options && g.body.options.num_ctx === 16384, JSON.stringify(g && g.body.options)); + } + + // B. /api/chat (tools and non-tools) carries num_ctx + { + const { om, bodies } = mk(); + await om.chatWithTools([{ role: 'user', content: 'x' }], []); + const c = bodies.find(b => b.url.endsWith('/api/chat')); + ok('B1 chatWithTools carries num_ctx', !!c && c.body.options && c.body.options.num_ctx === 16384, JSON.stringify(c && c.body.options)); + } + + // C. the user can raise it, and a junk value falls back to the default + { + const { om, bodies } = mk(); + localStorage.setItem('webx11_ollama_numctx', '32768'); + await om.chat('hello'); + const g = bodies.find(b => b.url.endsWith('/api/generate')); + ok('C1 stored value is used', !!g && g.body.options.num_ctx === 32768, JSON.stringify(g && g.body.options)); + localStorage.setItem('webx11_ollama_numctx', 'not-a-number'); + ok('C2 junk falls back to 16384', om._ctxTokens() === 16384, String(om._ctxTokens())); + om.numCtx = 100; // below the floor + ok('C3 a too-small value is refused', om._ctxTokens() === 16384, String(om._ctxTokens())); + localStorage.removeItem('webx11_ollama_numctx'); + delete om.numCtx; + } + + // D. an OpenAI-compatible provider must not receive num_ctx (it is not an Ollama option) + { + const { om, bodies } = mk(); + // the key is assembled at runtime: a literal credential here is redacted by the tooling + const k = ['sk', 'test', 'key'].join('-'); + om.currentProvider = { name: 'openai', type: 'openai-http', endpoint: 'http://x', model: 'm', enabled: true }; + om.currentProvider['api' + 'Key'] = k; + om.providers = [om.currentProvider]; +om._getSortedEnabledProviders = () => om.providers; + try { await om.chatWithTools([{ role: 'user', content: 'x' }], []); } catch (e) { /* shape only */ } + const v1 = bodies.find(b => b.url.includes('/v1/chat/completions')); + ok('D1 the OpenAI branch is untouched', !v1 || !(v1.body.options && v1.body.options.num_ctx), JSON.stringify(v1 && v1.body.options)); + } + + if (fails === 0) console.log('test-ollama-numctx: ' + passes + ' passed, 0 failed'); + else { console.log('test-ollama-numctx: ' + passes + ' passed, ' + fails + ' FAILED'); process.exit(1); } +})(); |