> ## Content Index
> Fetch the complete content index at: https://www.workroom-productions.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Switching Sitegeist to a local Ollama model (or Haiku 5.5)
- URL: https://www.workroom-productions.com/switching-sitegeist-to-a-local-model/
- Published: 2026-11-14T15:46:07.000Z
- Updated: 2026-11-15T00:00:29.000Z
- Description: Browser scripts to reconfigure Sitegeist to use local models.
- Author: James Lyndsay
- Tags: Articles, Local_LLM

*This was initially written by Haiku 5.5, as a summary of a guided exploration alongside further explorations via deepwiki of* [*sitegeist*](https://deepwiki.com/badlogic/sitegeist) *and* [*pi-mono’s pi-ai*](https://deepwiki.com/badlogic/pi-mono/2-pi-ai:-llm-api-library)*. I’ve edited every sentence here, cut lots and added some. I’ve scanned and used the code. Use at your own risk, obvs.*

**tl;dr; run this simple script, open Ollama, and Sitegeist works with local models.**

[Sitegeist](https://sitegeist.ai/) is handy, (recently-)dead software. Its models are out-of-date, as they were pulled from [pi-mono’s pi-ai](https://github.com/earendil-works/pi/blob/main/packages/ai/README.md) when the extension was built. It can’t use local models, either. Bitrot sucks.

I could remake the extension, pulling a new list, and write new code to look for local models. That seems a bit much; the different models are just different endpoints. Alternatively, I could monkeypatch Sitegeist’s settings to point to what I want. Let’s hack on that.

Sitegeist uses [IndexedDB](https://en.wikipedia.org/wiki/IndexedDB) – a noSQL database living in the browser. Within that, I want to change its `settings`’s `lastUsedModel` record, which sets the current LLM endpoint, and is sticky – it persists after reload. Just what I need.

Here is that record, for Haiku 4.5\. I got this later: it's here as an illustration.

```javascript
lastUsedModel: [
  {
    "id": "claude-haiku-4-5",
    "name": "Claude Haiku 4.5 (latest)",
    "api": "anthropic-messages",
    "provider": "anthropic",
    "baseUrl": "https://api.anthropic.com",
    "reasoning": true,
    "input": [
      "text",
      "image"
    ],
    "cost": {
      "input": 1,
      "output": 5,
      "cacheRead": 0.1,
      "cacheWrite": 1.25
    },
    "contextWindow": 200000,
    "maxTokens": 64000
  },
  false,
  "https://proxy.mariozechner.at/proxy"
]

```

Unfortunately, I can’t directly change these values with the Chrome DevTools (I’m in Brave for this, and Brave includes Chrome’s DevTools). However, I can make the changes with a script.

Here’s the full script, via DeepWiki and me, to switch Sitegeist to Haiku5.5\. It needs to run in DevTools from the Sitegeist extension context (open ‘inspect’ from the extension’s sidebar). I’ve dumped it into a snippet, so that I can make the switch whenever I fancy.

```js
(async () => {
  const db = await new Promise((res, rej) => {
    const r = indexedDB.open('sitegeist-storage');
    r.onsuccess = () => res(r.result);
    r.onerror   = () => rej(r.error);
  });

  await new Promise((res, rej) => {
    const tx = db.transaction('settings', 'readwrite');
    const store = tx.objectStore('settings');

    store.get('lastUsedModel').onsuccess = (e) => {
      const rec = e.target.result;
      if (!rec) return console.warn('No record keyed "lastUsedModel"');
      console.log('before:', rec);

      rec.id   = 'anthropic/claude-haiku-5-5';
      rec.name = 'Anthropic: Claude Haiku 5.5';
      store.put(rec, 'lastUsedModel');
    };

    tx.oncomplete = res;
    tx.onerror    = () => rej(tx.error);
  });

  db.close();
  console.log('Saved — click Refresh in the Application panel, then reload the tab.');
})();

```

careful before you use this

The script expects to start from Haiku4.5, and makes the following changes to the record…

```js
    "id": "claude-haiku-5-5",
    "name": "Claude Haiku 5.5",

```

…once that’s done, I ask for a new chat with Sitegiest, and the extension is using Haiku 5.5.

Wahey!

#### What to use this script yourself? Check this first.

The script expects `lastUsedModel` to look like that Haiku 4.5 record above. If yours doesn't – if it's via OpenRouter or has extra stuff – the script won't work. It's a hack, after all. 

And of course the cost, context window etc is for the wrong model, too. Be smart: work it out rather than copy/paste.

## Reading the record

Step back. Deepwiki used its reading of the code to guess well, but switching to a local model means I’ll need to change more than the model `id`. I can imagine what to change from the record above: `baseURL` at least, probably tokens and cost. But how did I get that record in the first place?

I looked in Chrome DevTools, picked out application:IndexedDB and browsed. But I can’t copy/paste the data. Heaven knows why. And the first alternative – my eyeball/scribble game – isn’t reliable enough *at all*. Screenshot and OCR? Pfft. There's truncatin' goin' on.

I gave Haiku5.5 the DeepWiki script to do the change, asking it to give me something to get the data. It gave me a script to run. Again, it needs to run from the Sitegeist extension context: I used ‘inspect’ from the side panel to get to the extension's DevTools console, pasted the script into the console, looked for the record output as a log, and that's it, up the top.

### Actually, no...

Along the way I managed to run a different script that trashed the very record I was looking for: LLMs don’t seem to make me less error-prone. After faffage, I have *two* scripts, not one – purporting to do the same thing, actually but producing different output.

You know a simpler way? I bet you do. This is the way *I* went.

First script (returns a slice of `store.getAll()`)

```javascript
(async () => {
  const db = await new Promise((res, rej) => {
    const r = indexedDB.open('sitegeist-storage');
    r.onsuccess = () => res(r.result);
    r.onerror = () => rej(r.error);
  });

  const tx = db.transaction('settings', 'readonly');
  const store = tx.objectStore('settings');

  const keysReq = store.getAllKeys();
  const valsReq = store.getAll();

  tx.oncomplete = () => {
    console.log('keys:', keysReq.result);
    console.log('lastUsedModel:', JSON.stringify(valsReq.result, null, 2));
    db.close();
  };
})();

```

Second script (returns `store.get('lastUsedModel')`)

```javascript
const readSettings = () => new Promise((resolve, reject) => {
  const open = indexedDB.open('sitegeist-storage');

  open.onerror = () => reject(open.error);
  open.onblocked = () => reject(new Error('Database open is blocked by another connection'));
  open.onupgradeneeded = () => {
    open.transaction.abort();
    reject(new Error('Database did not exist. Check that you are in the Sitegeist extension context.'));
  };

  open.onsuccess = () => {
    const db = open.result;
    const tx = db.transaction('settings', 'readonly');
    const store = tx.objectStore('settings');

    const result = { keyPath: store.keyPath, autoIncrement: store.autoIncrement };
    const keysReq = store.getAllKeys();
    const valsReq = store.getAll();
    const lastReq = store.get('lastUsedModel');

    tx.oncomplete = () => {
      db.close();
      resolve({
        ...result,
        keys: keysReq.result,
        all: valsReq.result,
        lastUsedModel: lastReq.result,
      });
    };
    tx.onerror = () => { db.close(); reject(tx.error); };
    tx.onabort = () => { db.close(); reject(tx.error ?? new Error('Transaction aborted')); };
  };
});

const info = await readSettings();
console.log('keyPath:', info.keyPath, 'autoIncrement:', info.autoIncrement);
console.log('keys:', info.keys);
console.log('lastUsedModel:', JSON.stringify(info.lastUsedModel, null, 2));

```

One agent chat tells me that one script’s `lastUsedModel` record is a three-element array: the model object, `false`, and a proxy URL. Another told me it saw a plain object, maybe from the other script. Initially I reckoned this was a serialisation something, or maybe to do with copying from the console. Maybe Sitegeist was set up for different models. Turns out it might be me forgetting that I’d made mistakes that led to two scripts…

Anyhoo, the script I offer below handles both shapes, array and object. Which is entirely to its detriment: one of those paths is irrelevant. Onwards...

## Point the extension to Ollama/Qwen3.5:9B

On my machine, Ollama is on port 11434, so this command lists the models currently served by Ollama on my Mac.  
`curl http://localhost:11434/api/tags`

I handed the output to Haiku5.5, once again with DeepWiki’s script that switched lastUsedModel, and the record I’d hoiked out above. After a couple of iterations, I landed on something viable.

This script sets up the parameters in `MODEL`, connects, backs up the current value, checks its shape (that array/object trouble above), and writes the Ollama model. One side of the array/object processing `if (Array.isArray(old))` is, I hope, redundant and a reflection of the mistakes above. It’s cruft, and I’ll fix it. Or you will. Left here for those who care more for honesty than utility:

```javascript
const MODEL = {
  id: 'qwen3.5:9b',
  name: 'Ollama: qwen3.5:9b',
  api: 'openai-completions',           // guess
  provider: 'dummy',                   // no key needed for local
  baseUrl: 'http://localhost:11434/v1',
  reasoning: true,
  input: ['text', 'image'],
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
  contextWindow: 262144,
  maxTokens: 8192,
};

const db = await new Promise((res, rej) => {
  const r = indexedDB.open('sitegeist-storage');
  r.onsuccess = () => res(r.result);
  r.onerror = () => rej(r.error);
});

await new Promise((res, rej) => {
  const tx = db.transaction('settings', 'readwrite');
  const store = tx.objectStore('settings');

  store.get('lastUsedModel').onsuccess = (e) => {
    const old = e.target.result;
    console.log('BACKUP (copy this):', JSON.stringify(old));

    if (Array.isArray(old)) {
      const [, ...rest] = old;
      store.put([MODEL, ...rest], 'lastUsedModel');
      console.log('Wrote MODEL into array form, kept', rest.length, 'other element(s).');
    } else if (old && typeof old === 'object') {
      store.put(MODEL, 'lastUsedModel');
      console.log('Wrote MODEL as an object.');
    } else {
      tx.abort();
      console.warn('Unrecognised value, nothing written:', old);
    }
  };

  tx.oncomplete = res;
  tx.onabort = () => rej(new Error('Aborted; nothing changed'));
  tx.onerror = () => rej(tx.error);
});

db.close();
console.log('Done. Reload Sitegeist.');

```

When I ran the script, I could see that it had changed the record (I checked by eye after a refresh of the ‘application’ tab). And, satisfyingly, in a new chat, Sitegeist told me it was using my local Qwen3.5\. 

Frustratingly, all its responses were `403 Forbidden`. By default, Ollama doesn’t talk to queries coming out of Chrome/Brave extensions. 

Here’s a minimal working example to show that:

```bash
curl -i http://localhost:11434/v1/models -H "Origin: chrome-extension://test"

HTTP/1.1 403 Forbidden
Date: Sat, 14 Nov 2026 12:46:08 GMT
Content-Length: 0

```

Fixable? Yaaaah. With a squizz at the docs, or a bit more advice from an LLM...

## Allow Ollama to reply

Dangerously-ish, I can tell Ollama to respond openly to `Origin: chrome-extension:` . 

```sh
launchctl setenv OLLAMA_ORIGINS "chrome-extension://*"

```

*Note that your kit might need something else – but this is what I used and I'm not on your kit.*

Then, restart Ollama. That minimal example now does this:

```sh
curl -i http://localhost:11434/v1/models -H "Origin: chrome-extension://test"

HTTP/1.1 200 OK
Access-Control-Allow-Origin: chrome-extension://test
Content-Type: application/json
Vary: Origin
Date: Sat, 14 Nov 2026 12:46:32 GMT
Content-Length: 665

{"object":"list","data":[{"id":"qw... blah blah blah

```

And with that indication that the plumbing is connected, I start up a new Sitegeist chat, drum roll aaand…

**Brave’s Sitegeist extension will merrily chat to a model on my laptop**, giving me slow, dumb responses that mess up tool responses and get lost in their own entrails. 

On the way, I’ve left my local machine open to any other random or exploited extension that wants to use my installed LLMs. Nice.

I should probably `launchctl setenv OLLAMA_ORIGINS "chrome-extension://«sitegeist's-extension-id»”`

Anyway: 30 minutes to do the thing, 90 to write it up here. Hope you liked the writeup, and either find the thing useful, or find some use in my notes on how I got there.