로컬 Claude Code / Codex CLI 구독을 토대로, 메터드 API 0원의 완결형 로컬 AI 스택을 repo 하나로 제공합니다. 모든 것이 로컬·독립 실행(중앙 서버·공유 계정 의존 0)이며, OpenAI·Anthropic 호환 API + 임베딩 + mem0 메모리 + second-brain RAG + MCP를 한 번에 제공합니다.
- LLM API — OpenAI(
/v1/chat/completions)·Anthropic(/v1/messages) 호환. 기존 코드의base_url만 바꾸면 됨. - 임베딩 — 로컬 bge-m3 (OpenAI
/v1/embeddings호환) - 메모리 — mem0 (진화하는 사실 기억)
- second-brain — 내 마크다운 노트에 대한 RAG
- MCP 도구 — Cursor/Claude Desktop/Cline에서 위 기능을 도구로 사용
🧩 처음 보는 용어가 많다면 — 게이트웨이·임베딩·RAG·MCP를 딱 하나의 비유(내 컴퓨터 안의 1인 비서실)로 5분 만에 풀어주는 입문서부터: 👉 비유로 이해하기
독립 실행 원칙: 각 인스턴스는 완전히 자립적입니다. 그 머신의 로컬 스택 (gateway·임베딩·메모리·brain) + 그 머신 자신의
claude/codex로그인 + localhost로만 동작하며, 중앙 homeserver나 공유 계정에 의존하지 않습니다(단일 장애점·ToS 회피). 여러 명/여러 서버는 각자 독립 인스턴스로 돌리고, 필요하면 원격 MCP(URL+토큰)로 접속만 합니다 — 추론은 항상 각 인스턴스의 자기 계정으로. 진화 방향은 ROADMAP.md.
HTTP API ┬─ /v1/chat/completions · /v1/messages → claude/codex CLI
├─ /v1/embeddings → bge-m3
└─ OpenMemory REST → mem0 + pgvector
MCP ───── ask · remember/recall · capture_note/search_notes/ask_brain
- Node.js ≥ 20, Docker
- 호스트에 로그인된
claude/codexCLI
git clone https://github.com/shaul1991/localmind && cd localmind
make install build # 의존성 + dist 빌드(로컬 MCP용)
make up # Docker 스택 기동(게이트웨이+메모리)
# chat :8787 · 게이트웨이 :4000 · 메모리 :8767 (최초 빌드/모델 pull은 수 분)운영은 전부
make로 일관됩니다:make up(기동) ·make health(점검) ·make logs·make down. 전체 목록은make help. (아래 문서의docker compose --profile ...는 make가 실행하는 내부 명령으로, 세분 제어가 필요할 때 참고)
curl http://localhost:8787/v1/chat/completions -H "Content-Type: application/json" \
-d '{"model":"sonnet","messages":[{"role":"user","content":"안녕"}]}'OpenAI/Anthropic SDK는 base_url만 위 주소로 바꾸면 그대로 동작 → 사용 예시.
Cursor .cursor/mcp.json / Claude Desktop claude_desktop_config.json / Cline MCP 설정에:
{ "mcpServers": { "localmind": {
"command": "node",
"args": ["/절대경로/localmind/dist/mcp.js"],
"env": { "NOTES_DIR": "/내/노트/폴더", "OPENMEMORY_USER": "내이름" }
}}}→ 호스트가 ask·remember/recall·capture_note/search_notes/ask_brain 도구를 갖습니다.
NOTES_DIR를 기존 .md 노트 폴더(예: second-brain-mesh)로 가리키면 그 지식으로 바로 RAG.
make smoke # API + MCP + brain 스모크 한 번에
make health # 엔드포인트 상태(:8787 / :4000 / :8767)더 가볍게: 채팅 API만 쓰려면
docker compose up -d --build(게이트웨이/메모리 프로파일 생략 — 이 경우만 raw).
모든 인스턴스는 각자 독립으로 동작합니다(중앙 서버·공유 계정 의존 0). 역할에 맞는 경로만 따라가세요.
① 개인 — 내 PC에서 바로 쓰기
git clone https://github.com/shaul1991/localmind && cd localmindmake install build && make up(전제: 호스트에claude/codex로그인)make health로 확인 → Cursor/Claude Desktop에 MCP 설정(아래 MCP 서버 섹션)NOTES_DIR를 내 노트 폴더로 가리키면 그 지식으로 바로 RAG
② 인프라 운영 — 서버별로 관리
- 서버마다
MCP_INSTANCE=서버명으로 독립 인스턴스 → 그 서버의 자원/메모리/노트를 로컬에서 관리 .env에MCP_HTTP_TOKEN설정 후make up-mcp→ 노트북에서 서버별 원격 MCP로 접속 (아래 디바이스/서버별 관리)
③ 팀원 — 이미 떠 있는 서버에 붙기
- 운영자에게 받은 URL + 토큰을 클라이언트에 원격 MCP로 등록 (아래 원격 MCP)
- 추론은 그 인스턴스의 계정으로 수행 — 내 코딩 구독(Cursor/Claude/Codex)은 그대로 따로 씀
어떤 경로든 메터드 API 0원, 데이터는 그 인스턴스 로컬에만 둡니다.
📂 바로 따라 할 케이스별 예제는 examples/ — API 대체·임베딩·메모리·second-brain·MCP까지. 👥 내 직군에선 어떻게? → 직군별 유즈케이스 — 개발(백엔드·프론트·앱·게임), 데이터/ML, QA·아키텍트·인프라, 비개발(PM·라이터·보안·연구자), 콘텐츠 크리에이터(AI 글작성·유튜브 대본/편집·썸네일·인플루언서) 등 19개 페르소나.
make up 후 내 직군 스크립트 하나만 돌리면 localmind 활용이 한 번에 체감됩니다. 전부 실행·검증됨.
| 그룹 | 바로 실행할 워크플로우 |
|---|---|
| 개발 | 백엔드 · 프론트 · 앱/모바일 · 게임 |
| 데이터/ML | ML 엔지니어 · 데이터 분석 |
| 품질·설계·운영 | QA · 아키텍트 · 인프라/SRE |
| 비개발 | PM · 테크니컬 라이터 · 보안 · 연구자 |
| 콘텐츠 | AI 글작성 · 유튜브 대본 · 유튜브 편집 · 썸네일 · 인플루언서 |
| 1인 개발 | 풀스택 투어 |
각 직군의 상황 → 활용 → 워크플로우 단계 → 효과는 👉 직군별 유즈케이스(19 페르소나). 모든 예제는 examples/.
실행 중인 스택에 그대로 태운 두 직군. 응답·저장 데이터 모두 실제 출력입니다.
백엔드 개발자 — 로그 요약 → 분류(perf) → 대응 결정을 노트로 적재 → NOTES_DIR에 .md 정본이 쌓임(이후 검색·RAG 대상)
기획자(PM) — 결정을 remember로 mem0에 저장 → PRD 초안 → 2주 뒤 recall로 의미 회상(결정·이유가 기억으로 쌓임)
- OpenAI 형식 요청(
messages,stream, ...)을 받습니다. model필드로 백엔드를 결정합니다 (claude vs codex).messages를 시스템 프롬프트 + 단일 프롬프트로 평탄화해 해당 CLI를-p/exec비대화형 모드로 실행합니다.- CLI의 JSON/스트리밍 출력을 OpenAI 형식(
chat.completion/chat.completion.chunkSSE)으로 변환해 돌려줍니다.
CLI는 순수 텍스트 생성기로 동작합니다 — claude는 --tools ""로 모든 내장 도구를 끄고, codex는 -s read-only로 격리하며, 둘 다 임시 디렉토리에서 실행해 프로젝트 설정(CLAUDE.md 등)이 섞이지 않습니다.
- Node.js >= 20
- 로그인된
claudeCLI (claude가 PATH에 있어야 함) - 로그인된
codexCLI (codex 백엔드를 쓸 경우)
make install # 의존성
make dev # 개발 모드(watch, 코드 변경 시 자동 재시작)
make build && npm start # 빌드 후 로컬 실행(비-Docker; npm start만 make 미대상)기본적으로 http://127.0.0.1:8787 에서 대기합니다. 설정은 환경변수로 변경합니다 (.env.example 참고).
이미지에는 claude·codex CLI가 함께 설치됩니다. 인증은 호스트의 CLI 인증 디렉토리를 볼륨 마운트해 재사용합니다(별도 로그인 불필요).
전제: 호스트에서
claude/codex에 이미 로그인되어 있어야 합니다(~/.claude,~/.codex).
docker compose up -d --build # 채팅 API만(프로파일 생략) · 전체 스택은 `make up`
make healthdocker-compose.yml이 호스트의 ~/.claude, ~/.claude.json, ~/.codex를 컨테이너로 마운트합니다.
docker build -t localmind .
docker run -d --name localmind -p 8787:8787 \
-v "$HOME/.claude:/root/.claude" \
-v "$HOME/.claude.json:/root/.claude.json" \
-v "$HOME/.codex:/root/.codex" \
localmind주의사항
- 컨테이너 안의 CLI는 호스트와 동일한 계정/인증/상태를 공유합니다. 호스트에서 같은 CLI를 동시에 무겁게 쓰면 상태(히스토리·토큰 갱신 등)가 섞일 수 있습니다.
- 컨테이너 내부는
HOST=0.0.0.0으로 바인딩해야 외부에서 접근됩니다(이미지 기본값). - claude는 glibc 네이티브 바이너리라 베이스 이미지는 Debian 계열(
node:24-slim)을 사용합니다(alpine/musl 비호환). - 인증 토큰 갱신을 컨테이너가 호스트 파일에 다시 쓰므로 볼륨은 읽기·쓰기로 마운트합니다.
localmind는 채팅(생성)만 다룹니다 — CLI는 임베딩(텍스트→벡터)을 못 하기 때문입니다. 그래서 임베딩이 필요한 소비자(예: supermemory 같은 메모리/RAG 시스템)를 위해, 임베딩 서버(Ollama+bge-m3) 와 통합 게이트웨이(LiteLLM) 를 opt-in 프로파일로 함께 제공합니다.
소비자 ──(base URL 하나)──▶ LiteLLM 게이트웨이 (:4000)
├─ /v1/embeddings → ollama (bge-m3)
└─ /v1/chat/completions → localmind → claude/codex CLI
소비자는 base URL 하나(http://<host>:4000/v1)만 바라보면, 임베딩은 로컬 모델로·채팅은 CLI 구독으로 자동 분기됩니다.
docker compose --profile gateway up -d --build # 임베딩만(메모리 제외); 메모리까지 한 번에: make up
# 최초 1회: ollama가 bge-m3(~1.2GB)를 자동 pull (수 분 소요)올라오는 서비스:
| 서비스 | 포트 | 역할 |
|---|---|---|
litellm |
4000 | 통합 게이트웨이 (소비자가 바라보는 단일 엔드포인트) |
localmind |
8787 | 채팅 (claude/codex CLI) |
ollama |
(내부) | 임베딩 백엔드 (bge-m3) |
OpenAI 호환 클라이언트의 base URL/key만 게이트웨이로 바꿉니다.
OPENAI_BASE_URL=http://<host>:4000/v1
OPENAI_API_KEY=sk-local # LITELLM_MASTER_KEY 값
EMBEDDING_MODEL=text-embedding-3-small # 또는 bge-m3 (둘 다 bge-m3로 매핑됨)from openai import OpenAI
c = OpenAI(base_url="http://localhost:4000/v1", api_key="sk-local")
c.embeddings.create(model="text-embedding-3-small", input="의미 검색용 텍스트") # → ollama bge-m3 (1024차원)
c.chat.completions.create(model="claude-sonnet-4-6", # → localmind → claude
messages=[{"role": "user", "content": "안녕"}])- 임베딩 모델명(
text-embedding-3-small/large,ada-002,bge-m3) → ollama bge-m3 - 그 외 모든 모델 → localmind (모델명으로 claude/codex 라우팅)
- 다른 임베딩 모델명을 쓰면
litellm.config.yaml의model_list에 한 줄 추가하세요(없으면 채팅으로 잘못 라우팅됨).
- 임베딩 모델 일관성: 색인과 쿼리는 같은 임베딩 모델이어야 합니다. 모델을 바꾸면 전체 재색인이 필요합니다(벡터 차원도 bge-m3=1024로 맞출 것).
EMBEDDING_MODEL/LITELLM_MASTER_KEY는.env로 바꿀 수 있습니다.- GPU가 있고 대량 색인이면 ollama 대신 HF TEI/Infinity 같은 임베딩 전용 서버로
litellm.config.yaml의api_base만 바꿔도 됩니다.
게이트웨이 위에 OpenMemory(mem0) 를 얹어, 메터드 API 0원으로 동작하는 메모리/RAG 서비스를 바로 띄울 수 있습니다. OpenMemory의 LLM(사실 추출)·임베더를 모두 게이트웨이로 돌려 claude(또는 codex) + bge-m3로 동작합니다.
OpenMemory(:8767) ──▶ LiteLLM 게이트웨이
│ add/list/search ├─ 임베딩 → ollama(bge-m3)
▼ └─ 추출 LLM → localmind → claude/codex CLI
Postgres + pgvector (메타데이터 + 벡터)
OpenMemory는 게시 이미지가 pgvector 미지원 + 읽기 버그가 있어, 최신 소스를 빌드하고 localmind 패치(
openmemory/)를 적용합니다. 저장소는 Postgres+pgvector 단일 DB로 통합합니다.
make up # = docker compose --profile gateway --profile memory up -d --build
# 최초 1회: OpenMemory 소스 빌드 + bge-m3(~1.2GB) pull (수 분)
# openmemory-init 사이드카가 자동으로:
# - pgvector 테이블을 임베딩 차원(bge-m3=1024)에 맞춰 선생성
# - LLM/임베더 모델을 게이트웨이 라우팅에 맞게 설정올라오는 서비스: openmemory(:8767 REST), mem0_pg(Postgres+pgvector), openmemory-init(일회성 부트스트랩) + gateway 일체.
# 추가: claude가 사실을 추출해 저장 — user_id는 OPENMEMORY_USER 값
curl -X POST http://localhost:8767/api/v1/memories/ \
-H "Content-Type: application/json" \
-d '{"user_id":"localmind","text":"내 강아지 초코는 오이를 간식으로 좋아한다.","infer":true}'
# 목록 (키워드+최신순)
curl "http://localhost:8767/api/v1/memories/?user_id=localmind&size=20"
# 검색 (키워드 필터)
curl -X POST http://localhost:8767/api/v1/memories/filter \
-H "Content-Type: application/json" \
-d '{"user_id":"localmind","search_query":"강아지","size":10}'의미 기반 회상(임베딩 벡터 검색)은 mem0 엔진으로:
docker exec localmind-openmemory python -c "
from app.utils.memory import get_memory_client
print(get_memory_client().search('반려견 간식', filters={'user_id':'localmind'}, limit=3))"
# → 의미 매칭 결과 (점수 높은 순)- ✅ 쓰기: REST 추가 → claude 사실 추출 → bge-m3 임베딩(1024) → pgvector 저장.
- ✅ 읽기:
GET /memories(목록)·POST /memories/filter(검색) 정상 동작 (소스 패치로 업스트림 읽기 버그 해소). - ✅ 의미 회상: mem0 엔진
search()가 게이트웨이로 임베딩해 의미 기반 검색. user_id는OPENMEMORY_USER로 시드된 사용자여야 합니다(임의 id는 "User not found").- 자동 카테고리화(categorization)는 OpenAI 구조화 출력(json_schema)을 요구하는데 CLI 경로에선 강제가 안 되고 재시도가 워커를 막아 비활성화했습니다(메모리 기능엔 영향 없음).
- mem0 사실 추출 프롬프트는 기본이 영어("User owns ...")라,
openmemory/patch.py에서 입력과 같은 언어 + 주어 생략으로 패치했습니다 → 한국어 입력은 "매일 아침 6시에 기상한다"처럼 자연스러운 한국어로 저장됩니다. - OpenMemory는 단일 워커라 동시 add가 몰리면 직후 요청이 잠깐 밀릴 수 있습니다.
- 임베딩 모델을 바꾸면
EMBEDDING_DIMS도 맞추고make clean(볼륨 삭제)으로 초기화하세요(차원이 테이블에 고정됨).
mem0 메모리는 Postgres에 있어 그대로는 git-native가 아닙니다. 마크다운으로 export해 git에 올려 백업합니다(메모리 1개당 한 줄 → diff가 깔끔). 노트(.md)는 이미 파일이라 그 자체로 git 백업되니, 이 둘이면 두뇌 전체가 git에 들어갑니다.
# 내보내기 (마크다운)
OPENMEMORY_USER=내이름 make memory-export FILE=~/brain/memory.md
# 백업: 노트 폴더(또는 백업 repo)에서 커밋·푸시
cd ~/brain && git add memory.md && git commit -m "memory backup" && git push
# 복원 (멱등 — 이미 있는 내용은 스킵)
OPENMEMORY_USER=내이름 make memory-import FILE=~/brain/memory.md- export 출력은
- <사실>한 줄씩이라 메모리 추가/삭제가 git diff에 1줄로 보입니다. - import는
infer:false로 사실을 그대로 저장하고, 이미 있는 내용은 건너뜁니다(여러 번 실행 안전). - 파생물(pgvector/
.brain-index.json)은 백업 불필요 — 노트·메모리에서 재생성됩니다.
localmind의 능력을 MCP 도구로 노출해, MCP 호스트(Claude Desktop / Cursor / Cline 등)가 자기 모델로 돌면서 끌어 쓰게 합니다. (MCP는 호스트의 모델을 바꾸는 게 아니라 도구를 줍니다.)
| 도구 | 설명 |
|---|---|
whoami |
이 인스턴스(디바이스/서버) 식별 — 어느 서버의 메모리/노트인지 |
ask |
claude/codex CLI에 교차 질의(다른 모델 상담) → localmind 경유 |
remember |
진화하는 기억에 사실 저장 (mem0: claude 추출 + bge-m3) |
recall |
의미 기반 회상 (mem0 벡터 검색) |
capture_note |
second-brain: 마크다운 노트 저장 + 인덱싱 (정본은 .md) |
search_notes |
second-brain: 내 노트 의미검색(원문·경로) |
ask_brain |
second-brain: 내 노트만 근거로 RAG 답변(출처 인용) |
두 종류의 기억:
remember/recall은 mem0의 진화하는 사실 메모리,capture_note/search_notes/ask_brain은 내 마크다운 노트(정본) RAG 입니다.
MCP 호스트(Claude Desktop/Cursor/Cline)
│ stdio
▼
localmind MCP 서버 (dist/mcp.js)
├─ ask → localmind :8787 (claude/codex)
└─ remember/recall → OpenMemory :8767 (pgvector)
make install build # dist/mcp.js 생성
make up # 스택 기동(게이트웨이+메모리)
# - ask는 gateway 스택(:8787)만 있으면 동작
# - remember/recall은 memory 스택(:8767)도 필요Claude Desktop claude_desktop_config.json (Cursor .cursor/mcp.json, Cline MCP 설정도 동일 구조):
{
"mcpServers": {
"localmind": {
"command": "node",
"args": ["/절대경로/localmind/dist/mcp.js"],
"env": { "OPENMEMORY_USER": "내이름" }
}
}
}stdio는 각자 로컬 서브프로세스라 "URL 공유"가 안 됩니다. 원격 서버(dist/mcp-http.js)는
URL 하나로 Claude Code / Cursor / ChatGPT 원격 connector 까지 접속하게 합니다.
# .env 에 MCP_HTTP_TOKEN(필수) + MCP_INSTANCE 설정 후
make up-mcp # = docker compose --profile gateway --profile memory --profile mcp up -d --build
# → http://<host>:8788/mcp (Streamable HTTP), http://<host>:8788/sse (레거시)POST /mcp— Streamable HTTP(현대 표준),GET /sse+POST /messages— 레거시 SSE,GET /health.- 인증 필수: 네트워크 노출이므로
MCP_HTTP_TOKEN없으면 기동 거부. 클라이언트는Authorization: Bearer <토큰>. - tailnet 등 신뢰 네트워크에만 노출하세요(포트 8788).
클라이언트 등록 예:
# Claude Code
claude mcp add --transport http my-server https://<host>:8788/mcp --header "Authorization: Bearer <토큰>"한 인스턴스 = 한 디바이스/서버. 서버마다 MCP_INSTANCE를 다르게 주면 그 서버의
메모리·노트가 인스턴스별로 격리됩니다. 각 서버가 자기 자원 정보(스펙·설정·장애 이력)를
로컬에서 관리하고, 클라이언트에선 서버별 원격 MCP를 각각 등록해 골라 씁니다.
db-server : MCP_INSTANCE=db-server → db-server의 메모리/노트 (http://db-server:8788/mcp)
app-server-1: MCP_INSTANCE=app-server-1 → app-server-1의 메모리/노트
- 각 인스턴스는 독립: 그 서버의 localmind + 그 서버의 CLI 로그인을 쓰므로, 한 서버가 다른 서버의 계정/스택에 의존하지 않습니다(중앙 의존 0).
- 메모리 격리:
OPENMEMORY_USER를 안 주면MCP_INSTANCE가 곧 메모리 owner가 됩니다. whoami도구로 "지금 어느 서버의 두뇌인지" 즉시 확인.- 각 서버에서
capture_note로 그 서버의 자원/장애를 기록 →ask_brain으로 그 서버 한정 RAG.
| 변수 | 기본값 | 설명 |
|---|---|---|
MCP_INSTANCE |
호스트명 | 디바이스/서버 식별자(메모리 owner 기본값) — 서버별 격리 |
MCP_HTTP_TOKEN |
(없음) | 원격 MCP Bearer 토큰. 네트워크 노출 시 필수 |
MCP_HTTP_PORT / MCP_HTTP_HOST |
8788 / 0.0.0.0 |
원격 MCP 바인딩 |
LOCALMIND_URL |
http://localhost:8787 |
ask가 호출할 localmind |
LOCALMIND_API_KEY |
(없음) | localmind 인증 시 |
OPENMEMORY_URL |
http://localhost:8767 |
remember/recall 대상 |
OPENMEMORY_USER |
localmind |
메모리 소유자 id |
MCP_DEFAULT_MODEL |
sonnet |
ask / ask_brain 기본 모델 |
NOTES_DIR |
~/localmind-brain |
second-brain 노트 폴더(정본). 내 노트 경로로 지정 |
BRAIN_INDEX |
<NOTES_DIR>/.brain-index.json |
임베딩 인덱스 위치. git/싱크 볼트를 안 더럽히려면 밖으로 |
EMBEDDINGS_URL |
http://localhost:4000/v1 |
노트 임베딩(게이트웨이) |
EMBEDDINGS_MODEL |
text-embedding-3-small |
(게이트웨이가 bge-m3로 매핑) |
BRAIN_BATCH / BRAIN_CONCURRENCY / BRAIN_CHUNK_SIZE |
32 / 4 / 2000 |
인덱싱 튜닝 |
인덱싱 성능: 첫 인덱싱은 노트 전체를 임베딩하므로 시간이 걸리고(증분·resumable이라 이후엔 변경분만), bge-m3 CPU 임베딩이 바닥입니다. 더 빠르게:
- bge-m3 그대로 + GPU/전용 임베딩 서버(TEI/Infinity) 로
EMBEDDINGS_URL교체 (한국어 품질 유지하며 가속, 권장)- 더 가벼운 모델로 바꾸려면 반드시 다국어 모델(multilingual-e5, snowflake-arctic-embed2)을 —
nomic-embed-text등 영어 전용 모델은 한국어 검색 품질이 크게 떨어집니다.- 모델 변경 시 벡터 차원이 달라지므로
EMBEDDING_DIMS조정 + 재인덱싱 필요.
검증: make smoke(MCP 포함 일괄) 또는 원격 MCP는 npm run smoke:mcp:http.
🧩 케이스별 runnable 예제 모음 → examples/ — API 대체(Python/Node/Anthropic), 함수호출, 임베딩·의미검색, 메모리, second-brain, LangChain, MCP(Cursor/Claude/Codex·원격 인프라), 역할별 시나리오. 전부 실행·검증됨.
curl http://127.0.0.1:8787/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "sonnet",
"messages": [{"role": "user", "content": "안녕하세요"}]
}'from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8787/v1",
api_key="not-needed", # LOCALMIND_API_KEY를 설정했다면 그 값
)
resp = client.chat.completions.create(
model="sonnet", # → claude 백엔드
messages=[{"role": "user", "content": "한 줄로 자기소개 해줘"}],
)
print(resp.choices[0].message.content)
# 스트리밍
for chunk in client.chat.completions.create(
model="gpt-5.5", # → codex 백엔드
messages=[{"role": "user", "content": "1부터 5까지 세줘"}],
stream=True,
):
print(chunk.choices[0].delta.content or "", end="")import OpenAI from "openai";
const client = new OpenAI({ baseURL: "http://127.0.0.1:8787/v1", apiKey: "not-needed" });
const res = await client.chat.completions.create({
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: "hello" }],
});OpenAI뿐 아니라 공식 Anthropic SDK도 그대로 붙습니다. base_url만 localmind로 바꾸면 됩니다.
from anthropic import Anthropic
client = Anthropic(
base_url="http://127.0.0.1:8787",
api_key="not-needed", # LOCALMIND_API_KEY를 설정했다면 그 값 (x-api-key 헤더로 전송됨)
)
msg = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system="간결하게 답해줘",
messages=[{"role": "user", "content": "한 줄로 자기소개 해줘"}],
)
print(msg.content[0].text)
# 스트리밍
with client.messages.stream(
model="sonnet",
max_tokens=256,
messages=[{"role": "user", "content": "1부터 5까지 세줘"}],
) as stream:
for text in stream.text_stream:
print(text, end="")
model에codex:gpt-5.5처럼 지정하면 Anthropic 포맷으로 codex 백엔드를 호출할 수도 있습니다.
model 필드로 백엔드와 실제 모델을 결정합니다.
입력 model |
백엔드 | CLI 모델 |
|---|---|---|
sonnet, opus, haiku, claude-* |
claude | 그대로 전달 |
gpt-*, o1/o3/o4-*, codex, gpt-5.5 |
codex | 그대로 전달 |
claude:<모델> |
claude (강제) | <모델> |
codex:<모델> |
codex (강제) | <모델> |
anthropic/<모델>, openai/<모델> |
패턴 매칭 | 프리픽스 제거 후 전달 |
| 그 외/빈 값 | DEFAULT_BACKEND |
백엔드 기본 모델 |
codex의 사용 가능 모델은 로그인 계정에 따라 다릅니다. 계정이 지원하지 않는 모델을 지정하면 400 오류가 반환됩니다. 기본값은
~/.codex/config.toml의 모델과 맞춰 두는 것을 권장합니다.
| 메서드 | 경로 | 포맷 | 설명 |
|---|---|---|---|
| POST | /v1/chat/completions |
OpenAI | 채팅 완성 (스트리밍/비스트리밍) |
| POST | /v1/messages |
Anthropic | 메시지 (스트리밍/비스트리밍) |
| GET | /v1/models |
OpenAI | 모델 목록 |
| GET | /health |
— | 헬스체크 (인증 불필요) |
인증(LOCALMIND_API_KEY 설정 시)은 OpenAI식 Authorization: Bearer <키> 와 Anthropic식 x-api-key: <키> 헤더를 모두 허용합니다.
OpenAI/Anthropic API는 stateless라 보통 매 요청마다 전체 대화 히스토리를 다시 보냅니다. localmind는 대화를 CLI 세션에 매핑해, 이어지는 요청에서는 claude --resume / codex exec resume로 새 턴만 전송합니다. 결과적으로 CLI 측 컨텍스트와 프롬프트 캐시를 활용해 토큰을 아낍니다.
SESSION_MODE로 동작을 정합니다.
| 모드 | 동작 |
|---|---|
auto (기본) |
메시지 prefix를 해시로 자동 인식. 클라이언트 코드 변경 불필요 — 일반적인 "히스토리를 계속 append하는" 채팅이면 자동으로 이어집니다. |
explicit |
x-localmind-session 헤더(또는 session_id/user/metadata.user_id 필드)가 있을 때만 해당 id로 세션을 잇습니다. 가장 견고합니다. |
off |
세션 없이 항상 전체 히스토리 전송. |
# explicit 모드 예: 같은 세션 id로 요청하면 컨텍스트가 이어짐
curl http://127.0.0.1:8787/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-localmind-session: my-convo-1" \
-d '{"model":"sonnet","messages":[{"role":"user","content":"내 이름은 지훈이야"}]}'auto 모드 주의점
- 클라이언트가 우리가 준 assistant 응답을 그대로 다시 보낼 때 prefix가 일치해 이어집니다. 응답을 편집/요약해서 보내면 매칭이 깨지고, 이 경우 안전하게 전체 히스토리로 새 세션을 다시 만듭니다(틀린 답이 나오지는 않고, 토큰 절약만 사라짐).
- 재생성(regeneration)·분기 안전: 같은 prefix가 두 번 오면 첫 번째만 resume하고(consume-once), 두 번째는 fresh로 복구해 세션 오염을 막습니다.
- 세션 매핑은 인메모리이며 서버 재시작 시 사라집니다(
SESSION_TTL_MS경과 시에도 만료). 그래도 컨텍스트는 항상 클라이언트가 보낸 히스토리로 복구 가능합니다.
OpenAI(/v1/chat/completions)와 Anthropic(/v1/messages) 양쪽 모두 함수 호출을 지원합니다. 모델이 함수 호출이 필요하다고 판단하면 각 포맷의 표준 응답(OpenAI tool_calls / Anthropic tool_use 블록)을 돌려줍니다. 공식 OpenAI/Anthropic SDK의 함수 호출 흐름이 그대로 동작합니다.
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
},
}]
# 1) 모델이 도구 호출을 결정
r = client.chat.completions.create(model="sonnet", tools=tools,
messages=[{"role": "user", "content": "서울 날씨 알려줘"}])
call = r.choices[0].message.tool_calls[0] # get_weather({"city":"서울"})
# 2) 함수를 실행하고 결과를 다시 전달 → 모델이 최종 답변
r2 = client.chat.completions.create(model="sonnet", tools=tools, messages=[
{"role": "user", "content": "서울 날씨 알려줘"},
r.choices[0].message,
{"role": "tool", "tool_call_id": call.id, "content": "맑음, 25도"},
])
print(r2.choices[0].message.content)Anthropic 포맷(tools는 {name, description, input_schema}, 결과는 user 메시지의 tool_result 블록)도 그대로 지원합니다.
tools = [{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
}]
r = client.messages.create(model="sonnet", max_tokens=1024, tools=tools,
messages=[{"role": "user", "content": "서울 날씨 알려줘"}])
tool_use = next(b for b in r.content if b.type == "tool_use") # get_weather(input={"city":"서울"})
r2 = client.messages.create(model="sonnet", max_tokens=1024, tools=tools, messages=[
{"role": "user", "content": "서울 날씨 알려줘"},
{"role": "assistant", "content": r.content},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_use.id, "content": "맑음, 25도"}]},
])
print(r2.content[0].text)작동 방식 (A2): CLI에는 "외부가 실행할 함수 스펙을 받아 호출만 내뱉고 멈추는" 모드가 없으므로, tools 스펙을 시스템 프롬프트에 주입하고 모델이 약속된 JSON({"tool_calls":[...]})으로 출력하게 한 뒤 그 텍스트를 파싱해 각 포맷의 tool_calls/tool_use로 변환합니다.
한계 (PoC)
- 프롬프트 기반이라 100% 보장은 아님: 모델이 형식을 벗어나면 도구 호출로 인식되지 않고 일반 텍스트로 처리됩니다.
- 스트리밍 시 버퍼링: 도구 호출 여부는 전체 출력을 봐야 알 수 있어,
tools가 있으면 전체를 모은 뒤 한 번에 방출합니다(토큰 단위 스트리밍 아님).tools가 없으면 기존대로 실시간 스트리밍. - 백엔드 특성 차이: claude는
--tools ""로 순수 텍스트화되어 주입한 도구 결과를 충실히 따릅니다. codex는 더 에이전트적이라 자체 도구(웹 검색 등)를 써서 주입한 결과와 다른 답을 낼 수 있습니다. - 멀티스텝 도구 루프에서 컨텍스트 유지는 세션 영속화를 따릅니다(매칭 안 되면 전체 히스토리로 복구).
| 변수 | 기본값 | 설명 |
|---|---|---|
PORT |
8787 |
포트 |
HOST |
127.0.0.1 |
바인딩 호스트 |
LOCALMIND_API_KEY |
(없음) | 설정 시 Authorization: Bearer <키> 필수 |
DEFAULT_BACKEND |
claude |
라우팅 실패 시 기본 백엔드 |
CLAUDE_DEFAULT_MODEL |
sonnet |
claude 기본 모델 |
CODEX_DEFAULT_MODEL |
gpt-5.5 |
codex 기본 모델 |
CLAUDE_BIN |
claude |
claude 실행 파일 경로 |
CODEX_BIN |
codex |
codex 실행 파일 경로 |
REQUEST_TIMEOUT_MS |
300000 |
요청 타임아웃(ms) |
SESSION_MODE |
auto |
세션 영속화 모드 (off/explicit/auto) |
SESSION_TTL_MS |
3600000 |
세션 매핑 보관 시간(ms) |
SESSION_MAX |
1000 |
세션 매핑 최대 개수 |
LOG_LEVEL |
info |
debug/info/warn/error |
기본 묶음은 make smoke(API+MCP+brain). 아래는 백엔드·엔드포인트별 세분 변형으로, make 타겟이 없어 npm run을 직접 씁니다.
make dev # 다른 터미널에서 서버 실행
# OpenAI 엔드포인트(/v1/chat/completions)
MODEL=sonnet npm run smoke # claude 백엔드
MODEL=gpt-5.5 npm run smoke # codex 백엔드
# Anthropic 엔드포인트(/v1/messages)
MODEL=sonnet npm run smoke:anthropic # claude 백엔드
MODEL=codex:gpt-5.5 npm run smoke:anthropic # codex 백엔드
# 함수 호출
MODEL=sonnet npm run smoke:tools # OpenAI tools
MODEL=sonnet npm run smoke:anthropic:tools # Anthropic tool_use| 증상 | 원인 / 해결 |
|---|---|
| 채팅 호출이 실패/빈 응답 | 호스트에 claude/codex가 로그인 안 됨 → 호스트에서 한 번 claude(또는 codex) 실행해 로그인. 컨테이너는 ~/.claude·~/.codex를 마운트해 재사용. |
make up-mcp가 바로 실패 |
.env에 MCP_HTTP_TOKEN 미설정 (네트워크 노출 안전장치). 값을 채우고 다시. |
make health에서 일부 000/비정상 |
첫 기동은 모델 pull(bge-m3 ~1.2GB) + OpenMemory 소스 빌드로 수 분 걸림 → make logs로 진행 확인. 부하 중 임베딩이 멈추면 make restart. |
| 포트 충돌(8787/4000/8767/8788) | 이미 쓰는 포트면 .env/compose에서 변경하거나 충돌 프로세스 정지. |
메모리 User not found |
user_id는 시드된 사용자(OPENMEMORY_USER/MCP_INSTANCE)여야 함. 임의 id 불가. |
| 노트 첫 인덱싱이 느림 | bge-m3 CPU 임베딩이 바닥(이후 증분이라 빠름). 급하면 GPU/TEI로 EMBEDDINGS_URL 교체. 한국어면 다국어 모델만(nomic 등 영어 전용 금지). |
| 임베딩 모델 변경 후 차원 오류 | EMBEDDING_DIMS 맞추고 make clean(볼륨 초기화) 후 재기동. |
GitHub Actions(.github/workflows/ci.yml)에서 push/PR마다 다음을 검증합니다.
- typecheck & build: Node 20·22·24 매트릭스로
npm ci → typecheck → build - docker build: 이미지가 정상 빌드되는지 확인
스모크 테스트(
npm run smoke*)는 인증된claude/codexCLI가 필요해 CI에서는 실행하지 않습니다. 로컬에서 서버를 띄운 뒤 수동으로 돌리세요.
- Function calling / tools: OpenAI·Anthropic 양쪽 엔드포인트에서 A2 프롬프트 방식 PoC로 지원.
- 멀티모달: 이미지 등 비텍스트 입력은 자리표시자로 치환됩니다.
- 대화 맥락: 세션 영속화(위 참고)로 CLI 세션을 resume합니다. 매칭이 안 되거나
SESSION_MODE=off면 멀티턴messages를User:/Assistant:라벨로 평탄화해 전체 전달합니다. - 토큰 수: CLI가 보고하는 값을 그대로 전달하므로, CLI 내부 시스템 프롬프트 토큰이
prompt_tokens(input_tokens)에 포함됩니다. temperature,max_tokens등 일부 샘플링 파라미터는 CLI가 지원하지 않아 무시될 수 있습니다.- Anthropic 스트리밍:
input_tokens는 스트림 시작 시점(message_start)에 0으로 보내고, 최종message_delta에서 실제 값을 채웁니다(SDK가 이를 합산해 최종 usage를 계산).
실제 실행 중인 스택의 세션입니다 — make health → OpenAI 호환 chat(claude) → 로컬 임베딩(bge-m3). 전부 메터드 API 0원, 완전 로컬. (응답·임베딩 값 모두 실제 출력)
직접 보려면
make up후 위 명령들을 그대로 실행하면 됩니다.


