Spaces:
Running
Running
| """Pipeline observability helpers for step, API, LLM, and cost tracking.""" | |
| from __future__ import annotations | |
| from contextlib import asynccontextmanager | |
| from contextvars import ContextVar | |
| from time import perf_counter | |
| from typing import Any, AsyncIterator | |
| from backend.config import settings | |
| _current_step: ContextVar[dict[str, Any] | None] = ContextVar("current_pipeline_step", default=None) | |
| def _elapsed_ms(start: float) -> int: | |
| return max(0, round((perf_counter() - start) * 1000)) | |
| def current_step_id() -> int | None: | |
| """pipeline_step id of the step currently running (None outside a step). | |
| Lets pipeline code link evidence_items / provider_calls to the live step | |
| without threading the id through every call. | |
| """ | |
| context = _current_step.get() | |
| return context.get("step_id") if context else None | |
| async def tracked_step( | |
| access_key: str, | |
| step_name: str, | |
| step_order: float | None = None, | |
| metadata: dict[str, Any] | None = None, | |
| ) -> AsyncIterator[dict[str, Any]]: | |
| from backend.storage import jobs as job_store | |
| started = perf_counter() | |
| step = await job_store.start_pipeline_step( | |
| access_key, | |
| step_name, | |
| step_order=step_order, | |
| metadata=metadata, | |
| ) | |
| token = _current_step.set( | |
| { | |
| "access_key": access_key, | |
| "step_id": step.get("id"), | |
| "step_name": step_name, | |
| } | |
| ) | |
| try: | |
| yield step | |
| except Exception as exc: | |
| await job_store.finish_pipeline_step( | |
| access_key, | |
| step["id"], | |
| "failed", | |
| latency_ms=_elapsed_ms(started), | |
| error=f"{type(exc).__name__}: {exc}", | |
| ) | |
| raise | |
| else: | |
| # The body may stash its output on the step dict (see _run_tracked_step). | |
| await job_store.finish_pipeline_step( | |
| access_key, | |
| step["id"], | |
| "completed", | |
| latency_ms=_elapsed_ms(started), | |
| error=None, | |
| output_json=step.get("_output_json"), | |
| output_summary=step.get("_output_summary"), | |
| ) | |
| finally: | |
| _current_step.reset(token) | |
| async def record_llm_call( | |
| provider: str, | |
| model: str, | |
| response_format: str | None, | |
| prompt_tokens: int | None, | |
| completion_tokens: int | None, | |
| total_tokens: int | None, | |
| latency_ms: int | None, | |
| status: str, | |
| error: str | None, | |
| ) -> None: | |
| context = _current_step.get() | |
| if context is None: | |
| return | |
| from backend.storage import jobs as job_store | |
| await job_store.record_llm_call( | |
| access_key=context["access_key"], | |
| step_id=context.get("step_id"), | |
| step_name=context.get("step_name"), | |
| provider=provider, | |
| model=model, | |
| response_format=response_format, | |
| prompt_tokens=prompt_tokens, | |
| completion_tokens=completion_tokens, | |
| total_tokens=total_tokens, | |
| input_price_per_1m=_input_price_for_provider(provider), | |
| output_price_per_1m=_output_price_for_provider(provider), | |
| latency_ms=latency_ms, | |
| status=status, | |
| error=error, | |
| ) | |
| async def record_api_call( | |
| provider: str, | |
| operation: str, | |
| status_code: int | None = None, | |
| latency_ms: int | None = None, | |
| request_count: int = 1, | |
| status: str = "success", | |
| error: str | None = None, | |
| cost_usd: float | None = None, | |
| metadata: dict[str, Any] | None = None, | |
| ) -> None: | |
| context = _current_step.get() | |
| if context is None: | |
| return | |
| from backend.storage import jobs as job_store | |
| await job_store.record_api_call( | |
| access_key=context["access_key"], | |
| step_id=context.get("step_id"), | |
| step_name=context.get("step_name"), | |
| provider=provider, | |
| operation=operation, | |
| status_code=status_code, | |
| latency_ms=latency_ms, | |
| request_count=request_count, | |
| status=status, | |
| error=error, | |
| cost_usd=cost_usd, | |
| metadata=metadata, | |
| ) | |
| def _input_price_for_provider(provider: str) -> float | None: | |
| if provider == "openai": | |
| return settings.openai_input_price_per_1m | |
| if provider == "anthropic": | |
| return settings.anthropic_input_price_per_1m | |
| if provider == "gemini": | |
| return settings.gemini_input_price_per_1m | |
| if provider == "openrouter": | |
| return settings.openrouter_input_price_per_1m | |
| return None | |
| def _output_price_for_provider(provider: str) -> float | None: | |
| if provider == "openai": | |
| return settings.openai_output_price_per_1m | |
| if provider == "anthropic": | |
| return settings.anthropic_output_price_per_1m | |
| if provider == "gemini": | |
| return settings.gemini_output_price_per_1m | |
| if provider == "openrouter": | |
| return settings.openrouter_output_price_per_1m | |
| return None | |