"""Adapters that persist :class:`packages.llm.cost.AICallRecord`. The SQLAlchemy recorder deliberately opens its **own** session instead of joining the caller's transaction. If a request later rolls back — quota refund, moderation rejection, an unhandled error — the cost still happened and must still be attributable. Losing those rows would silently under-report spend, which is exactly the blind spot 架构文档 §13 forbids. """ from __future__ import annotations from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from packages.llm.cost import AICallRecord from packages.shared.models import AiCall from packages.shared.observability import LLM_CALLS, LLM_COST_CENTS, LLM_LATENCY, LLM_TOKENS class SqlAlchemyAICallRecorder: """Writes one ``ob_ai_call`` row per provider attempt.""" def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None: self._session_factory = session_factory async def record(self, record: AICallRecord) -> None: async with self._session_factory() as session: session.add( AiCall( call_id=record.call_id, user_id=record.user_id, scene=record.scene, provider=record.provider, model=record.model, prompt_id=record.prompt_id, prompt_version=record.prompt_version, tokens_in=record.usage.tokens_in, tokens_out=record.usage.tokens_out, tokens_cached=record.usage.tokens_cached, tokens_reasoning=record.usage.tokens_reasoning, latency_ms=record.latency_ms, cost_cents=record.cost_cents, rate_tier=record.rate_tier, success=record.success, error_code=record.error_code, error_message=record.error_message, ref_type=record.ref_type, ref_id=record.ref_id, ) ) await session.commit() class PrometheusAICallRecorder: """Feeds the AI 成本看板 (架构文档 §15) straight from the same records.""" async def record(self, record: AICallRecord) -> None: labels = (record.scene, record.provider, record.model) LLM_CALLS.labels(*labels, "true" if record.success else "false").inc() LLM_LATENCY.labels(*labels).observe(record.latency_ms / 1000) LLM_COST_CENTS.labels(*labels).inc(float(record.cost_cents)) LLM_TOKENS.labels(*labels, "in").inc(record.usage.tokens_in) LLM_TOKENS.labels(*labels, "out").inc(record.usage.tokens_out) LLM_TOKENS.labels(*labels, "cached").inc(record.usage.tokens_cached) # 思考模式 token 单独出一条曲线:它按输出价计费,是最容易悄悄涨上去的一项。 LLM_TOKENS.labels(*labels, "reasoning").inc(record.usage.tokens_reasoning)