Stabilize runtime auth and E2E coverage

This commit is contained in:
Yun Chan 2026-06-26 14:47:00 +09:00
parent 6a3e3b541c
commit 188e899394
133 changed files with 55987 additions and 6775 deletions

View file

@ -15,6 +15,7 @@ text 는 *PII 마스킹 후(text_masked)* 만 보낸다 (R7/F-03, 마스킹은
from __future__ import annotations
import asyncio
from typing import Any, AsyncIterator, Literal, Optional
import httpx
@ -72,10 +73,18 @@ 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.default_model: Optional[str] = None
self._client: Optional[httpx.AsyncClient] = None
self._lock = asyncio.Lock()
async def startup(self) -> None:
self._client = httpx.AsyncClient(
async with self._lock:
if self._client is None:
self._client = self._new_client()
def _new_client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(
base_url=self.base_url,
timeout=httpx.Timeout(
settings.engine_timeout,
@ -84,9 +93,42 @@ class EngineClient:
)
async def shutdown(self) -> None:
if self._client is not None:
await self._client.aclose()
self._client = None
async with self._lock:
if self._client is not None:
await self._client.aclose()
self._client = None
@staticmethod
def _model_override(model: Optional[str]) -> Optional[str]:
value = (model or "").strip()
if not value or value == "gateway-default":
return None
return value
async def configure(
self,
*,
base_url: str,
engine_mode: str,
default_model: Optional[str] = None,
) -> None:
next_url = base_url.rstrip("/")
next_model = self._model_override(default_model)
async with self._lock:
url_changed = next_url != self.base_url
self.base_url = next_url
self.engine_mode = engine_mode
self.default_model = next_model
if self._client is not None and url_changed:
old_client = self._client
self._client = self._new_client()
await old_client.aclose()
def _payload(self, req: GenerateRequest) -> dict[str, Any]:
payload = req.model_dump(exclude_none=True)
if self.default_model and "model" not in payload:
payload["model"] = self.default_model
return payload
@property
def client(self) -> httpx.AsyncClient:
@ -95,16 +137,40 @@ class EngineClient:
return self._client
async def health(self) -> bool:
return bool((await self.health_detail()).get("ok"))
async def health_detail(self) -> dict[str, Any]:
try:
r = await self.client.get("/health")
return r.status_code == 200
except httpx.HTTPError:
return False
r = await self.client.get("/ready")
if r.status_code == 404:
live = await self.client.get("/health")
return {
"ok": live.status_code == 200,
"detail": "gateway liveness only; readiness endpoint unavailable",
"status_code": live.status_code,
}
payload: dict[str, Any] = {}
try:
payload = r.json()
except ValueError:
payload = {}
return {
"ok": r.status_code == 200 and bool(payload.get("ok", False)),
"detail": str(payload.get("detail") or r.text or "engine readiness failed"),
"status_code": r.status_code,
"cached": bool(payload.get("cached", False)),
}
except httpx.HTTPError as exc:
return {
"ok": False,
"detail": f"engine readiness transport error: {exc}",
"status_code": None,
}
async def generate(self, req: GenerateRequest) -> GenerateResponse:
"""단발 생성. TODO: 게이트웨이 응답 스키마 확정 후 cost 텔레메트리 turns 적재."""
try:
r = await self.client.post("/v1/generate", json=req.model_dump(exclude_none=True))
r = await self.client.post("/v1/generate", json=self._payload(req))
r.raise_for_status()
except httpx.HTTPStatusError as e:
raise EngineError(f"engine generate {e.response.status_code}: {e.response.text}") from e
@ -121,7 +187,7 @@ class EngineClient:
"""
try:
async with self.client.stream(
"POST", "/v1/stream", json=req.model_dump(exclude_none=True)
"POST", "/v1/stream", json=self._payload(req)
) as r:
r.raise_for_status()
async for line in r.aiter_lines():