ops(ai)!: 맥미니 생성 모델 Qwen3.6-27B-6bit 전환 + 생성 LLM 홀드 해제
B안(사용자 2026-06-11): Gemma 26B-A4B → Qwen3.6-27B-6bit 풀교체. - config.yaml triage/primary model 교체 + dense 감속 반영 timeout 상향(30→120/180→300) - held_stages [] (홀드 해제 — 적체 자연 드레인, deep_summary 는 primary 복귀) - eid deep 모드 = mac-mini-default 재지정(맥북 백지화). llm_gate '예외 없이 gate' invariant 에 따라 deep 도 alias 조건으로 자동 게이트 (구 무게이트 = 맥북 별 endpoint 예외였음) - deep probe 실패 reason = router_unreachable 로 정정 + 테스트 동기화 잔여(별 PR): ask 표면 qwen-macbook 옵션/백엔드 클래스/처리보드 맥북 카드 정리 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+6
-5
@@ -2,8 +2,9 @@
|
||||
|
||||
확정 결정:
|
||||
- D-1 경로 = /api/eid/chat (main.py prefix=/api/eid + 본 라우터 POST /chat)
|
||||
- D-2 mode 닫힌 어휘: daily(mac-mini-default) / deep(qwen-macbook). 클라는 mode 만 보냄 —
|
||||
claude-cloud / auto 금지 (Literal 로 422 차단). 심층(deep) 모드 무게이트.
|
||||
- D-2 mode 닫힌 어휘: daily / deep — 둘 다 mac-mini-default (맥북 백지화 2026-06-11,
|
||||
맥미니 Qwen 27B 단일 호스트. deep = ReAct 자동검색 모드 구분). 클라는 mode 만 보냄 —
|
||||
claude-cloud / auto 금지 (Literal 로 422 차단). 게이트 = alias 기준 자동 적용(무게이트 폐지).
|
||||
- D-3 독립 /chat 라우트 (frontend) — 본 모듈은 백엔드 API 만.
|
||||
- D-5 LLM 호출 = EidAIClient.call_stream 한 곳 (이드 egress 봉쇄 불변식 #5,
|
||||
RouterBackend 직접 호출 금지).
|
||||
@@ -43,7 +44,7 @@ logger = setup_logger("eid_chat")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ── ds-eid-ask-absorb P1: deep 모드 = ReAct 자동검색 (qwen-macbook 27B) ──
|
||||
# ── ds-eid-ask-absorb P1: deep 모드 = ReAct 자동검색 (맥미니 Qwen 27B, 2026-06-11~) ──
|
||||
# 비생성 reachability probe — router 도달만 확인(coarse). 27B(맥북) 자체 미가용은
|
||||
# 첫 generate_with_tools 호출의 BackendUnavailable → mid-stream error envelope 로 커버
|
||||
# (plan: probe 정밀도 불필요, TOCTOU 는 in-stream error 가 처리). ~2s 타임아웃·생성 슬롯 비점유.
|
||||
@@ -160,10 +161,10 @@ async def _eid_chat_deep(body: ChatRequest, session: AsyncSession) -> StreamingR
|
||||
"""
|
||||
# ① 첫 SSE 바이트(=HTTP 200 확정) 전 비생성 probe — router 도달 실패 시 503 (재매핑 가능 구간)
|
||||
if not await _probe_router_reachable():
|
||||
return _backend_unavailable_response(body, "macbook_unavailable", "qwen-macbook")
|
||||
return _backend_unavailable_response(body, "router_unreachable", "mac-mini-default")
|
||||
|
||||
query = body.messages[-1].content # 메시지 단독 처리 (마지막 user 턴)
|
||||
backend = get_backend("qwen-macbook")
|
||||
backend = get_backend("mac-mini-default")
|
||||
|
||||
async def _stream() -> AsyncIterator[bytes]:
|
||||
# ② phase:searching 방출 = HTTP 200 확정. 이후 미가용은 503 불가 → in-stream error.
|
||||
|
||||
+10
-7
@@ -29,16 +29,19 @@ import httpx
|
||||
from ai.client import AIClient
|
||||
from services.llm.backends import (
|
||||
MAC_MINI_DEFAULT,
|
||||
QWEN_MACBOOK,
|
||||
BackendUnavailable,
|
||||
_router_url, # router URL 단일 출처 재사용 (settings → env LLM_ROUTER_URL → MVP default)
|
||||
)
|
||||
from services.search.llm_gate import Priority, acquire_mlx_gate
|
||||
|
||||
# 이드 채팅 mode → router alias 닫힌 매핑 (D-2). 클라는 mode 만 보냄 — claude-cloud/auto 금지.
|
||||
# 2026-06-11 맥북 백지화: deep 도 mac-mini-default (맥미니 Qwen 27B 단일 호스트).
|
||||
# mode 구분은 유지 — deep = ReAct 자동검색 경로(모델이 아니라 동작이 다름).
|
||||
# 게이트는 alias==MAC_MINI_DEFAULT 조건이라 deep 도 자동으로 mlx gate 적용
|
||||
# (llm_gate "예외 없이 gate 획득 필수" invariant 충족 — 구 무게이트는 맥북 예외였음).
|
||||
_CHAT_ALIAS: dict[str, str] = {
|
||||
"daily": MAC_MINI_DEFAULT, # router tier_b → Mac mini :8801 gemma-4-26b
|
||||
"deep": QWEN_MACBOOK, # router named upstream → M5 Max Qwen3.6-27B (무게이트, D-2)
|
||||
"daily": MAC_MINI_DEFAULT, # router tier_b → Mac mini :8801
|
||||
"deep": MAC_MINI_DEFAULT, # 맥북 폐기로 동일 upstream — ReAct 검색 모드 구분만 유지
|
||||
}
|
||||
|
||||
# read 는 per-chunk 적용이라 MacBook wake(24s)+토큰 생성 간격 커버. connect 는 내부 router 라 짧게.
|
||||
@@ -161,10 +164,10 @@ class EidAIClient(AIClient):
|
||||
_rewrite_sse_line 으로 model 치환(mode 어휘)·usage 제거만 하고 프레이밍은 보존.
|
||||
취소/disconnect 시 AsyncExitStack 이 response·client 정리(upstream 닫힘 보장).
|
||||
|
||||
daily(mac-mini-default)는 Mac mini MLX 단일 inference 영구 룰(llm_gate docstring
|
||||
"예외 없이 gate 획득 필수")에 따라 acquire_mlx_gate(FOREGROUND) 안에서 스트리밍 —
|
||||
RouterBackend 의 requires_gate=True 와 동일한 client-side mutex 효과.
|
||||
deep(qwen-macbook)은 별 endpoint 라 무게이트 (D-2, RouterBackend 동형).
|
||||
daily/deep 모두 mac-mini-default(2026-06-11 맥북 백지화) → Mac mini MLX 단일
|
||||
inference 영구 룰(llm_gate docstring "예외 없이 gate 획득 필수")에 따라
|
||||
acquire_mlx_gate(FOREGROUND) 안에서 스트리밍 — 게이트 조건이 alias 기준이라
|
||||
deep 도 자동 적용 (구 무게이트는 맥북 별 endpoint 시절 예외였음).
|
||||
|
||||
중계 전체(업스트림 진입~종료)는 asyncio.timeout(_STREAM_DEADLINE_S) wall-clock
|
||||
deadline 안 — llm_gate 계약 "timeout 은 gate 안쪽" 준수(gate 대기엔 미적용).
|
||||
|
||||
+16
-18
@@ -6,25 +6,26 @@ ai:
|
||||
|
||||
models:
|
||||
# ─── 단일 generation 호스트 routing (2026-05-14 GPU LLM 제거) ───
|
||||
# GPU Ollama gemma4:e4b-it-q8_0 제거. Mac mini 26B-A4B 가 triage + primary + classifier 모두 흡수.
|
||||
# fallback 은 Claude Sonnet 4 API (Mac mini 다운 시 자동 trigger, premium 과 budget 공유).
|
||||
# plan: ~/.claude/plans/rosy-launching-otter.md §C/§D/§E
|
||||
# 2026-06-11 B안: 맥미니 모델 = Gemma 26B-A4B → Qwen3.6-27B-6bit 풀교체 (사용자 결정).
|
||||
# dense 27B 라 디코드 ~13 tok/s 급 (a4b ~42 대비 감속) → timeout 상향 (triage 30→120, primary 180→300).
|
||||
# fallback 은 Claude Sonnet 4 API (CLAUDE_API_KEY 미주입 = 비활성).
|
||||
# plan: ~/.claude/plans/rosy-launching-otter.md §C/§D/§E + project_macmini_model_decision
|
||||
|
||||
# triage: 상시 분류·요약·근거 선별. Mac mini 26B (primary 와 동일 endpoint, 짧은 max_tokens).
|
||||
# triage: 상시 분류·요약·근거 선별. Mac mini Qwen 27B (primary 와 동일 endpoint, 짧은 max_tokens).
|
||||
triage:
|
||||
endpoint: "http://100.76.254.116:8801/v1/chat/completions"
|
||||
model: "mlx-community/gemma-4-26b-a4b-it-8bit"
|
||||
model: "mlx-community/Qwen3.6-27B-6bit"
|
||||
max_tokens: 4096
|
||||
timeout: 30
|
||||
timeout: 120
|
||||
context_char_limit: 120000
|
||||
temperature: 0.0
|
||||
|
||||
# primary: 에스컬레이션 전용. 26B MLX (맥미니 Semaphore(1) 보호 대상).
|
||||
# primary: 에스컬레이션 전용. Qwen 27B MLX (맥미니 Semaphore(1) 보호 대상).
|
||||
primary:
|
||||
endpoint: "http://100.76.254.116:8801/v1/chat/completions"
|
||||
model: "mlx-community/gemma-4-26b-a4b-it-8bit"
|
||||
model: "mlx-community/Qwen3.6-27B-6bit"
|
||||
max_tokens: 8192
|
||||
timeout: 180
|
||||
timeout: 300
|
||||
context_char_limit: 260000
|
||||
temperature: 0.3
|
||||
top_p: 0.9
|
||||
@@ -177,13 +178,10 @@ schedule:
|
||||
file_watcher_interval_minutes: 5
|
||||
queue_consumer_interval_minutes: 10
|
||||
|
||||
# 생성 LLM 홀드 (2026-06-11, 사용자 지시): 맥미니 모델 확정까지 생성 LLM 소비 스테이지 보류.
|
||||
# - 큐: classify(triage)/summarize/deep_summary — claim 자체를 안 함 (attempts 미소모, pending 적체 = 의도)
|
||||
# - cron/컨슈머: digest(global 04:00), briefing(05:10), study_explanation/study_session_analysis/
|
||||
# study_memo_card (1분 컨슈머)
|
||||
# - 무영향: extract/embed/chunk/markdown/stt/preview/thumbnail/fulltext, 수집기 전부,
|
||||
# 인터랙티브(ask/eid chat), daily_digest(LLM 미사용)
|
||||
# 유효 키 = 위 8개 — 그 외 문자열은 무동작(오타 주의). 해제 = held_stages: [] 후 fastapi 재기동.
|
||||
# 생성 LLM 홀드 게이트 (2026-06-11 신설): held_stages 에 든 이름의 컨슈머/워커는 claim 자체를
|
||||
# 하지 않는다 (attempts 미소모, pending 적체). 유효 키 8 = classify/summarize/deep_summary(큐) +
|
||||
# digest/briefing(cron) + study_explanation/study_session_analysis/study_memo_card(컨슈머).
|
||||
# 그 외 문자열은 무동작(오타 주의). 적용/해제 = 리스트 수정 후 fastapi 재기동.
|
||||
# 이력: 2026-06-11 맥미니 모델 확정까지 8키 홀드 → 同日 Qwen3.6-27B-6bit 전환과 함께 해제([]).
|
||||
pipeline:
|
||||
held_stages: ["classify", "summarize", "deep_summary", "digest", "briefing",
|
||||
"study_explanation", "study_session_analysis", "study_memo_card"]
|
||||
held_stages: []
|
||||
|
||||
@@ -126,11 +126,11 @@ async def test_deep_conversational_no_sources(client, monkeypatch):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deep_probe_fail_503(client, monkeypatch):
|
||||
"""probe 실패(router 미도달) → 첫 바이트 전 503 macbook_unavailable."""
|
||||
"""probe 실패(router 미도달) → 첫 바이트 전 503 router_unreachable."""
|
||||
monkeypatch.setattr(eid_chat, "_probe_router_reachable", _async_false)
|
||||
r = await client.post("/api/eid/chat", json=_DEEP)
|
||||
assert r.status_code == 503
|
||||
assert r.json()["error_reason"] == "macbook_unavailable"
|
||||
assert r.json()["error_reason"] == "router_unreachable"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -104,7 +104,7 @@ async def test_anthropic_router_url_blocked(monkeypatch):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deep_mode_alias_and_sse_line_rewrite(monkeypatch):
|
||||
"""deep → qwen-macbook alias, system 은 messages[0] 단일 주입, 라인 단위 정화 중계."""
|
||||
"""deep → mac-mini-default alias (맥북 백지화 2026-06-11), system 은 messages[0] 단일 주입, 라인 단위 정화 중계."""
|
||||
seen: dict = {}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
@@ -139,7 +139,7 @@ async def test_deep_mode_alias_and_sse_line_rewrite(monkeypatch):
|
||||
]
|
||||
assert seen["url"].endswith("/v1/chat/completions")
|
||||
body = seen["json"]
|
||||
assert body["model"] == "qwen-macbook"
|
||||
assert body["model"] == "mac-mini-default"
|
||||
assert body["stream"] is True
|
||||
assert body["max_tokens"] == 2048
|
||||
assert body["temperature"] == 0.4
|
||||
@@ -202,7 +202,7 @@ async def test_prestream_503_maps_reason(monkeypatch):
|
||||
with pytest.raises(BackendUnavailable) as ei:
|
||||
await anext(stream)
|
||||
assert ei.value.reason == "macbook_unavailable"
|
||||
assert ei.value.backend_name == "qwen-macbook"
|
||||
assert ei.value.backend_name == "mac-mini-default"
|
||||
finally:
|
||||
await c.close()
|
||||
|
||||
@@ -253,7 +253,7 @@ async def test_prestream_400_raises_valueerror_failloud(monkeypatch):
|
||||
c = EidAIClient()
|
||||
try:
|
||||
stream = c.call_stream("deep", _MSG, "sys")
|
||||
with pytest.raises(ValueError, match="router rejected alias='qwen-macbook'"):
|
||||
with pytest.raises(ValueError, match="router rejected alias='mac-mini-default'"):
|
||||
await anext(stream)
|
||||
finally:
|
||||
await c.close()
|
||||
@@ -290,7 +290,7 @@ async def test_stream_deadline_exceeded(monkeypatch):
|
||||
async for _ in stream:
|
||||
pass
|
||||
assert ei.value.reason == "stream_deadline_exceeded"
|
||||
assert ei.value.backend_name == "qwen-macbook"
|
||||
assert ei.value.backend_name == "mac-mini-default"
|
||||
finally:
|
||||
await c.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user