Skip to content

feat: local LLM support via Ollama (OpenAI-compatible, behind ENABLE_OLLAMA) - #34

Open
amal66 wants to merge 1 commit into
upstream-pr/provider-registryfrom
upstream-pr/local-llm-ollama
Open

feat: local LLM support via Ollama (OpenAI-compatible, behind ENABLE_OLLAMA)#34
amal66 wants to merge 1 commit into
upstream-pr/provider-registryfrom
upstream-pr/local-llm-ollama

Conversation

@amal66

@amal66 amal66 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in local LLM support via Ollama, so a self-hosted deployment can run chat inference entirely on its own hardware — documents never leave the building, and with this flag the model inference doesn't either. Ollama is exposed through its OpenAI-compatible endpoint, so the existing OpenAI streaming/completion adapter does all the work; the Ollama "provider" is just a registry entry that routes local model IDs (llama3.3, phi4, qwen2.5, ...) through that adapter pointed at http://localhost:11434/v1.

Everything is behind ENABLE_OLLAMA=true (default false): with the flag off, no Ollama models appear in the model set and nothing about existing behavior changes.

Changes

  • backend/src/lib/llm/providers/ollama.ts — Ollama adapter registered through the provider registry: setupOllamaFromEnv() gates on ENABLE_OLLAMA=true, registers a default local-model list plus optional extras from OLLAMA_MODELS (comma-separated), and reuses the OpenAI adapter's stream/complete functions. Also registered as an API-key provider that reuses OPENAI_API_KEY (Ollama accepts any non-empty string).
  • backend/src/lib/llm/index.tsregisterBuiltinProviders() now takes an injectable env and registers Ollama when enabled.
  • backend/src/lib/llm/baseUrl.tsOPENAI_BASE_URL resolution for the OpenAI adapter with an SSRF guard: http/localhost/private-IP endpoints are rejected in production unless OPENAI_ALLOW_LOCAL_BASE_URL=true. Defaults to https://api.openai.com/v1 (unchanged behavior when unset).
  • backend/src/lib/llm/openai.ts — fetches openAIResponsesUrl() instead of the hard-coded api.openai.com constant (same URL by default).
  • backend/src/lib/privateIp.ts — shared private/reserved IP classifier (IPv4 + IPv6 incl. mapped/NAT64/6to4 forms) used by the base-URL guard.
  • backend/.env.example — documents the opt-in configuration.
  • Tests: providers/__tests__/ollama.test.ts (ENABLE_OLLAMA gate, custom models), __tests__/baseUrl.test.ts (default endpoint, normalization, production SSRF rejections), __tests__/privateIp.test.ts (classifier ranges).

Why

  • Off by default: ENABLE_OLLAMA defaults to false, so cloud deployments see zero cost and zero behavior change — no new models in the picker, no new egress, and the OpenAI adapter still hits https://api.openai.com/v1.
  • Uses Ollama's OpenAI-compatible endpoint rather than a bespoke client, so no new runtime dependencies and no new streaming code path — local models exercise the exact same adapter as OpenAI.
  • Stacks on the provider-registry PR (base branch upstream-pr/provider-registry): the registry was built so a provider like this is a single self-contained file plus one gated registration call; this PR is the first external provider to use it.

Testing

  • npm install && npm run build (tsc) in backend/ — clean on the committed tree.
  • With the vitest harness branch merged locally (harness files not committed here): npx vitest run in backend/ — 6 test files, 53 tests, all passing. This includes the 3 new test files added by this PR (ollama gate: 3 tests, baseUrl: 7 tests, privateIp: 9 tests) alongside the existing registry/models/downloadTokens suites.
  • Verified the flag gate: setupOllamaFromEnv({}) and ENABLE_OLLAMA=false/"1" register nothing; only the exact string "true" registers the provider.

Provenance

All changes are mechanical ports of code in amal66/mike@origin/main (commit b3166dd); exceptions:

  • baseUrl.ts: reads process.env.* directly instead of the fork's zod-validated lib/env module (this repo has no such module); same defaults and semantics.
  • baseUrl.test.ts / ollama.test.ts: dropped the fork's vi.mock("../env") blocks for the same reason (nothing to mock; env reads are lazy).
  • llm/index.ts: only the ENABLE_OLLAMA branch of the fork's registration logic is ported (its air-gap branches are intentionally out of scope); the boot log uses console.log (this repo has no lib/logger); doc comments condensed to drop air-gap references.
  • .env.example: fork's Ollama block minus the OLLAMA_EMBEDDING_MODELS line (local embeddings are not part of this PR).

privateIp.ts, privateIp.test.ts, and providers/ollama.ts are byte-identical to the fork's files.

Credits & prior art

🤖 Generated with Claude Code

https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC

…OLLAMA)

Mechanical port from b3166dd (apps/api -> backend):
- lib/llm/providers/ollama.ts: Ollama adapter registered through the
  provider registry, gated by ENABLE_OLLAMA (off by default)
- lib/llm/baseUrl.ts: OPENAI_BASE_URL resolution with SSRF guard
  (OPENAI_ALLOW_LOCAL_BASE_URL) so the OpenAI adapter can point at a
  local Ollama server
- lib/privateIp.ts: shared private/reserved IP classifier used by the
  base-URL guard
- lib/llm/openai.ts: fetch openAIResponsesUrl() instead of the
  hard-coded api.openai.com constant
- lib/llm/index.ts: register Ollama in registerBuiltinProviders() when
  ENABLE_OLLAMA=true
- .env.example: document the opt-in Ollama configuration
- tests for the ollama gate, base-URL resolution, and IP classifier

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant