diff --git a/5_django_backend/.env.example b/5_django_backend/.env.example index 3f84263..6808026 100644 --- a/5_django_backend/.env.example +++ b/5_django_backend/.env.example @@ -1,6 +1,8 @@ # CodeAssist 전용 OpenCode 서버 (opencode/ 폴더에서 `opencode serve --port 4096`) OPENCODE_BASE_URL=http://localhost:4096 OPENCODE_AGENT=codeassist +# 이미지 요청만 이 모델로 (로컬 OpenRouter). 고객사는 비우고 AAF_FABRIX_VISION_MODEL_ID 사용 +OPENCODE_VISION_MODEL=openrouter/google/gemma-4-31b-it # 토큰 서명 키 (비우면 data/.auth_secret 자동 생성) AUTH_SECRET= diff --git a/5_django_backend/apps/chat/stream.py b/5_django_backend/apps/chat/stream.py index 27b6202..24d63e6 100644 --- a/5_django_backend/apps/chat/stream.py +++ b/5_django_backend/apps/chat/stream.py @@ -85,7 +85,12 @@ def _prompt_payload(req: dict) -> dict: for i, img in enumerate(req["images"]): ext = img["mediaType"].split("/")[1] parts.append({"type": "file", "mime": img["mediaType"], "filename": f"image-{i + 1}.{ext}", "url": img["data"]}) - return {"agent": settings.OPENCODE_AGENT, "parts": parts} + payload: dict = {"agent": settings.OPENCODE_AGENT, "parts": parts} + # 이미지 있으면 vision 모델로 — 텍스트는 기본 모델(빠름). 고객사 FabriX 는 게이트웨이가 따로 분기. + if req["images"] and settings.OPENCODE_VISION_MODEL and "/" in settings.OPENCODE_VISION_MODEL: + provider, model = settings.OPENCODE_VISION_MODEL.split("/", 1) + payload["model"] = {"providerID": provider, "modelID": model} + return payload # ── OpenCode 이벤트 → 우리 이벤트 ──────────────────────────────── diff --git a/5_django_backend/config/settings.py b/5_django_backend/config/settings.py index 7805003..d7eb718 100644 --- a/5_django_backend/config/settings.py +++ b/5_django_backend/config/settings.py @@ -75,6 +75,9 @@ OPENCODE_BASE_URL = _env("OPENCODE_BASE_URL", "http://localhost:4096").rstrip("/ OPENCODE_DIRECTORY = _env("OPENCODE_DIRECTORY") or str((BASE_DIR / "opencode").resolve()) # OpenCode 에 넘길 에이전트 이름 (opencode/.opencode/agent/<이름>.md) OPENCODE_AGENT = _env("OPENCODE_AGENT", "codeassist") +# 이미지가 붙은 요청만 이 모델로("provider/model" 형식, opencode.json 에 등록돼 있어야 함). 비우면 기본 모델. +# 로컬(OpenRouter)용 — 고객사(FabriX)는 게이트웨이가 AAF_FABRIX_VISION_MODEL_ID 로 같은 분기를 함. +OPENCODE_VISION_MODEL = _env("OPENCODE_VISION_MODEL") # 채팅 스트림 타임아웃(초). Gemma4 가 "안녕"에도 200초 걸리는 날이 있어 첫 이벤트 300, 한 턴 600. STREAM_TURN_TIMEOUT_S = int(_env("STREAM_TURN_TIMEOUT_S", "600")) diff --git a/5_django_backend/opencode/opencode.json b/5_django_backend/opencode/opencode.json index a92d7e0..b5aa8e5 100644 --- a/5_django_backend/opencode/opencode.json +++ b/5_django_backend/opencode/opencode.json @@ -4,7 +4,8 @@ "openrouter": { "options": { "apiKey": "{env:OPENROUTER_API_KEY}" }, "models": { - "z-ai/glm-5.2": { "name": "GLM 5.2 (OpenRouter)" } + "z-ai/glm-5.2": { "name": "GLM 5.2 (OpenRouter)" }, + "google/gemma-4-31b-it": { "name": "Gemma 4 31B (OpenRouter, vision)" } } } }, diff --git a/5_django_backend/tests/test_stream.py b/5_django_backend/tests/test_stream.py index c181b30..cf6d6ff 100644 --- a/5_django_backend/tests/test_stream.py +++ b/5_django_backend/tests/test_stream.py @@ -357,3 +357,17 @@ async def test_stream_title_arriving_after_idle_is_mirrored(fake, django_user_mo await asyncio.sleep(0.3) session = await ChatSession.objects.aget(id="s1") assert session.title_llm == "늦게 온 제목" + + + +# ── 이미지 있으면 vision 모델 ───────────────────────────────────── +def test_prompt_payload_switches_model_only_with_images(settings): + settings.OPENCODE_VISION_MODEL = "openrouter/google/gemma-4-31b-it" + text_only = stream_mod._prompt_payload({"content": "hi", "explain": False, "images": []}) + assert "model" not in text_only + with_img = stream_mod._prompt_payload( + {"content": "", "explain": False, "images": [{"mediaType": "image/png", "data": "data:image/png;base64,AA"}]} + ) + assert with_img["model"] == {"providerID": "openrouter", "modelID": "google/gemma-4-31b-it"} + settings.OPENCODE_VISION_MODEL = "" + assert "model" not in stream_mod._prompt_payload({"content": "", "explain": False, "images": with_img["parts"] and [{"mediaType": "image/png", "data": "x"}]}) diff --git a/z-my-docs/work-log/2026-09/2026-09-21.md b/z-my-docs/work-log/2026-09/2026-09-21.md index 28159b9..ef9ac45 100644 --- a/z-my-docs/work-log/2026-09/2026-09-21.md +++ b/z-my-docs/work-log/2026-09/2026-09-21.md @@ -13,3 +13,4 @@ | 14:13 | 답변 완료 알림 — 창 안 보고 있으면 Windows 토스트 + 트레이 아이콘 파란 점, 창 포커스 받으면 원복. tauri-plugin-notification, 프론트 onDone 에서 chat.done 한 줄 | | 14:37 | ABAP 소스 인덱스(ABAP_INDEXING :8100)를 OpenCode 커스텀 툴 3개로 연결(abap_index_search/chunk/source). AGENTS.md 에 출처 규칙 — 인덱스/SAP조회/생성 코드 구분해 코드 위에 표기. 서버 없을 때 '출처: 없음 — 생성 코드' 로 답하는 것 실측 | | 14:49 | 새 대화 예시 질문에서 ALV 리포트 뼈대(CL_SALV_TABLE) 버튼 제거 | +| 14:54 | 로컬도 이미지 요청만 vision 모델 — OPENCODE_VISION_MODEL(openrouter/google/gemma-4-31b-it), 백엔드가 prompt 에 model 실음. 텍스트는 GLM 5.2 그대로 |