feat: 운영 안정성과 세션 음성 경험 개선
This commit is contained in:
parent
facc4ad2d9
commit
c788343467
95 changed files with 8431 additions and 1785 deletions
|
|
@ -1,12 +1,14 @@
|
|||
"""엔진 게이트웨이 HTTP 클라이언트.
|
||||
|
||||
⚠️ 게이트웨이 자체(apps/api/engine_gateway/)는 람다가 직접 만든다. 여기는 *호출부*만.
|
||||
게이트웨이가 provider 라우팅(claude_api/claude_cli/openai/solar)·캐싱·상주 claude -p 풀을
|
||||
흡수한다(마스터플랜 §0, R1). 이 백엔드는 ENGINE_URL 로 HTTP 호출만 한다.
|
||||
게이트웨이가 provider 라우팅(claude_cli/claude_api/codex_cli/agy_cli/openai/solar)·
|
||||
모델 탐색·캐싱·상주 claude -p 풀을 흡수한다(마스터플랜 §0, R1).
|
||||
이 백엔드는 ENGINE_URL로 HTTP 호출만 한다.
|
||||
|
||||
계약 (람다와 합의할 게이트웨이 API):
|
||||
POST {ENGINE_URL}/v1/generate — 단발 생성 (평가 deep-loop 등)
|
||||
POST {ENGINE_URL}/v1/stream — SSE 토큰 스트림 (내담자 AI 응답)
|
||||
GET {ENGINE_URL}/v1/capabilities — provider별 사용 가능 모델·추론 강도
|
||||
GET {ENGINE_URL}/health
|
||||
|
||||
요청 바디는 3-AI 역할별 system 레이어(L0~L6, 설계서 §1.2)를 게이트웨이에 넘기되,
|
||||
|
|
@ -23,11 +25,14 @@ import httpx
|
|||
from .config import settings
|
||||
from .contracts.engine_gateway import (
|
||||
AIRole as AIRole,
|
||||
EngineCapabilitiesResponse,
|
||||
EngineMessage as EngineMessage,
|
||||
EngineGatewaySseLineDecoder,
|
||||
EngineGatewaySsePacket,
|
||||
EngineProvider,
|
||||
GenerateRequest,
|
||||
GenerateResponse,
|
||||
ReasoningEffort,
|
||||
StreamRequest,
|
||||
normalize_engine_gateway_model,
|
||||
)
|
||||
|
|
@ -43,7 +48,9 @@ class EngineClient:
|
|||
def __init__(self, base_url: Optional[str] = None) -> None:
|
||||
self.base_url = (base_url or settings.engine_url).rstrip("/")
|
||||
self.engine_mode = settings.engine_mode
|
||||
self.live_client_provider: Optional[EngineProvider] = settings.live_client_provider
|
||||
self.default_model: Optional[str] = None
|
||||
self.default_reasoning_effort: Optional[ReasoningEffort] = None
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
|
|
@ -71,8 +78,9 @@ class EngineClient:
|
|||
self,
|
||||
*,
|
||||
base_url: str,
|
||||
engine_mode: str,
|
||||
engine_mode: EngineProvider,
|
||||
default_model: Optional[str] = None,
|
||||
default_reasoning_effort: Optional[ReasoningEffort] = None,
|
||||
) -> None:
|
||||
next_url = base_url.rstrip("/")
|
||||
next_model = normalize_engine_gateway_model(default_model)
|
||||
|
|
@ -81,6 +89,7 @@ class EngineClient:
|
|||
self.base_url = next_url
|
||||
self.engine_mode = engine_mode
|
||||
self.default_model = next_model
|
||||
self.default_reasoning_effort = default_reasoning_effort
|
||||
if self._client is not None and url_changed:
|
||||
old_client = self._client
|
||||
self._client = self._new_client()
|
||||
|
|
@ -88,8 +97,22 @@ class EngineClient:
|
|||
|
||||
def _payload(self, req: GenerateRequest) -> dict[str, Any]:
|
||||
payload = req.model_dump(exclude_none=True)
|
||||
if self.default_model and "model" not in payload:
|
||||
provider = self.engine_mode
|
||||
if req.ai_role == "client" and req.session_id and self.live_client_provider:
|
||||
provider = self.live_client_provider
|
||||
if "provider" not in payload:
|
||||
payload["provider"] = provider
|
||||
# 관리자 기본 모델/추론 강도는 그 provider에 속한 값이다. 실시간 lane이
|
||||
# 다른 provider면 잘못된 모델 slug를 넘기지 않고 해당 provider 기본값을 쓴다.
|
||||
same_provider = payload["provider"] == self.engine_mode
|
||||
if same_provider and self.default_model and "model" not in payload:
|
||||
payload["model"] = self.default_model
|
||||
if (
|
||||
same_provider
|
||||
and self.default_reasoning_effort
|
||||
and "reasoning_effort" not in payload
|
||||
):
|
||||
payload["reasoning_effort"] = self.default_reasoning_effort
|
||||
return payload
|
||||
|
||||
@property
|
||||
|
|
@ -103,7 +126,12 @@ class EngineClient:
|
|||
|
||||
async def health_detail(self) -> dict[str, Any]:
|
||||
try:
|
||||
r = await self.client.get("/ready")
|
||||
params: dict[str, str] = {"provider": self.engine_mode}
|
||||
if self.default_model:
|
||||
params["model"] = self.default_model
|
||||
if self.default_reasoning_effort:
|
||||
params["reasoning_effort"] = self.default_reasoning_effort
|
||||
r = await self.client.get("/ready", params=params)
|
||||
if r.status_code == 404:
|
||||
live = await self.client.get("/health")
|
||||
return {
|
||||
|
|
@ -129,6 +157,33 @@ class EngineClient:
|
|||
"status_code": None,
|
||||
}
|
||||
|
||||
async def capabilities(
|
||||
self,
|
||||
*,
|
||||
provider: EngineProvider,
|
||||
base_url: str | None = None,
|
||||
force: bool = False,
|
||||
) -> EngineCapabilitiesResponse:
|
||||
target_url = (base_url or self.base_url).rstrip("/")
|
||||
params = {"provider": provider, "force": str(force).lower()}
|
||||
try:
|
||||
if target_url == self.base_url:
|
||||
response = await self.client.get("/v1/capabilities", params=params)
|
||||
else:
|
||||
async with httpx.AsyncClient(
|
||||
base_url=target_url,
|
||||
timeout=httpx.Timeout(30, connect=settings.engine_connect_timeout),
|
||||
) as client:
|
||||
response = await client.get("/v1/capabilities", params=params)
|
||||
response.raise_for_status()
|
||||
return EngineCapabilitiesResponse.model_validate(response.json())
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise EngineError(
|
||||
f"engine capabilities {exc.response.status_code}: {exc.response.text}"
|
||||
) from exc
|
||||
except (httpx.HTTPError, ValueError) as exc:
|
||||
raise EngineError(f"engine capabilities unavailable: {exc}") from exc
|
||||
|
||||
async def generate(self, req: GenerateRequest) -> GenerateResponse:
|
||||
"""단발 생성."""
|
||||
try:
|
||||
|
|
@ -172,6 +227,18 @@ class EngineClient:
|
|||
if packet is not None:
|
||||
yield packet
|
||||
|
||||
async def close_session(self, session_id: str) -> bool:
|
||||
"""회기 종료 시 게이트웨이의 상주 페르소나 프로세스를 회수한다."""
|
||||
if not session_id:
|
||||
return False
|
||||
try:
|
||||
response = await self.client.delete(f"/session/{session_id}")
|
||||
response.raise_for_status()
|
||||
return bool(response.json().get("closed"))
|
||||
except (httpx.HTTPError, ValueError):
|
||||
# DB 회기 종료 성공을 게이트웨이 정리 실패 때문에 되돌리지는 않는다.
|
||||
return False
|
||||
|
||||
|
||||
# 앱 전역 싱글톤 (main lifespan 에서 startup/shutdown)
|
||||
engine_client = EngineClient()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue