Stan 기술블로그

자동완성이 안 뜬다 — 겹쳐 있던 네 가지 원인

지난 글에서 Tabby 서버를 겨우 띄운 다음 날, VS Code에서 회색 자동완성이 하나도 뜨지 않았다. 로그에는 Provided inline completion items: 0만 반복됐다. 원인은 하나가 아니라 네 겹이었다. 설정 혼선, Ollama의 알려진 버그, 재시작을 거듭하다 잠긴 DB, 그리고 서버를 다 고친 뒤에도 예전의 빈 응답을 재생하던 클라이언트 캐시.

(사내 인프라 특성상 IP·서버 이름은 일반화했다.)


서버는 떴는데 자동완성이 안 뜬다

Tabby 서버는 정상이었다. curl localhost:8080도 응답했다. VS Code에 Tabby 확장(tabbyml.vscode-tabby 1.28.0 — 처음엔 프리릴리즈인가 의심했지만 정식 릴리즈였다)을 깔고 주소와 토큰을 넣었다.

그런데 코드를 쳐도 회색 텍스트가 뜨지 않는다. 확장 로그는 이 줄만 반복했다.

Provided inline completion items: 0

요청은 나가는 것 같은데 결과가 0개다. 여기서부터 한 겹씩 벗겼다.

네 겹 벗기기

[1차] 주소를 두 곳에 넣었다

Tabby VS Code 확장은 서버 주소를 두 군데서 읽을 수 있다.

  • VS Code settings.json의 tabby.endpoint
  • 로컬 agent 설정 ~/.tabby-client/agent/config.toml의 [server] 섹션

양쪽에 다 넣어 뒀는데, 어느 순간 settings.json 쪽 값이 빈 문자열("")로 바뀌어 있었다. 확인해 보니 실제로 적용되고 있던 건 agent 설정 쪽이었다. 원인이라기보다 헷갈리는 변수였으니, 주소와 토큰은 agent 설정 한 곳에만 두기로 정리했다.

토큰은 Tabby 웹 화면에 로그인하면 나오는 “Endpoint URL / Token” 페이지에서 받는다. 이 페이지는 주소를 localhost:8080으로 보여 주는데, 다른 PC에서는 통하지 않는 주소라 실제 서버 IP로 바꿔 넣어야 한다. 이 함정은 온보딩 가이드 편에도 적었다.

여전히 0개.

[2차] 200인데 비어 있다 — Ollama /v1/completions의 FIM 버그

자동완성 요청은 200으로 돌아오는데 내용이 비어 있었다. 모델 문제인지, Tabby가 요청을 잘못 만드는지 가르려고 Tabby를 거치지 않고 Ollama에 직접 FIM 프롬프트를 보냈다.

curl http://<LLM 서버 A>:11434/api/generate -d '{
  "model": "qwen2.5-coder:14b",
  "prompt": "<|fim_prefix|>def add(a, b):\n    return a<|fim_suffix|>\n<|fim_middle|>",
  "raw": true,
  "stream": false
}'

` + b` 같은 완성 텍스트가 정상으로 왔다. 모델도, 프롬프트 형식도 멀쩡하다. 남은 건 그 사이, Tabby가 요청을 보내는 입구다.

Ollama 이슈 #5544에 있는 알려진 버그였다. OpenAI 호환 /v1/completions가 넘겨받은 FIM 프롬프트를 안에서 다시 채팅 템플릿으로 감싸 버려서 FIM 토큰이 힘을 잃는다. 임베딩 편에서 kind = "openai/completion"으로 붙인 게 이 버그를 그대로 맞았다. 자동완성만 Ollama 고유 API로 바꿨다.

[model.completion.http]
kind = "ollama/completion"                   # openai/completion 아님
model_name = "qwen2.5-coder:14b"
api_endpoint = "http://192.168.x.x:11434"    # /v1 을 뺀다
prompt_template = "<|fim_prefix|>{prefix}<|fim_suffix|>{suffix}<|fim_middle|>"

버그는 /v1/completions(FIM) 쪽이라 채팅과 임베딩은 그대로 뒀다.

[3차] 이번엔 서버가 안 뜬다 — database is locked

이것저것 바꾸며 docker compose restart와 --force-recreate를 여러 번 반복하는 사이, Tabby가 쓰는 SQLite가 비정상 종료 흔적을 남겼다. 서버가 아예 뜨지 못했다.

Must be able to initialize db: database is locked

컨테이너를 완전히 내리고, 남아 있던 잠금·저널 파일만 지운 뒤 다시 올렸다.

docker compose down
sudo rm -f ~/.tabby/ee/db.sqlite-shm \
           ~/.tabby/ee/db.sqlite-wal \
           ~/.tabby/index/.tantivy-writer.lock \
           ~/.tabby/index/.tantivy-meta.lock
docker compose up -d

본체 db.sqlite와 백업 db.backup-*.sqlite는 건드리지 않는다. 그걸 지우면 계정과 인덱스가 통째로 날아간다. 서버는 다시 떴다.

[4차] 서버는 멀쩡한데 0개 — 클라이언트 캐시

이제 Tabby 서버에 직접 자동완성을 요청하면 ` + b`가 정상으로 온다. 그런데 VS Code에서는 여전히 0개였다. 로그를 다시 보니 이 줄이 있었다.

Completion cache hit

클라이언트가 캐시에서 답을 꺼내 쓰느라 서버에 요청을 보내지도 않고 있었다. 그 캐시에 든 건, 앞의 문제들로 서버가 망가져 있던 동안 같은 코드로 반복 테스트하며 받은 빈 응답이었다.

오늘 한 번도 쳐 보지 않은 새 코드로 테스트했다. Fetching more completions from the server...가 세 번 이어서 찍히며 진짜 요청이 나갔고, 마침내:

Provided inline completion items: 3

Tab으로 받아들이면 Tabby 서버로 POST /v1/events가 나가는 것까지 확인했다. VS Code 기본 자동완성이나 다른 확장이 아니라 Tabby가 만든 결과라는 증거다.

디버깅하는 동안에는 Alt+\(Trigger Inline Suggestion)로 요청을 하나씩만 보냈다. 타이핑 중 자동으로 나가는 요청이 취소되며 생기는 잡음을 줄이려는 것이고, 평소에는 타이핑을 잠깐 멈추면 자동으로 뜬다.

덤 — 느린 건 GPU를 안 써서였다

뜨긴 뜨는데 후보 하나에 1초 가까이, 세 개면 3초쯤 걸렸다. 백엔드에 물어봤다.

curl -s http://<LLM 서버 A>:11434/api/ps | jq '.models[] | {name, size_vram}'

qwen2.5-coder:14b의 size_vram이 0이었다. GPU를 전혀 쓰지 않고 CPU로 추론하고 있었다. 채팅 서버(qwen3-coder, 30.5B)도 똑같이 0이었다. 채팅이 상대적으로 빠르게 느껴졌던 건 GPU 때문이 아니라, 자동완성은 후보 세 개를 차례로 만들고 채팅은 답 하나만 만들기 때문이다.

Tabby 설정 문제가 아니라 LLM 서버 쪽 문제라 담당자에게 넘겼다. 담당자가 모델을 다시 올리는 사이 서버가 아예 내려가 Failed to connect가 됐고, 복구 직후 첫 요청은 모델을 메모리에 다시 올리느라 15초 넘게 걸렸다. 이럴 땐 다시 시도를 거듭하지 말고 1분쯤 손을 떼고 기다리는 게 낫다. 같은 맥락에서, 응답이 수 ms 만에 오는데 비어 있다면 “빠른 성공”이 아니라 백엔드에 연결조차 못 한 것일 가능성이 크다.

그 뒤

한 달이 안 돼 자동완성 백엔드는 Ollama에서 vLLM으로, 채팅은 llama.cpp로 바뀌었다. 그래서 2차의 ollama/completion 우회는 지금 구성에는 해당하지 않는다. GPU를 쓰지 않는 문제는 8월 기록까지도 풀리지 않은 채 남아 있었고, 지금 백엔드에서도 그런지는 다시 확인해야 한다.

마치며

진짜로 배운 것

  1. 여러 겹이 겹친 버그는 안쪽부터 벗긴다. 백엔드에 직접 curl → Tabby 서버에 직접 curl → 마지막에 IDE. IDE부터 만지면 변수가 너무 많고, “설정을 바꿨더니 됐다”는 착각에 빠지기 쉽다.
  2. 반복 테스트는 매번 새 코드로. 같은 입력을 반복하면 클라이언트 캐시가 예전 실패를 계속 재생한다. 서버는 멀쩡한데 클라이언트에서만 안 될 때 캐시를 의심한다.
  3. 200을 믿지 말고 내용을 본다. 200인데 비어 있으면 입구(엔드포인트) 문제일 수 있고, 너무 빠른데 비어 있으면 백엔드가 죽어 있을 수 있다.
  4. 재시작을 거듭했다면 잠금 파일을 본다. database is locked면 -shm·-wal·.lock만 지운다. 본체 DB는 건드리지 않는다.

사내 코딩 어시스턴트 카테고리의 글