Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7a5cd81
wip
yujonglee-berri Sep 30, 2026
fdbd54e
feat(traces): establish shared Rust storage foundation
yujonglee-berri Sep 30, 2026
ed9ac76
fix(traces): escape ClickHouse text parameters
yujonglee-berri Sep 30, 2026
194b88a
test(traces): exercise response cap with bounded strings
yujonglee-berri Sep 30, 2026
bf4150f
fix(traces): remove unnecessary lint expectation
yujonglee-berri Sep 30, 2026
98711e7
fix(traces): encode ClickHouse timestamp units in Rust
yujonglee-berri Sep 30, 2026
6c67eb5
test(traces): mark exception match as a regex
yujonglee-berri Sep 30, 2026
573c0ac
refactor(traces): execute schema setup in Rust
yujonglee-berri Sep 30, 2026
874742b
refactor(traces): use shared logging execution wrapper
yujonglee-berri Sep 30, 2026
62aaaf4
docs(traces): replace foundation README with boundary rules
yujonglee-berri Sep 30, 2026
535c3ad
fix(traces): use current bridge execution facade
yujonglee-berri Sep 30, 2026
c7482fd
feat(tracing): add tenant scoped agent tracing integration
yujonglee-berri Sep 30, 2026
799ba8c
fix(tracing): include new tests in CI and meet UI lint budgets
yujonglee-berri Sep 30, 2026
b0c1c56
style(tracing): format Rust bridge facade
yujonglee-berri Sep 30, 2026
495f52a
feat(tracing): render traces as Markdown for Claude / Codex
ishaan-berri Sep 30, 2026
af2bbc3
feat(tracing): fetch all span input/output in one query for exports
ishaan-berri Sep 30, 2026
fa57efa
feat(tracing): add TraceReceiver.get_trace_markdown
ishaan-berri Sep 30, 2026
8ed0c6e
feat(proxy): support format=md on GET /v1/traces/{trace_id}
ishaan-berri Sep 30, 2026
bd80952
fix(auth): match placeholder native routes like /v1/traces/{trace_id}
ishaan-berri Sep 30, 2026
1d1a5d8
feat(tracing): add markdown export field limit constant
ishaan-berri Sep 30, 2026
baaee82
test(tracing): cover Markdown trace export
ishaan-berri Sep 30, 2026
e3f5f65
test(auth): trace routes are callable by virtual keys
ishaan-berri Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ jobs:

- shard: integrations
artifact-name: integrations
test-path: ""
test-path: >-
tests/test_litellm/integrations
tests/test_litellm/tracing
unit-flag: integrations
workers: 2
reruns: 3
Expand Down
86 changes: 82 additions & 4 deletions litellm-rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions litellm-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ repository = "https://github.com/BerriAI/litellm"
litellm-config = { path = "crates/config" }
litellm-router = { path = "crates/router" }
litellm-tracing = { path = "crates/tracing" }
litellm-traces = { path = "crates/traces" }
litellm-core = { path = "crates/core" }
litellm-gateway-mcp = { path = "crates/gateway-mcp" }
litellm-gateway = { path = "crates/gateway" }
Expand Down
1 change: 1 addition & 0 deletions litellm-rust/crates/python-bridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ tiktoken = ["litellm-token-counter/tiktoken"]
[dependencies]
fancy-regex.workspace = true
litellm-tracing.workspace = true
litellm-traces.workspace = true
litellm-host.workspace = true
bytes.workspace = true
futures-util.workspace = true
Expand Down
4 changes: 4 additions & 0 deletions litellm-rust/crates/python-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ mod _native {
use crate::routes::responses::{ResponsesWebSocketConnection, aresponses, responses};
#[pymodule_export]
use crate::routes::token_counter::TokenCounter;
#[pymodule_export]
use crate::routes::traces::{NativeTraceStorage, trace_decode_otlp};
#[cfg(feature = "huggingface")]
#[pymodule_export]
use crate::tokenizer::HuggingFaceEncoding;
Expand Down Expand Up @@ -107,6 +109,8 @@ mod tests {
"aresponses",
"ResponsesWebSocketConnection",
"NativeDiagnosticProcessor",
"NativeTraceStorage",
"trace_decode_otlp",
"TokenCounter",
"Tokenizer",
"gil_stats",
Expand Down
1 change: 1 addition & 0 deletions litellm-rust/crates/python-bridge/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) mod messages;
pub(crate) mod ocr;
pub(crate) mod responses;
pub(crate) mod token_counter;
pub(crate) mod traces;

use litellm_callbacks_legacy_python::LoggingOperation;
use litellm_callbacks_legacy_python::{LegacyLogging, PublicCall};
Expand Down
134 changes: 134 additions & 0 deletions litellm-rust/crates/python-bridge/src/routes/traces.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::collections::BTreeMap;

use litellm_http::ClientVariant;
use litellm_traces::{Connection, Error, InsertTable, Parameter};
use pyo3::{
exceptions::{PyOverflowError, PyRuntimeError, PyValueError},
prelude::*,
};

fn map_error(error: Error) -> PyErr {
match error {
Error::InvalidRow | Error::InvalidTable | Error::InvalidSchema | Error::EmptySql => {
PyValueError::new_err(error.to_string())
}
Error::InsertTooLarge => PyOverflowError::new_err(error.to_string()),
Error::InvalidUrl
| Error::QueryFailed(_)
| Error::InsertFailed(_)
| Error::SchemaFailed(_)
| Error::ResponseTooLarge
| Error::InvalidResponse
| Error::Transport => PyRuntimeError::new_err(error.to_string()),
}
}

#[pyclass]
pub struct NativeTraceStorage {
database: String,
writer: Connection,
reader: Option<Connection>,
}

#[pymethods]
impl NativeTraceStorage {
#[new]
fn new(database: String, url: &str, reader_url: Option<&str>) -> PyResult<Self> {
litellm_traces::schema_statements(&database, 1, 1).map_err(map_error)?;
Ok(Self {
writer: Connection::writer(url).map_err(map_error)?,
reader: reader_url
.map(|value| Connection::reader(value, &database))
.transpose()
.map_err(map_error)?,
database,
})
}

fn ensure_schema<'py>(
&self,
py: Python<'py>,
trace_retention_days: u32,
spend_log_retention_days: u32,
) -> PyResult<Bound<'py, PyAny>> {
let client = crate::http::host_client(py, ClientVariant::NoRedirect)?;
let connection = self.writer.clone();
let database = self.database.clone();
crate::execution::run_async(
py,
async move {
litellm_traces::ensure_schema(
&client,
&connection,
&database,
trace_retention_days,
spend_log_retention_days,
)
.await
},
map_error,
)
}

fn insert_rows<'py>(
&self,
py: Python<'py>,
table: &str,
#[pyo3(from_py_with = litellm_host_python::from_py_argument)] rows: Vec<
BTreeMap<String, serde_json::Value>,
>,
) -> PyResult<Bound<'py, PyAny>> {
let table = InsertTable::parse(table).map_err(map_error)?;
let client = crate::http::host_client(py, ClientVariant::NoRedirect)?;
let connection = self.writer.clone();
let database = self.database.clone();
crate::execution::run_async(
py,
async move {
litellm_traces::insert_rows(&client, &connection, &database, table, rows).await
},
map_error,
)
}

fn query<'py>(
&self,
py: Python<'py>,
sql: String,
#[pyo3(from_py_with = litellm_host_python::from_py_argument)] parameters: BTreeMap<
String,
Parameter,
>,
) -> PyResult<Bound<'py, PyAny>> {
let connection = self.reader.clone().ok_or_else(|| {
PyRuntimeError::new_err("Trace reads require a separate ClickHouse reader URL")
})?;
let client = crate::http::host_client(py, ClientVariant::NoRedirect)?;
crate::execution::run_async(
py,
async move { litellm_traces::execute_read(&client, &connection, &sql, &parameters).await },
map_error,
)
}
}

#[pyfunction]
pub fn trace_decode_otlp<'py>(
py: Python<'py>,
body: &[u8],
content_type: Option<&str>,
content_encoding: Option<&str>,
max_decompressed_bytes: usize,
) -> PyResult<Bound<'py, PyAny>> {
let spans = py
.detach(|| {
litellm_traces::decode_otlp(
body,
content_type,
content_encoding,
max_decompressed_bytes,
)
})
.map_err(|error| PyValueError::new_err(error.to_string()))?;
litellm_host_python::Pythonized(spans).into_pyobject(py)
}
6 changes: 6 additions & 0 deletions litellm-rust/crates/traces/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- Rust owns OTLP wire decoding, ClickHouse schema, row encoding, named reads, connection validation and transport
- Keep this crate independent of Python; PyO3 conversion and public Python exceptions belong in `python-bridge`
- Keep the SQL migrations here as the only ClickHouse schema definition
- Use typed query parameters and a dedicated SELECT-only reader with server-side limits
- Bound insert time and encoded bytes; make retry deduplication behavior explicit for supported ClickHouse versions
- Test storage behavior through the crate's public API against ClickHouse
24 changes: 24 additions & 0 deletions litellm-rust/crates/traces/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "litellm-traces"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true

[dependencies]
base64.workspace = true
flate2.workspace = true
opentelemetry-proto = { version = "0.33.0", default-features = false, features = ["gen-tonic-messages", "trace", "with-serde"] }
prost = "0.14.4"
time = { workspace = true, features = ["formatting"] }
litellm-http.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
url.workspace = true

[dev-dependencies]
litellm-http = { workspace = true, features = ["test-support"] }
rstest.workspace = true
testcontainers-modules = { version = "0.15.0", features = ["clickhouse"] }
tokio.workspace = true
Loading
Loading