feat: 운영 안정성과 세션 음성 경험 개선
This commit is contained in:
parent
facc4ad2d9
commit
c788343467
95 changed files with 8431 additions and 1785 deletions
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
import time
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Annotated, Literal
|
||||
from typing import Annotated, Literal, cast
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
|
@ -25,7 +25,14 @@ from ..auth_sessions import (
|
|||
upsert_managed_user,
|
||||
)
|
||||
from ..config import settings
|
||||
from ..contracts.engine_gateway import ENGINE_GATEWAY_DEFAULT_MODEL_SENTINEL
|
||||
from ..contracts.engine_gateway import (
|
||||
ENGINE_PROVIDER_DEFAULTS,
|
||||
ENGINE_PROVIDERS,
|
||||
ENGINE_REASONING_EFFORTS,
|
||||
EngineCapabilitiesResponse,
|
||||
EngineProvider,
|
||||
ReasoningEffort,
|
||||
)
|
||||
from ..db import acquire, get_pool, healthcheck
|
||||
from ..deps import Principal, require_admin_access
|
||||
from ..engine_client import engine_client
|
||||
|
|
@ -269,9 +276,10 @@ class AdminTicketsResponse(BaseModel):
|
|||
|
||||
|
||||
class AdminEngineConfigResponse(BaseModel):
|
||||
engine_mode: str
|
||||
engine_mode: EngineProvider
|
||||
engine_url: str
|
||||
model: str
|
||||
reasoning_effort: ReasoningEffort | None = None
|
||||
updated_by: str | None = None
|
||||
updated_at: float | None = None
|
||||
durable: bool = False
|
||||
|
|
@ -282,6 +290,7 @@ class AdminEngineConfigPatch(BaseModel):
|
|||
engine_mode: str | None = None
|
||||
engine_url: str | None = None
|
||||
model: str | None = None
|
||||
reasoning_effort: str | None = None
|
||||
|
||||
|
||||
class AdminTicketPatch(BaseModel):
|
||||
|
|
@ -977,7 +986,7 @@ class AdminUserDeleteResponse(BaseModel):
|
|||
|
||||
|
||||
_ENGINE_CONFIG: AdminEngineConfigResponse | None = None
|
||||
ENGINE_MODES = {"claude_api", "claude_cli", "openai", "solar"}
|
||||
ENGINE_MODES = set(ENGINE_PROVIDERS)
|
||||
ENGINE_MODE_ALIASES = {"messages_api": "claude_api"}
|
||||
|
||||
|
||||
|
|
@ -992,23 +1001,37 @@ def _normalize_email(value: str) -> str:
|
|||
|
||||
|
||||
def _default_engine_config() -> AdminEngineConfigResponse:
|
||||
default_model, default_effort = ENGINE_PROVIDER_DEFAULTS[settings.engine_mode]
|
||||
return AdminEngineConfigResponse(
|
||||
engine_mode=settings.engine_mode,
|
||||
engine_url=settings.engine_url,
|
||||
model=ENGINE_GATEWAY_DEFAULT_MODEL_SENTINEL,
|
||||
model=default_model,
|
||||
reasoning_effort=default_effort,
|
||||
durable=False,
|
||||
source="runtime_default",
|
||||
)
|
||||
|
||||
|
||||
def _normalize_engine_mode(value: str) -> str:
|
||||
def _normalize_engine_mode(value: str) -> EngineProvider:
|
||||
mode = ENGINE_MODE_ALIASES.get(value.strip(), value.strip())
|
||||
if mode not in ENGINE_MODES:
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"unsupported engine mode {value}",
|
||||
)
|
||||
return mode
|
||||
return cast(EngineProvider, mode)
|
||||
|
||||
|
||||
def _normalize_reasoning_effort(value: str | None) -> ReasoningEffort | None:
|
||||
effort = (value or "").strip().lower()
|
||||
if not effort:
|
||||
return None
|
||||
if effort not in ENGINE_REASONING_EFFORTS:
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"unsupported reasoning effort {value}",
|
||||
)
|
||||
return cast(ReasoningEffort, effort)
|
||||
|
||||
|
||||
def _normalize_engine_url(value: str) -> str:
|
||||
|
|
@ -1071,6 +1094,7 @@ def _engine_config_from_row(row) -> AdminEngineConfigResponse:
|
|||
engine_mode=_normalize_engine_mode(row["engine_mode"]),
|
||||
engine_url=_normalize_engine_url(row["engine_url"]),
|
||||
model=row["model"],
|
||||
reasoning_effort=_normalize_reasoning_effort(row.get("reasoning_effort")),
|
||||
updated_by=row["updated_by"],
|
||||
updated_at=_updated_at_ts(row["updated_at"]),
|
||||
durable=True,
|
||||
|
|
@ -1266,7 +1290,7 @@ async def _current_engine_config() -> AdminEngineConfigResponse:
|
|||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT engine_mode, engine_url, model, updated_by, updated_at
|
||||
SELECT engine_mode, engine_url, model, reasoning_effort, updated_by, updated_at
|
||||
FROM app.admin_engine_config
|
||||
WHERE id = TRUE
|
||||
"""
|
||||
|
|
@ -1286,6 +1310,7 @@ async def apply_engine_config_from_store() -> AdminEngineConfigResponse:
|
|||
base_url=config.engine_url,
|
||||
engine_mode=config.engine_mode,
|
||||
default_model=config.model,
|
||||
default_reasoning_effort=config.reasoning_effort,
|
||||
)
|
||||
return config
|
||||
|
||||
|
|
@ -1752,6 +1777,74 @@ async def get_engine_config(principal: AdminPrincipal) -> AdminEngineConfigRespo
|
|||
return await _current_engine_config()
|
||||
|
||||
|
||||
@router.get("/engine-capabilities", response_model=EngineCapabilitiesResponse)
|
||||
async def get_engine_capabilities(
|
||||
principal: AdminPrincipal,
|
||||
engine_mode: str | None = Query(default=None),
|
||||
engine_url: str | None = Query(default=None),
|
||||
force: bool = Query(default=False),
|
||||
) -> EngineCapabilitiesResponse:
|
||||
"""Return gateway-discovered models and reasoning levels for one provider."""
|
||||
|
||||
current = await _current_engine_config()
|
||||
provider = _normalize_engine_mode(engine_mode or current.engine_mode)
|
||||
capability_url = (
|
||||
_normalize_engine_url(engine_url)
|
||||
if engine_url is not None
|
||||
else current.engine_url
|
||||
)
|
||||
try:
|
||||
return await engine_client.capabilities(
|
||||
provider=provider,
|
||||
base_url=capability_url,
|
||||
force=force,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"engine capabilities unavailable: {exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
async def _validate_engine_selection(
|
||||
*,
|
||||
provider: EngineProvider,
|
||||
engine_url: str,
|
||||
model: str,
|
||||
reasoning_effort: ReasoningEffort | None,
|
||||
) -> None:
|
||||
try:
|
||||
capabilities = await engine_client.capabilities(
|
||||
provider=provider,
|
||||
base_url=engine_url,
|
||||
force=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"선택한 엔진의 모델 목록을 검증할 수 없습니다: {exc}",
|
||||
) from exc
|
||||
if not capabilities.available:
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=capabilities.detail or "선택한 엔진을 사용할 수 없습니다.",
|
||||
)
|
||||
selected = next((option for option in capabilities.models if option.id == model), None)
|
||||
if selected is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"{provider}에서 사용할 수 없는 모델입니다: {model}",
|
||||
)
|
||||
if (
|
||||
reasoning_effort is not None
|
||||
and reasoning_effort not in selected.reasoning_efforts
|
||||
):
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"{model}에서 사용할 수 없는 추론 강도입니다: {reasoning_effort}",
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/engine-config", response_model=AdminEngineConfigResponse)
|
||||
async def patch_engine_config(
|
||||
body: AdminEngineConfigPatch,
|
||||
|
|
@ -1763,10 +1856,28 @@ async def patch_engine_config(
|
|||
current = await _current_engine_config()
|
||||
next_mode = _normalize_engine_mode(body.engine_mode or current.engine_mode)
|
||||
next_url = _normalize_engine_url(body.engine_url or current.engine_url)
|
||||
next_model = (body.model or current.model).strip()
|
||||
if not next_model:
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="model is required",
|
||||
)
|
||||
next_effort = _normalize_reasoning_effort(
|
||||
body.reasoning_effort
|
||||
if "reasoning_effort" in body.model_fields_set
|
||||
else current.reasoning_effort
|
||||
)
|
||||
await _validate_engine_selection(
|
||||
provider=next_mode,
|
||||
engine_url=next_url,
|
||||
model=next_model,
|
||||
reasoning_effort=next_effort,
|
||||
)
|
||||
next_config = AdminEngineConfigResponse(
|
||||
engine_mode=next_mode,
|
||||
engine_url=next_url,
|
||||
model=(body.model or current.model).strip(),
|
||||
model=next_model,
|
||||
reasoning_effort=next_effort,
|
||||
updated_by=principal.email,
|
||||
updated_at=time.time(),
|
||||
durable=False,
|
||||
|
|
@ -1778,20 +1889,22 @@ async def patch_engine_config(
|
|||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.admin_engine_config (
|
||||
id, engine_mode, engine_url, model, updated_by, updated_at
|
||||
id, engine_mode, engine_url, model, reasoning_effort, updated_by, updated_at
|
||||
)
|
||||
VALUES (TRUE, $1, $2, $3, $4, now())
|
||||
VALUES (TRUE, $1, $2, $3, $4, $5, now())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
engine_mode = EXCLUDED.engine_mode,
|
||||
engine_url = EXCLUDED.engine_url,
|
||||
model = EXCLUDED.model,
|
||||
reasoning_effort = EXCLUDED.reasoning_effort,
|
||||
updated_by = EXCLUDED.updated_by,
|
||||
updated_at = now()
|
||||
RETURNING engine_mode, engine_url, model, updated_by, updated_at
|
||||
RETURNING engine_mode, engine_url, model, reasoning_effort, updated_by, updated_at
|
||||
""",
|
||||
next_config.engine_mode,
|
||||
next_config.engine_url,
|
||||
next_config.model,
|
||||
next_config.reasoning_effort,
|
||||
principal.email,
|
||||
)
|
||||
next_config = _engine_config_from_row(row)
|
||||
|
|
@ -1806,6 +1919,7 @@ async def patch_engine_config(
|
|||
base_url=next_config.engine_url,
|
||||
engine_mode=next_config.engine_mode,
|
||||
default_model=next_config.model,
|
||||
default_reasoning_effort=next_config.reasoning_effort,
|
||||
)
|
||||
return next_config
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue