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