defmodule BilldogEng.Llm do @moduledoc """ LLM observability — capture a model invocation as a trace span. Wraps `POST /llm/trace`. The wire payload is camelCase (matching the edge function's Zod schema) and authenticates with `X-BillDog-API-Key`. Mirrors the Node SDK `llm.ts`. """ alias BilldogEng.Transport @doc """ Record a single LLM trace span. Required params (keyword list): `:trace_id`, `:span_id`, `:model`, `:input_text`, `:output_text`. Optional: `:parent_span_id`, `:prompt_tokens`, `:completion_tokens`, `:duration_ms`, `:cost_usd`, `:properties`, `:metadata`, `:timestamp` (ISO-8601; defaults to now). """ @spec capture_trace(BilldogEng.t(), keyword()) :: {:ok, term()} | {:error, BilldogEng.Error.t()} def capture_trace(client, params) do body = %{ api_key: client.api_key, traceId: Keyword.fetch!(params, :trace_id), spanId: Keyword.fetch!(params, :span_id), parentSpanId: Keyword.get(params, :parent_span_id, ""), model: Keyword.fetch!(params, :model), inputText: Keyword.fetch!(params, :input_text), outputText: Keyword.fetch!(params, :output_text), promptTokens: Keyword.get(params, :prompt_tokens, 0), completionTokens: Keyword.get(params, :completion_tokens, 0), durationMs: Keyword.get(params, :duration_ms, 0), costUsd: Keyword.get(params, :cost_usd, 0), properties: Keyword.get(params, :properties, %{}), metadata: Keyword.get(params, :metadata, %{}), timestamp: Keyword.get(params, :timestamp) || iso_now() } Transport.request(client.transport, path: "/llm/trace", body: body, headers: [{"x-billdog-api-key", client.api_key}], gzip: false ) end defp iso_now do DateTime.utc_now() |> DateTime.to_iso8601() end end