feat: NanoClaude Phase 2 — EXAONE→Gemma 파이프라인, 큐, 상태 API

- ModelAdapter: 범용 OpenAI-compat 어댑터 (stream/complete/health)
- BackendRegistry: rewriter(EXAONE) + reasoner(Gemma4) 헬스체크 루프
- 2단계 파이프라인: EXAONE rewrite → Gemma reasoning (SSE rewrite 이벤트 노출)
- Fallback: 맥미니 다운 시 EXAONE 단독 모드, stream 중간 실패 시 자동 전환
- Cancel-safe: rewrite 전/후, streaming loop 내, fallback 경로 모두 체크
- Rewrite heartbeat: complete_chat 대기 중 2초 간격 processing 이벤트
- JobQueue: Semaphore(3) 기반 동시성 제한, 정확한 queue position
- GET /chat/{job_id}/status, GET /queue/stats 엔드포인트
- DB: rewrite_model, reasoning_model, rewritten_message 컬럼 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-06 12:04:15 +09:00
parent 8c41a5dead
commit c4c32170f1
14 changed files with 495 additions and 141 deletions

View File

@@ -1,4 +1,4 @@
"""NanoClaude — 비동기 job 기반 AI Gateway."""
"""NanoClaude — 비동기 job 기반 AI Gateway (Phase 2: EXAONE → Gemma 파이프라인)."""
from __future__ import annotations
@@ -12,22 +12,29 @@ from fastapi.responses import JSONResponse
from config import settings
from db.database import init_db
from routers import chat
from services.backend_registry import backend_registry
from services.job_queue import init_queue, job_queue
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s%(message)s",
)
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
backend_registry.init_from_settings(settings)
backend_registry.start_health_loop(settings.health_check_interval)
init_queue(settings.max_concurrent_jobs)
yield
backend_registry.stop_health_loop()
app = FastAPI(
title="NanoClaude",
version="0.1.0",
description="비동기 job 기반 AI Gateway — Phase 1",
version="0.2.0",
description="비동기 job 기반 AI Gateway — Phase 2 (EXAONE → Gemma 파이프라인)",
lifespan=lifespan,
)
@@ -54,9 +61,14 @@ app.include_router(chat.router)
@app.get("/")
async def root():
return {"service": "NanoClaude", "version": "0.1.0", "phase": 1}
return {"service": "NanoClaude", "version": "0.2.0", "phase": 2}
@app.get("/health")
async def health():
return {"status": "ok"}
from services.job_queue import job_queue
return {
"status": "ok",
"backends": backend_registry.health_summary(),
"queue": job_queue.stats if job_queue else {},
}