diff --git a/5_django_backend/.env.example b/5_django_backend/.env.example index fcc71f8..608c5e4 100644 --- a/5_django_backend/.env.example +++ b/5_django_backend/.env.example @@ -19,11 +19,13 @@ CORS_ORIGINS=http://localhost:15173,http://tauri.localhost,tauri://localhost # ── 사내 LLM(FabriX) — 고객사 전용. 개발(OpenRouter)에선 전부 비워둠 ── # OpenCode 는 opencode/opencode.json 을 FabriX 용으로 렌더해서(opencode/render_opencode.py) 이 서버의 /api/ito 로 붙음. AAF_FABRIX_BASE_URL= -AAF_FABRIX_MODEL_ID=605 -AAF_FABRIX_MODELS=605:Gemma4,339:GaussO Flash,581:GaussO Think +AAF_FABRIX_MODEL_ID=339 +AAF_FABRIX_MODELS=339:GaussO Flash,581:GaussO Think,605:Gemma4 AAF_FABRIX_CLIENT_KEY= AAF_FABRIX_OPENAPI_TOKEN= AAF_FABRIX_USER_EMAIL= +# 이미지 붙은 요청만 이 모델로. Gemma4 가 텍스트엔 너무 느려서(안녕 94s vs 339 0.7s) 기본은 339 +AAF_FABRIX_VISION_MODEL_ID=605 # 게이트웨이마다 다를 수 있는 것 — 보통 비워둠(401 나면 자동으로 다른 형식도 시도함) AAF_FABRIX_CLIENT_HEADER= AAF_FABRIX_TOKEN_PREFIX=bearer diff --git a/5_django_backend/apps/gateway/fabrix.py b/5_django_backend/apps/gateway/fabrix.py index 2f22bf8..3cc5cbc 100644 --- a/5_django_backend/apps/gateway/fabrix.py +++ b/5_django_backend/apps/gateway/fabrix.py @@ -30,9 +30,21 @@ ENV_KEYS = ( "AAF_FABRIX_MAX_TOKENS", # 있으면 max_completion_tokens 로 실음 "AAF_RELAY_STREAM_USAGE", # "1" 이면 stream_options.include_usage — 제공자가 거부하면 끔 "AAF_GATEWAY_KEY", # OpenCode 가 Authorization: Bearer 로 보내는 키. 비우면 검사 안 함 + "AAF_FABRIX_VISION_MODEL_ID", # 이미지가 붙은 요청만 이 모델로(Gemma4). 비우면 분기 안 함 ) +def has_image(payload: dict) -> bool: + """OpenAI 형식 messages 안에 image_url 파트가 하나라도 있으면 True.""" + for m in payload.get("messages") or []: + content = m.get("content") if isinstance(m, dict) else None + if isinstance(content, list) and any( + isinstance(p, dict) and p.get("type") == "image_url" for p in content + ): + return True + return False + + def parse_models(spec: str) -> dict[str, str]: """"339:GaussO Flash,581:GaussO Think" → {"339": "GaussO Flash", "581": "GaussO Think"}.""" out: dict[str, str] = {} @@ -67,6 +79,7 @@ class FabrixConfig: max_tokens: int | None = None stream_usage: bool = False gateway_key: str = "" + vision_model_id: str = "" # 이미지 있을 때만 쓰는 모델. Gemma 가 느려서 텍스트는 기본 모델로 connect_timeout_s: float = 10.0 read_timeout_s: float = 120.0 # 스트림 조각 사이 무수신 한계 total_timeout_s: float = 600.0 @@ -88,6 +101,7 @@ class FabrixConfig: max_tokens=int(max_tokens) if max_tokens.isdigit() else None, stream_usage=g("AAF_RELAY_STREAM_USAGE") == "1", gateway_key=g("AAF_GATEWAY_KEY"), + vision_model_id=g("AAF_FABRIX_VISION_MODEL_ID"), ) def missing(self) -> list[str]: @@ -125,7 +139,10 @@ class FabrixConfig: def prepare(self, payload: dict) -> tuple[dict, dict[str, str]]: """OpenCode 가 보낸 OpenAI 요청 → FabriX 로 보낼 (body, headers).""" body = dict(payload) - headers = self.headers(self.pick_model_id(str(payload.get("model") or ""))) + model_id = self.pick_model_id(str(payload.get("model") or "")) + if self.vision_model_id and has_image(payload): + model_id = self.vision_model_id # 이미지 있으면 무조건 vision 모델(느려도 이미지는 얘만 읽음) + headers = self.headers(model_id) body["model"] = self.body_model if body.get("stream") and self.stream_usage: so = dict(body.get("stream_options") or {}) diff --git a/5_django_backend/tests/test_gateway.py b/5_django_backend/tests/test_gateway.py index a3ce855..a1904c3 100644 --- a/5_django_backend/tests/test_gateway.py +++ b/5_django_backend/tests/test_gateway.py @@ -38,6 +38,19 @@ def test_prepare_picks_model_by_header_not_body(): assert headers["x-llm-model-id"] == "339" +def test_vision_model_only_when_image_attached(): + cfg = FabrixConfig.from_env({**FULL_ENV, "AAF_FABRIX_VISION_MODEL_ID": "605"}) + text = {"model": "339", "messages": [{"role": "user", "content": "hi"}]} + img = {"model": "339", "messages": [{"role": "user", "content": [ + {"type": "text", "text": "what is this"}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}}, + ]}]} + assert cfg.prepare(text)[1]["x-llm-model-id"] == "339" + assert cfg.prepare(img)[1]["x-llm-model-id"] == "605" + # 설정 없으면 분기 안 함 + assert FabrixConfig.from_env(FULL_ENV).prepare(img)[1]["x-llm-model-id"] == "339" + + def test_missing_config_names(): assert FabrixConfig.from_env({}).missing() == [ "AAF_FABRIX_BASE_URL", "AAF_FABRIX_MODEL_ID", "AAF_FABRIX_CLIENT_KEY", "AAF_FABRIX_OPENAPI_TOKEN", diff --git a/docs/tech/fabrix-model-perf.md b/docs/tech/fabrix-model-perf.md index 87a203d..8a6d632 100644 --- a/docs/tech/fabrix-model-perf.md +++ b/docs/tech/fabrix-model-perf.md @@ -10,3 +10,14 @@ - 세 모델 다 같은 client key 로 열려 있음. 토큰은 `Bearer ` 접두 필수(날것은 401), 클라이언트 헤더는 `x-generative-ai-client`·`x-fabrix-client` 둘 다 됨. - Gemma4 는 토큰당 속도가 눈에 띄게 느림 → 텍스트 기본은 339, 이미지 있을 때만 605 로 보내는 게 맞음. + +## 2차 (같은 날 저녁, "안녕" 한 단어) + +| id | 모델 | 전체 | +|---|---|---| +| 339 | GaussO Flash | 0.7s | +| 605 | Gemma4 | 94.4s | +| 580 | Gemma4(다른 인스턴스) | 188.5s | + +짧은 입력에 더 오래 걸림 → 출력 길이가 아니라 **큐 대기/서빙 문제**. 사용 불가 수준. FabriX 담당에 서빙 스펙 확인 요청. +대응: 기본 339, 이미지 붙은 요청만 605 (`AAF_FABRIX_VISION_MODEL_ID`, 게이트웨이 자동 분기). diff --git a/z-my-docs/work-log/2026-09/2026-09-18.md b/z-my-docs/work-log/2026-09/2026-09-18.md new file mode 100644 index 0000000..674e0dc --- /dev/null +++ b/z-my-docs/work-log/2026-09/2026-09-18.md @@ -0,0 +1,5 @@ +# 작업로그 · 2026-09-18 (금) + +| 시간 | 내용 | +|------|------| +| 10:34 | Gemma4 '안녕' 94s/188s 실측 → 기본 339 로, 이미지 붙은 요청만 605 로 가는 자동 분기(AAF_FABRIX_VISION_MODEL_ID) 게이트웨이에 추가 |