billdogeng 1.0.0-beta.1

Official BilldogEng server SDK for Rust — Analytics, Feature Flags (remote + local eval), Surveys, Messaging, and LLM observability.
Documentation
//! 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`.

use serde_json::{json, Value};

use crate::error::Result;
use crate::transport::{RequestOptions, Transport};
use crate::types::LlmTraceParams;

/// LLM observability client.
pub struct Llm {
    transport: Transport,
    api_key: String,
}

impl Llm {
    /// Construct the LLM client.
    pub fn new(transport: Transport, api_key: String) -> Self {
        Self { transport, api_key }
    }

    /// Record a single LLM trace span.
    pub fn capture_trace(&self, params: &LlmTraceParams) -> Result<Value> {
        let body = json!({
            "api_key": self.api_key,
            "traceId": params.trace_id,
            "spanId": params.span_id,
            "parentSpanId": params.parent_span_id.clone().unwrap_or_default(),
            "model": params.model,
            "inputText": params.input_text,
            "outputText": params.output_text,
            "promptTokens": params.prompt_tokens.unwrap_or(0),
            "completionTokens": params.completion_tokens.unwrap_or(0),
            "durationMs": params.duration_ms.unwrap_or(0),
            "costUsd": params.cost_usd.unwrap_or(0.0),
            "properties": params.properties.clone().unwrap_or_default(),
            "metadata": params.metadata.clone().unwrap_or_default(),
            "timestamp": params.timestamp.clone().unwrap_or_else(now_iso8601),
        });

        let opts = RequestOptions::post("/llm/trace", body)
            .header("X-BillDog-API-Key", &self.api_key)
            .gzip(false);

        self.transport.request(&opts)
    }
}

/// Minimal ISO-8601 UTC timestamp (seconds precision) without external crates.
fn now_iso8601() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let secs = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    // Civil date from epoch seconds (Howard Hinnant's algorithm).
    let days = (secs / 86_400) as i64;
    let rem = secs % 86_400;
    let (hour, min, sec) = (rem / 3600, (rem % 3600) / 60, rem % 60);
    let z = days + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let year = if m <= 2 { y + 1 } else { y };
    format!(
        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.000Z",
        year, m, d, hour, min, sec
    )
}