Stabilize runtime auth and E2E coverage
This commit is contained in:
parent
6a3e3b541c
commit
188e899394
133 changed files with 55987 additions and 6775 deletions
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue