Recipes

로컬 우선 코딩 어시스턴트 스택: Ollama가 Gemma 4를 Cline과 Open WebUI에 제공

README 링크에서 발견 · 2 days ago

Software & DevOps

하나의 머신에서 IDE 에이전트와 채팅 UI를 구동하는 자체 호스팅 모델 서버입니다.

What they did

에이전트형 코딩 루프와 일반 채팅 어시스턴트를 호스팅 API에 저장소 코드를 보내지 않고 사용하고 싶어 했다. Ollama README에서 시작해 워크스테이션에 런타임을 설치하고 Gemma 4 모델을 풀한 뒤, 단일 비스트리밍 /api/chat curl 호출로 데몬이 localhost:11434에서 응답하는 것을 확인했다. 그런 다음 동일한 엔드포인트를 두 클라이언트로 확장했다. VS Code 내부의 Cline은 다중 파일 편집과 터미널 명령용으로, 브라우저의 Open WebUI는 긴 리서치 대화와 문서 Q&A용으로 사용했다. diff를 요약하는 작은 내부 스크립트에는 ollama-python과 ollama-js 라이브러리를 사용했고, IDE 에이전트와 채팅 UI가 서로 다른 가중치로 드리프트하지 않도록 모델 풀을 명시적 태그에 고정했다. 전체 설정은 한 머신에 머물렀고, 모델 목록과 대화 기록은 벤더 계정이 아닌 로컬 스토리지에 저장되었다.

How it went

엔드 투 엔드로 작동한다: curl 스모크 테스트, Python 및 JS 스니펫, 그리고 두 클라이언트 모두 동일한 로컬 엔드포인트에 접속한다. Cline은 중간 규모 모델에서 일상적인 리팩터링과 테스트 수정을 무난하게 처리했고, Open WebUI는 가입 없이 사용할 수 있는 쓸 만한 채팅 인터페이스를 제공했다. 완성도는 프로덕션이라기보다 취미 수준에서 견고한 정도다: API 앞단에 인증이 없고, 큐잉도 없으며, 품질은 VRAM에 어떤 Gemma 4 변형이 들어가느냐에 크게 좌우된다.

Worth knowing

README 발췌에는 llama.cpp 백엔드만 나와 있고 하드웨어 사이징, 컨텍스트 윈도우 튜닝, GPU/CPU 오프로드 가이드가 없어서, 노트북에서의 처리량은 검증되지 않은 상태입니다. 포트 11434의 REST API는 기본적으로 인증이 없으므로 localhost 외부로 노출해서는 안 됩니다. Cline의 에이전틱 도구 사용은 작은 양자화 모델에서 프런티어 API보다 약하므로, 다단계 편집 시 재시도가 더 많을 것으로 예상해야 합니다. 모델 태그는 변경되므로 명시적으로 고정하세요.

Wiring

플랫폼 스크립트(macOS/Linux에서는 curl | sh, Windows에서는 irm | iex) 또는 ollama/ollama Docker 이미지로 Ollama를 설치한 다음, `ollama run gemma4`를 한 번 실행해 가중치를 받고 데몬을 확인합니다. 서버는 http://localhost:11434에서 수신하며 /api/chat을 노출합니다. model, messages, stream:false를 포함한 비스트리밍 curl POST로 검증하세요. 클라이언트는 병렬로 연결됩니다. Cline은 IDE에서 로컬 Ollama 엔드포인트를 모델 제공자로 사용하도록 구성하고, Open WebUI는 별도 컨테이너 또는 프로세스로 실행되어 동일한 호스트 URL을 가리키며, 스크립트는 `pip install ollama` 또는 `npm i ollama`를 사용해 동일한 모델 태그로 chat()을 호출합니다. 데몬을 127.0.0.1에 바인딩하고, 공유 설정 파일에 모델 태그를 고정하며, 두 클라이언트 중 어느 것을 실행하기 전에 curl 스모크 테스트를 실행해 실패가 통합이 아닌 서버로 격리되도록 하세요.

Try it yourself

# Local-First AI Dev Stack with Ollama

I can't actually execute commands in your environment, but here's the complete set of files and commands you need. Run the verification steps first, then drop in the files below.

## Step 1 — Verify Ollama

Run this in your shell:

```bash
command -v ollama && curl -sf http://localhost:11434/api/tags >/dev/null && echo "OK" || echo "MISSING"
```

If it prints `MISSING`, install per OS and stop:

- **macOS:** `brew install ollama && brew services start ollama`
- **Linux:** `curl -fsSL https://ollama.com/install.sh | sh`
- **Windows:** `winget install Ollama.Ollama` (then launch the Ollama app)

## Step 2 — Pick a Gemma 4 tag for your VRAM

Check VRAM:

```bash
# NVIDIA
nvidia-smi --query-gpu=memory.total --format=csv,noheader
# Apple Silicon (unified memory)
sysctl hw.memsize
```

Suggested tag mapping (pin the exact tag you pull):

| VRAM / Unified RAM | Tag |
|---|---|
| ≥ 24 GB | `gemma4:27b` |
| 12–23 GB | `gemma4:12b` |
| 6–11 GB | `gemma4:4b` |
| < 6 GB | `gemma4:2b` |

Pull it:

```bash
ollama pull gemma4:12b   # replace with the tag that matches your VRAM
```

Record the exact tag in `docs/local-ai.md` (template below).

## Step 3 — Smoke scripts

### `scripts/smoke_chat.py`

```python
#!/usr/bin/env python3
"""Send one non-streaming chat message to the local Ollama daemon."""
import os
import sys

from ollama import Client

MODEL = os.environ.get("OLLAMA_MODEL", "gemma4:12b")  # keep in sync with docs/local-ai.md
HOST = os.environ.get("OLLAMA_HOST", "http://localhost:11434")

def main() -> int:
    client = Client(host=HOST)
    resp = client.chat(
        model=MODEL,
        messages=[{"role": "user", "content": "Reply with a single short sentence confirming you are running locally."}],
        stream=False,
    )
    print(resp["message"]["content"])
    return 0

if __name__ == "__main__":
    sys.exit(main())
```

### `scripts/smoke_chat.mjs`

```javascript
#!/usr/bin/env node
// Send one non-streaming chat message to the local Ollama daemon.
import ollama from "ollama";

const MODEL = process.env.OLLAMA_MODEL ?? "gemma4:12b"; // keep in sync with docs/local-ai.md
const HOST = process.env.OLLAMA_HOST ?? "http://localhost:11434";

const client = new ollama.Ollama({ host: HOST });

const resp = await client.chat({
  model: MODEL,
  messages: [
    { role: "user", content: "Reply with a single short sentence confirming you are running locally." },
  ],
  stream: false,
});

console.log(resp.message.content);
```

Install the client packages (do not touch app source):

```bash
pip install ollama
npm install --no-save ollama
```

## Step 4 — `docs/local-ai.md`

```markdown
# Local AI Dev Stack (Ollama)

This repo uses a local-first AI stack backed by [Ollama](https://ollama.com).
No application source depends on it; the docs and smoke scripts here are
opt-in tooling for developers who want a fully local model endpoint.

## Pinned model

- **Tag:** `gemma4:12b`  <!-- replace with the exact tag you pulled -->
- **Daemon:** `http://localhost:11434`

The tag above is the single source of truth. Update it here and in
`scripts/smoke_chat.py` / `scripts/smoke_chat.mjs` together.

## Pointing Cline at the local endpoint

1. Open Cline → **Settings → API Provider**.
2. Choose **Ollama**.
3. Set **Base URL** to `http://localhost:11434`.
4. Set **Model** to the pinned tag from this doc (e.g. `gemma4:12b`).
5. Save. Cline will now route completions to your local daemon.

If Cline is running inside a container or remote dev box, use
`http://host.docker.internal:11434` (Docker Desktop) or the host's LAN IP —
but see the security note below before exposing the port.

## Running Open WebUI against the same daemon

Open WebUI talks to the same Ollama daemon, so both tools share the loaded
model and VRAM budget.

```bash
docker run -d \
  --name open-webui \
  -p 3000:8080 \
  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
  -v open-webui:/app/backend/data \
  --add-host=host.docker.internal:host-gateway \
  ghcr.io/open-webui/open-webui:main
```

Then open <http://localhost:3000>. The pinned model tag will appear in the
model picker automatically.

## Model tag pinning policy

- **Always pin an explicit tag** (`gemma4:12b`), never `gemma4:latest`.
  `latest` can silently change under you and break reproducibility.
- **One tag per repo.** If you need a different size, add a new tag to this
  doc and update the smoke scripts in the same PR.
- **Record the tag in three places:** this doc, `scripts/smoke_chat.py`,
  `scripts/smoke_chat.mjs`. The `OLLAMA_MODEL` env var overrides all three
  for ad-hoc experiments but must not be committed.
- **Re-pull deliberately.** `ollama pull <tag>` is the only way to refresh a
  pinned tag; do it as an explicit step, not as part of CI.

## Security: keep port 11434 on localhost

The Ollama daemon has **no authentication**. Anyone who can reach
`http://<host>:11434` can run inference, list models, and pull new ones.

- Bind the daemon to `127.0.0.1` only. On Linux, set
  `OLLAMA_HOST=127.0.0.1:11434` in the systemd unit; on macOS/Windows the
  default is already localhost.
- **Never** expose 11434 to `0.0.0.0`, a LAN interface, or the public
  internet. Do not add it to a reverse proxy or tunnel.
- If a container needs access, use `host.docker.internal` (Docker Desktop)
  rather than publishing the port.
- Verify with: `ss -ltnp | grep 11434` — the address must be `127.0.0.1`.
```

## Step 5 — `make smoke`

Add to your `Makefile` (create it if absent; this does not touch app source):

```make
.PHONY: smoke
smoke:
	python3 scripts/smoke_chat.py
	node scripts/smoke_chat.mjs
```

Run it:

```bash
make smoke
```

Both scripts should print a one-line confirmation from the local model. If either fails, check that the daemon is up (`curl http://localhost:11434/api/tags`) and that the pinned tag is pulled (`ollama list`).

Source

English

Tools in this stack