- apps/gateway: OpenCode→FabriX 통과 중계. 헤더 3종 인증, x-llm-model-id 로 모델 선택, 401 시 Bearer/날것×클라이언트 헤더 재시도, 모델 허용 목록. ABAP_OPENCODE apps_ito 통째 복사 대신 200줄로 - opencode/opencode.fabrix.json.tmpl + render_opencode.py — .env AAF_* 로 렌더 - .env.example AAF 블록(기본 605 Gemma4, TOKEN_PREFIX=bearer). 파서가 줄 끝 # 주석을 값으로 읽던 것 수정 - tests/test_gateway.py 9개(MockTransport) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
103 lines
5.9 KiB
Markdown
103 lines
5.9 KiB
Markdown
# 5_django_backend — CodeAssist 백엔드 (Django + OpenCode)
|
|
|
|
프론트(`2_frontend`)가 기대하는 base-backend 계약을 그대로 구현한 Django 서버. AI 답변은 CodeAssist 전용 OpenCode 서버(`opencode/`)가 만들고, 여기는 인증·세션 미러·SSE 어댑터를 맡음.
|
|
ABAP_OPENCODE `web/BE` 에서 뼈대(settings·JWT·OpenCode 클라)를 복사해 왔고, 그쪽 서버와는 독립.
|
|
|
|
```
|
|
CodeAssist(Tauri/React) ──/api/v1──▶ Django(uvicorn :8001) ──HTTP/SSE──▶ opencode serve(:4096) ──▶ LLM
|
|
Bearer JWT (localStorage) ├─ accounts 로그인·refresh
|
|
├─ chat 세션·검색 미러(SQLite/PG) + /chat/stream 어댑터
|
|
└─ opencode/ workspace (AGENTS.md·agent·opencode.json)
|
|
```
|
|
|
|
## 띄우기
|
|
|
|
```bash
|
|
# 1) OpenCode (터미널 1)
|
|
cd 5_django_backend/opencode
|
|
export OPENROUTER_API_KEY=sk-or-...
|
|
npx -y opencode-ai@1.18.6 serve --hostname 127.0.0.1 --port 4096
|
|
|
|
# 2) Django (터미널 2)
|
|
cd 5_django_backend
|
|
python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt
|
|
cp .env.example .env # 필요하면 값 수정
|
|
.venv/bin/python manage.py migrate # 빈 DB 면 admin@codeassist.local / guest@codeassist.local (test1234) 시드
|
|
.venv/bin/uvicorn config.asgi:application --host 0.0.0.0 --port 8001 --reload
|
|
|
|
# 3) 프론트 (터미널 3) — .env 의 VITE_DEV_API_TARGET 기본이 :8001 이라 그대로
|
|
cd 2_frontend && npm run dev
|
|
```
|
|
|
|
`runserver` 말고 **uvicorn(ASGI)** 이어야 SSE 가 버퍼링 없이 흐름.
|
|
|
|
## API (전부 envelope `CommonResponse`)
|
|
|
|
| 엔드포인트 | 설명 |
|
|
|---|---|
|
|
| `GET /api/v1/health` | 상태 (무인증) |
|
|
| `POST /api/v1/auth/login` `{email,password}` | `TokenResponse` — access 60분 / refresh 14일 |
|
|
| `POST /api/v1/auth/refresh` `{refreshToken}` | access 재발급 (refresh 회전 안 함). 없으면 401 `REFRESH_TOKEN_MISSING` |
|
|
| `POST /api/v1/auth/logout` | null (클라가 토큰 버림) |
|
|
| `GET /api/v1/users/me` | `UserResponse` |
|
|
| `GET /api/v1/auth/entra/config` | 항상 501 — 프론트가 Entra 버튼 숨김 |
|
|
| `GET /api/v1/chat/sessions?page&limit` | 내 세션 목록, 최근순 + meta |
|
|
| `POST /api/v1/chat/sessions` | OpenCode 세션 생성 + 미러 |
|
|
| `GET /api/v1/chat/sessions/search?query&page&limit` | 내 메시지 본문 검색 |
|
|
| `GET /api/v1/chat/sessions/{id}/messages` | 세션 + 메시지 (`isGenerating` 폴링용) |
|
|
| `POST /api/v1/chat/sessions/{id}/cancel` | OpenCode abort + isGenerating 해제 |
|
|
| `POST /api/v1/chat/stream` | SSE — `token{delta}`… `title{title}` `usage{used,limit,ratio,elapsed_ms}` `done{}` / `error{message,code}` |
|
|
|
|
에러 code: `INVALID_CREDENTIALS` `REFRESH_TOKEN_MISSING` `REFRESH_TOKEN_INVALID` `CHAT_GENERATION_IN_PROGRESS`(409) `UPSTREAM_UNAVAILABLE`(503) `LLM_ERROR` `LLM_ABORTED`.
|
|
|
|
## 스트림이 도는 법 (`apps/chat/stream.py`)
|
|
|
|
1. Bearer 검사 → 내 세션 → `is_generating` 이면 409.
|
|
2. user 메시지 미러 저장, `is_generating=true`.
|
|
3. `apps/chat/events.py` 버스(프로세스당 하나가 OpenCode `/event` 구독) 에 세션 큐 등록 → `POST /session/{id}/prompt_async`.
|
|
4. `message.part.updated`(text, delta) → `token`, `session.updated` 제목 → `title`, `session.idle` → OpenCode 에서 최종 메시지·tokens·cost 가져와 assistant 미러 저장 → `usage` → `done`. `session.error` → `error` + 부분 답변 저장.
|
|
5. 이 감시는 클라이언트와 별개 태스크라 창을 닫아도 idle 까지 돌고 DB 를 마무리함 → 프론트 `isGenerating` 폴링이 답변 복구.
|
|
|
|
## 테스트
|
|
|
|
```bash
|
|
.venv/bin/python -m pytest -q # OpenCode 는 가짜(tests/test_stream.py FakeOpencode)
|
|
```
|
|
|
|
## 폴더
|
|
|
|
```
|
|
config/ settings · urls · asgi
|
|
common/ envelope.py(응답 포장·CodedError) · opencode_service.py(OpenCode HTTP/SSE)
|
|
apps/accounts User(email 로그인) · JWT · 시드
|
|
apps/chat models(ChatSession/ChatMessage) · views · events(버스) · stream(어댑터)
|
|
opencode/ OpenCode workspace — AGENTS.md · opencode.json · .opencode/agent/codeassist.md
|
|
docs-lib/ OpenCode SDK 1.18.6 타입 (API 진실원천)
|
|
tests/ pytest
|
|
```
|
|
|
|
## 고객사 = 사내 LLM(FabriX)
|
|
|
|
개발은 OpenRouter 직결이고, 고객사에선 ABAP_OPENCODE 와 같은 방식으로 **FabriX** 를 씀. OpenCode 는 LLM 을 직접 안 부르고 이 서버의 `/api/ito`(OpenAI 호환) 로 붙고, `apps/gateway` 가 FabriX 인증 헤더를 얹어 그대로 흘림.
|
|
|
|
```
|
|
OpenCode ──OpenAI 호환──▶ Django /api/ito/chat/completions ──x-llm-model-id / x-openapi-token / x-generative-ai-client──▶ FabriX
|
|
```
|
|
|
|
```bash
|
|
# 1) .env 에 AAF_FABRIX_* 채움 (.env.example 의 사내 LLM 블록. 키 이름은 ABAP_OPENCODE 와 같음)
|
|
# 2) OpenCode 설정을 FabriX 용으로 렌더 (opencode/opencode.json 덮어씀)
|
|
.venv/bin/python opencode/render_opencode.py
|
|
# 3) OpenCode 는 키 없이 그냥 띄움 (OPENROUTER_API_KEY 불필요)
|
|
cd opencode && opencode serve --hostname 127.0.0.1 --port 4096
|
|
# 4) 확인
|
|
curl http://localhost:8001/api/ito/healthcheck
|
|
curl http://localhost:8001/api/ito/models
|
|
```
|
|
|
|
- `AAF_FABRIX_MODELS="605:Gemma4,339:GaussO Flash,581:GaussO Think"` 처럼 두면 OpenCode 화면에서 모델을 고를 수 있고, 고른 id 가 그대로 `x-llm-model-id` 로 감.
|
|
- 고객사 게이트웨이가 토큰 형식(Bearer/날것)·클라이언트 헤더 이름을 서버마다 다르게 받아서, 401 이면 다른 조합을 자동으로 더 시도하고 통과한 걸 기억함.
|
|
- 설정이 비어 있어도 서버는 뜨고, `/api/ito/chat/completions` 만 503 으로 뭐가 빠졌는지 알려줌.
|
|
- ABAP_OPENCODE 의 `apps_ito` 를 통째로 안 가져온 이유: 그쪽은 11k 줄 + DB 테이블 + langgraph 등 의존성 15개인데 여기 필요한 건 통과 중계뿐. 사용자별 키(IP 마스터)는 아직 없음 — 서비스 키 하나로 감.
|
|
- 개발로 되돌리기: `git checkout opencode/opencode.json`.
|