AI: backend Ollama + wybór modelu w komendzie przełączania #25

Merged
gitea merged 3 commits from ai-ollama-provider into main 2026-08-24 15:41:28 +00:00
Owner

Co daje

Bot mówił przez OpenAI albo Anthropic. Dochodzi Ollama — modele hostowane u nas — a komenda przełączania pozwala wybrać który model, nie tylko który backend.

Jak wpięte

Ollama wystawia OpenAI-kompatybilne /v1, więc klient to po prostu openai.AsyncOpenAI(base_url=OLLAMA_URL + "/v1"). Dzięki temu reużywamy istniejący format wiadomości i całe mapowanie błędów (_map_openai_error) zamiast dublować drugą taksonomię. Klucza API nie ma — endpoint JEST konfiguracją, więc backend jest uśpiony i odmawia wyboru (z komunikatem podającym nazwę zmiennej), dopóki nie ustawisz CONJURER_OLLAMA_URL.

Wybór modelu

  • $modele_ai [config]pyta serwer (/v1/models), więc pokazuje to, co realnie masz wgrane, a nie listę zaszytą w kodzie.
  • $gadaj_teraz ollama <model> — przełącza i przypina model (trwale). Model, którego Ollama nie ma, jest odrzucany od razu z prawdziwą listą — inaczej literówka wyszłaby dopiero jako nieudana odpowiedź.
  • cheap_model (ścieżka MUSIC) zostaje nietknięty.

Dwie rzeczy, które to odsłoniło

  • AI_CONFIGS scala wbudowane domyślne z blokiem z pliku ustawień. Każde przełączenie zapisuje blok configs, więc plik zapisany starszą wersją na stałe ukryłby nowo dodanego providera.
  • Twardy timeout 120 s to teraz CONJURER_AI_TIMEOUT_SECONDS — model na domowym GPU może potrzebować więcej.

Regresja złapana przez adversarialny review (drugi commit)

Pierwsza wersja zapisywała cały blok configs przy każdym przełączeniu. To kasowało ręczne edycje operatora (jedyny sposób na zmianę cheap_model/temperature), wskrzeszało usunięte configi i unieważniało CONJURER_OLLAMA_MODEL. Odtworzone na realnym kodzie, nie wywnioskowane. Poprawka jest węższa niż revert: zapisujemy tylko to pole, które ten proces faktycznie zmienił.

Ścieżka zapisu na dysk nie miała żadnego pokrycia — i tak to weszło. Dołożone 4 testy jadące realny _persist_active_ai_config na tymczasowym pliku; zweryfikowane mutacyjnie, że mają zęby (cofnięcie poprawki wywala 2 z nich). Plus utwardzone 2 słabe testy: jeden asertował na obiekcie zwracanym przez set_active_model (czyli na tym samym, zmutowanym dict — przechodził zawsze), drugi ustawiał OLLAMACLIENT na None, gdy i tak już był None.

Do ustawienia u Ciebie

CONJURER_OLLAMA_URLpodaj mi adres Twojej Ollamy, to dorzucę go do manifestów deploya (bot testowy + deploy-bot). Sam nie zgaduję: pamięć opisuje Ollamę w LXC na pve2, a Ty piszesz o klastrze.

Suite: 72 unit + 70 integration zielone.

🤖 Generated with Claude Code

## Co daje Bot mówił przez OpenAI albo Anthropic. Dochodzi **Ollama** — modele hostowane u nas — a komenda przełączania pozwala wybrać **który model**, nie tylko który backend. ## Jak wpięte Ollama wystawia **OpenAI-kompatybilne** `/v1`, więc klient to po prostu `openai.AsyncOpenAI(base_url=OLLAMA_URL + "/v1")`. Dzięki temu reużywamy istniejący format wiadomości i **całe mapowanie błędów** (`_map_openai_error`) zamiast dublować drugą taksonomię. Klucza API nie ma — **endpoint JEST konfiguracją**, więc backend jest uśpiony i odmawia wyboru (z komunikatem podającym nazwę zmiennej), dopóki nie ustawisz `CONJURER_OLLAMA_URL`. ## Wybór modelu - `$modele_ai [config]` — **pyta serwer** (`/v1/models`), więc pokazuje to, co **realnie masz wgrane**, a nie listę zaszytą w kodzie. - `$gadaj_teraz ollama <model>` — przełącza i **przypina** model (trwale). Model, którego Ollama nie ma, jest odrzucany **od razu** z prawdziwą listą — inaczej literówka wyszłaby dopiero jako nieudana odpowiedź. - `cheap_model` (ścieżka MUSIC) zostaje nietknięty. ## Dwie rzeczy, które to odsłoniło - `AI_CONFIGS` **scala** wbudowane domyślne z blokiem z pliku ustawień. Każde przełączenie zapisuje blok `configs`, więc plik zapisany starszą wersją **na stałe ukryłby** nowo dodanego providera. - Twardy timeout 120 s to teraz `CONJURER_AI_TIMEOUT_SECONDS` — model na domowym GPU może potrzebować więcej. ## Regresja złapana przez adversarialny review (drugi commit) Pierwsza wersja zapisywała **cały** blok `configs` przy każdym przełączeniu. To kasowało ręczne edycje operatora (jedyny sposób na zmianę `cheap_model`/`temperature`), **wskrzeszało usunięte configi** i unieważniało `CONJURER_OLLAMA_MODEL`. Odtworzone na realnym kodzie, nie wywnioskowane. Poprawka jest węższa niż revert: zapisujemy **tylko to pole, które ten proces faktycznie zmienił**. Ścieżka zapisu na dysk **nie miała żadnego pokrycia** — i tak to weszło. Dołożone 4 testy jadące realny `_persist_active_ai_config` na tymczasowym pliku; zweryfikowane mutacyjnie, że **mają zęby** (cofnięcie poprawki wywala 2 z nich). Plus utwardzone 2 słabe testy: jeden asertował na obiekcie zwracanym przez `set_active_model` (czyli na tym samym, zmutowanym dict — przechodził zawsze), drugi ustawiał `OLLAMACLIENT` na `None`, gdy i tak już był `None`. ## Do ustawienia u Ciebie `CONJURER_OLLAMA_URL` — **podaj mi adres Twojej Ollamy**, to dorzucę go do manifestów deploya (bot testowy + deploy-bot). Sam nie zgaduję: pamięć opisuje Ollamę w LXC na pve2, a Ty piszesz o klastrze. Suite: **72 unit + 70 integration** zielone. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea self-assigned this 2026-08-24 12:07:59 +00:00
gitea added 2 commits 2026-08-24 12:07:59 +00:00
The bot could talk to OpenAI or Anthropic; this adds Ollama as a third
provider so it can run against models hosted on our own box, and extends
the switch command to pick WHICH model - not just which backend.

Provider: Ollama exposes an OpenAI-compatible /v1 surface, so the client is
just openai.AsyncOpenAI(base_url=OLLAMA_URL + "/v1"). That reuses the
existing message format and the whole _map_openai_error mapping instead of
forking a second error taxonomy. There is no API key - the endpoint IS the
configuration, so the backend stays dormant (and refuses to be selected,
with a message naming the variable) until CONJURER_OLLAMA_URL is set, the
same way the Conan bridge behaves.

Model selection:
* list_provider_models() asks the SERVER for Ollama (/v1/models), so the
  picker shows what is actually pulled on the box rather than a hardcoded
  list. Hosted providers just report what they are wired to.
* set_active_model() pins the config's latest_model and persists it;
  cheap_model is left alone so the MUSIC path keeps its cheaper backend.
* $gadaj_teraz now takes "<config> [model]", and a new read-only $modele_ai
  lists what is available. Pinning an id Ollama does not have is rejected up
  front with the real list - otherwise the typo only surfaces later as a
  failed reply.

Two fixes this exposed:
* AI_CONFIGS now merges built-in defaults with the settings-file block
  instead of letting the file win outright. Every provider switch persists a
  "configs" block, so a file written by an older build would have
  permanently hidden ollama from the picker after an upgrade.
* _persist_active_ai_config assigns "configs" instead of setdefault, so a
  pinned model actually survives a restart.
* the hardcoded 120s response timeout is now CONJURER_AI_TIMEOUT_SECONDS - a
  self-hosted model on a modest GPU can legitimately need longer.

Tests cover: ollama appears in the picker, select_model maps the legacy
gpt-4o default instead of leaking it, model listing (server-queried, sorted,
de-duplicated, failure -> AIError, unconfigured -> auth), pinning (latest
only, blank/unknown rejected), and that provider_generate routes to the new
path. Suite: 68 unit + 70 integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AI: persist only the pinned field, not the whole config block
CI / compile (pull_request) Successful in 7s
CI / unit (pull_request) Successful in 23s
CI / integration (pull_request) Failing after 24s
bf7c3d9093
Adversarial review of the previous commit found a real regression it
introduced, reproduced against the actual code rather than inferred.

Changing _persist_active_ai_config from setdefault("configs", ...) to a
direct assignment made every backend switch write the whole in-memory
AI_CONFIGS over the settings file. Because AI_CONFIGS is now the built-in
defaults merged UNDER the file, that meant:

* an operator's hand edits were destroyed - and hand editing is the only
  way to change cheap_model / temperature / max_tokens, since
  set_active_model writes latest_model and there is no command for the rest,
* a config deliberately deleted from the file was re-seeded from the
  defaults and written back, permanently,
* pinning a model for one provider silently reverted another provider's
  entry,
* CONJURER_OLLAMA_MODEL stopped having any effect once the env-derived
  block had been persisted once.

The original motivation was still valid (plain setdefault would drop a
pinned model), so the fix is narrower rather than a revert: persist ONLY
the field this process actually changed. _persist_active_ai_config takes
model_for and writes back just that config's latest_model; everything else
in the on-disk block is left exactly as found. The constants.py merge stays
- it is what keeps a newly added provider visible after an upgrade - and is
now in-memory only, so it cannot reach the file.

Tests: the disk-write path had ZERO coverage, which is precisely how this
got in. Added four tests that drive the real _persist_active_ai_config
against a temp settings file: the pin lands while operator edits survive and
a deleted config is not resurrected; a plain switch leaves the configs block
byte-identical; a pin survives a re-read; a corrupt file does not raise.
Verified they have teeth - reintroducing the regression fails two of them.

Also hardened two weak tests the review caught: the pin test asserted on the
object set_active_model returns, which IS the mutated dict (so it passed
regardless), and the unconfigured-endpoint test monkeypatched OLLAMACLIENT
to None when it was already None, passing vacuously.

Suite: 72 unit + 70 integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gitea added 1 commit 2026-08-24 12:54:11 +00:00
AI: warn when the selected Ollama model is not on the server
CI / compile (pull_request) Successful in 5s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 24s
3279bb923d
Switching to a backend whose configured model the server does not have
succeeded silently, and then every reply failed with "model not found" with
nothing explaining why. The switch already fetches the model list to show
what else is available, so use it: if the config's model is absent, say so
and list what IS there.

Found while probing the real server (192.168.1.72): it has exactly one
model, gemma4:e2b, so the built-in llama3.1:8b default would have hit this
on the first switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gitea merged commit fdc1fa1817 into main 2026-08-24 15:41:28 +00:00
gitea deleted branch ai-ollama-provider 2026-08-24 15:41:28 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gitea/conjurer#25