aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-ollama-numctx.js
blob: 2179f6d6b6f14afc546fa3cbb41e4c252c4dce3d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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); }
})();