"""Engine gateway HTTP and SSE contract. Keep this module adapter-neutral. The Python API client, the current Python gateway, and a future Node.js gateway must preserve these shapes. """ from __future__ import annotations import json from typing import Any, Literal, Optional from pydantic import BaseModel, Field AIRole = Literal["client", "counselor", "evaluator"] EngineMessageRole = Literal["system", "user", "assistant"] EngineGatewaySseEvent = Literal["token", "done", "error"] ENGINE_GATEWAY_SSE_TOKEN: EngineGatewaySseEvent = "token" ENGINE_GATEWAY_SSE_DONE: EngineGatewaySseEvent = "done" ENGINE_GATEWAY_SSE_ERROR: EngineGatewaySseEvent = "error" ENGINE_GATEWAY_SSE_EVENTS: tuple[EngineGatewaySseEvent, ...] = ( ENGINE_GATEWAY_SSE_TOKEN, ENGINE_GATEWAY_SSE_DONE, ENGINE_GATEWAY_SSE_ERROR, ) class EngineMessage(BaseModel): role: EngineMessageRole content: str cache: bool = False class GenerateRequest(BaseModel): ai_role: AIRole = "client" messages: list[EngineMessage] model: Optional[str] = None max_tokens: int = 1024 temperature: float = 0.7 structured_schema: Optional[dict[str, Any]] = None session_id: Optional[str] = None metadata: dict[str, Any] = Field(default_factory=dict) class StreamRequest(GenerateRequest): """SSE stream request for live client AI turns.""" class GenerateResponse(BaseModel): text: str model: str provider: str tokens_in: int = 0 tokens_out: int = 0 cost_usd: float = 0.0 inference_geo: Optional[str] = None structured: Optional[dict[str, Any]] = None class StreamTokenEvent(BaseModel): text: str class StreamDoneEvent(BaseModel): provider: str model: str tokens_in: int = 0 tokens_out: int = 0 cost_usd: float = 0.0 turns: int = 0 class StreamErrorEvent(BaseModel): detail: str def sse_frame(event: EngineGatewaySseEvent, payload: BaseModel | dict[str, Any]) -> str: if isinstance(payload, BaseModel): body = payload.model_dump() else: body = payload return f"event: {event}\ndata: {json.dumps(body, ensure_ascii=False)}\n\n"