로컬 Gemma를 Claude Code 백엔드로 쓰기
로컬 Gemma를 Claude Code 백엔드로 쓰기
ollama 없이, 파이썬 FastAPI 서버만으로 Gemma를 띄우고 LAN의 어떤 PC에서든 Claude Code가 이를 백엔드로 사용하게 하는 방법입니다.
전체 구조
[Host PC - Windows] gemma_server.py (FastAPI + transformers) └─ /v1/messages ← Anthropic Messages API └─ port 8000 → LAN 전체 허용 ────────── LAN ────────── [Client - Windows / Linux] ANTHROPIC_BASE_URL = http://192.168.x.x:8000 claude → Gemma로 응답
⚠️ 중요: Claude Code는 OpenAI 포맷(/chat/completions)이 아니라 Anthropic Messages API 포맷(/v1/messages)을 사용합니다.
1. 패키지 설치
pip install fastapi uvicorn transformers accelerate torch
2. 서버 코드 (gemma_server.py)
import json, os, time, uuid
from threading import Thread
from typing import Optional
import torch, uvicorn
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
MODEL_ID = os.environ.get("MODEL_ID", "google/gemma-3-4b-it")
HF_TOKEN = os.environ.get("HF_TOKEN")
if HF_TOKEN:
from huggingface_hub import login
login(token=HF_TOKEN)
# 최초 실행 시 자동 다운로드, 이후 캐시 사용
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
device_map="auto",
torch_dtype=torch.bfloat16,
)
model.eval()
app = FastAPI()
class Message(BaseModel):
role: str
content: str | list
class MessagesRequest(BaseModel):
model: str
messages: list[Message]
max_tokens: Optional[int] = 1024
temperature: Optional[float] = 0.7
stream: Optional[bool] = False
system: Optional[str] = None
def extract_text(content):
if isinstance(content, str): return content
return "".join(b.get("text","") for b in content if isinstance(b,dict) and b.get("type")=="text")
@app.get("/v1/models")
def list_models():
return {"object":"list","data":[{"id":MODEL_ID,"object":"model","owned_by":"local"}]}
@app.post("/v1/messages")
async def messages(req: MessagesRequest):
hf_msgs = []
if req.system:
hf_msgs += [{"role":"user","content":f"[System]: {req.system}"},
{"role":"assistant","content":"Understood."}]
for m in req.messages:
hf_msgs.append({"role":m.role,"content":extract_text(m.content)})
ids = tokenizer.apply_chat_template(
hf_msgs, add_generation_prompt=True, return_tensors="pt"
).to(model.device)
gen = dict(input_ids=ids, max_new_tokens=req.max_tokens,
temperature=max(req.temperature,1e-7), do_sample=req.temperature>0)
if req.stream:
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
gen["streamer"] = streamer
Thread(target=model.generate, kwargs=gen, daemon=True).start()
async def event_stream():
cid = f"msg_{uuid.uuid4().hex}"
yield f"event: message_start\ndata: {json.dumps({'type':'message_start','message':{'id':cid,'type':'message','role':'assistant','content':[],'model':req.model,'stop_reason':None}})}\n\n"
yield f"event: content_block_start\ndata: {json.dumps({'type':'content_block_start','index':0,'content_block':{'type':'text','text':''}})}\n\n"
for token in streamer:
yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':token}})}\n\n"
yield f"event: content_block_stop\ndata: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
yield f"event: message_stop\ndata: {json.dumps({'type':'message_stop'})}\n\n"
return StreamingResponse(event_stream(), media_type="text/event-stream")
with torch.no_grad():
out = model.generate(**gen)
text = tokenizer.decode(out[0][ids.shape[-1]:], skip_special_tokens=True)
return {"id":f"msg_{uuid.uuid4().hex}","type":"message","role":"assistant",
"content":[{"type":"text","text":text}],"model":req.model,"stop_reason":"end_turn"}
@app.get("/health")
def health(): return {"status":"ok","model":MODEL_ID}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT",8000)))
3. 서버 실행 (Host PC)
# HuggingFace 토큰 설정 (Gemma 라이선스 동의 필요) $env:HF_TOKEN = "hf_xxxxxxxxxxxx" # 실행 → 최초 1회 자동 다운로드 후 서빙 python gemma_server.py
모델은 ~/.cache/huggingface/hub/에 자동 저장되고, 다음 실행부터는 캐시에서 로드합니다.
Windows 방화벽 허용 (관리자 PowerShell):
New-NetFirewallRule -DisplayName "Gemma LAN" -Direction Inbound -Protocol TCP -LocalPort 8000 -Action Allow
4. Claude Code 연결 (Client PC)
Claude Code는 Opus / Sonnet / Haiku 3개 역할 모두를 같은 모델로 지정해야 합니다.
Windows:$env:ANTHROPIC_BASE_URL = "http://192.168.1.xxx:8000" $env:ANTHROPIC_API_KEY = "local" $env:ANTHROPIC_DEFAULT_OPUS_MODEL = "google/gemma-3-4b-it" $env:ANTHROPIC_DEFAULT_SONNET_MODEL = "google/gemma-3-4b-it" $env:ANTHROPIC_DEFAULT_HAIKU_MODEL = "google/gemma-3-4b-it" claudeLinux:
export ANTHROPIC_BASE_URL="http://192.168.1.xxx:8000" export ANTHROPIC_API_KEY="local" export ANTHROPIC_DEFAULT_OPUS_MODEL="google/gemma-3-4b-it" export ANTHROPIC_DEFAULT_SONNET_MODEL="google/gemma-3-4b-it" export ANTHROPIC_DEFAULT_HAIKU_MODEL="google/gemma-3-4b-it" claude영구 설정 (~/.claude/settings.json):
{
"env": {
"ANTHROPIC_BASE_URL": "http://192.168.1.xxx:8000",
"ANTHROPIC_API_KEY": "local",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "google/gemma-3-4b-it",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "google/gemma-3-4b-it",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "google/gemma-3-4b-it"
}
}
핵심 정리
| 항목 | 내용 |
|---|---|
| API 포맷 | Anthropic Messages API (/v1/messages) — OpenAI 아님 |
| 모델 다운로드 | 최초 실행 시 HuggingFace에서 자동, 이후 캐시 |
| HF 토큰 | Gemma 라이선스 동의 후 발급 필요 |
| 모델명 env | Opus / Sonnet / Haiku 3개 모두 동일하게 지정 |
| GPU | device_map="auto" 로 자동 배분, CPU도 동작하나 느림 |
댓글
댓글 쓰기