Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
37ac6ce
feat(proxy): add logging_endpoints package init
ishaan-jaff Jun 23, 2026
8db98fe
feat(proxy): add POST /v1/callbacks/logs to replay logging payloads t…
ishaan-jaff Jun 23, 2026
ed3c448
feat(proxy): register callback_logs_router
ishaan-jaff Jun 23, 2026
4c78a20
test(proxy): add logging_endpoints test package init
ishaan-jaff Jun 23, 2026
63790ca
test(proxy): cover /v1/callbacks/logs replay, admin guard, and partia…
ishaan-jaff Jun 23, 2026
64c0a12
refactor(proxy): move callback-logs request/response models to litell…
ishaan-jaff Jun 23, 2026
86f85da
refactor(proxy): wrap callback-logs replay in CallbackLogsReplayer cl…
ishaan-jaff Jun 23, 2026
0727631
test(proxy): update callback-logs tests for class-based replayer and …
ishaan-jaff Jun 23, 2026
6bb369e
fix(proxy): cover /v1/callbacks/ in backend component allowlist
ishaan-jaff Jun 23, 2026
e791b2f
refactor(proxy): use builtin dict/list generics in callback-logs endp…
ishaan-jaff Jun 23, 2026
834af9f
refactor(proxy): use builtin dict/list generics in callback-logs types
ishaan-jaff Jun 23, 2026
123f138
chore(ui): regenerate schema.d.ts for /v1/callbacks/logs
ishaan-jaff Jun 23, 2026
e18342e
fix(proxy): force stream=False when replaying callback logs
ishaan-berri Jun 24, 2026
56ef43a
feat(litellm-rust): CustomLogger callback layer posting to /v1/callba…
ishaan-berri Jun 24, 2026
2af8b5e
feat(litellm-rust): RealTimeStreaming per-session log collector
ishaan-berri Jun 24, 2026
f62b35e
feat(litellm-rust): wire realtime logging into the splice (lock-free …
ishaan-berri Jun 24, 2026
5130ad7
docs(litellm-rust): ai-gateway realtime logging architecture
ishaan-berri Jun 24, 2026
a0fe2c5
docs(litellm-rust): document request-log egress to the LiteLLM contro…
ishaan-berri Jun 24, 2026
acc8ffa
feat(litellm-rust): make log-egress tunables env-overridable
ishaan-berri Jun 24, 2026
f79a5ed
docs(litellm-rust): document log-egress tuning env vars
ishaan-berri Jun 24, 2026
b66dc84
docs(litellm-rust): require constants in a crate-level constants.rs
ishaan-berri Jun 24, 2026
e58084c
refactor(litellm-rust): move ai-gateway constants into constants.rs
ishaan-berri Jun 24, 2026
0cc1896
ci: run logging_endpoints tests in the proxy-infra coverage shard
ishaan-berri Jun 24, 2026
5072e32
fix(litellm-rust): hash the master key before logging — never send th…
ishaan-berri Jun 24, 2026
ba71c73
fix(litellm-rust): observe realtime logging on upstream events only
ishaan-berri Jun 24, 2026
17c5cb6
feat(proxy): bound callback-logs batch + return per-record failures
ishaan-berri Jun 24, 2026
4fcc554
chore(ui): regenerate schema.d.ts for CallbackLogFailure / failures f…
ishaan-berri Jun 24, 2026
7efe8c0
Merge remote-tracking branch 'upstream/litellm_internal_staging' into…
ishaan-berri Jun 24, 2026
13e12f1
fix(constants): make MAX_CALLBACK_LOG_RECORDS a plain constant
ishaan-berri Jun 24, 2026
2f122a9
docs(litellm-rust): trim ai-gateway ARCHITECTURE.md to one diagram + …
ishaan-berri Jun 24, 2026
10ce828
docs(litellm-rust): tighten the README request-logging section
ishaan-berri Jun 24, 2026
9dbc013
docs(litellm-rust): ARCHITECTURE.md is just the diagram (gateway = in…
ishaan-berri Jun 24, 2026
d8733be
docs(litellm-rust): drop em-dashes from the request-logging section
ishaan-berri Jun 24, 2026
e4f9066
Merge remote-tracking branch 'upstream/litellm_internal_staging' into…
ishaan-berri Jun 24, 2026
7f9f4fd
chore: stop tracking stray .coverage artifact and gitignore it
ishaan-berri Jun 24, 2026
616ca9f
feat(proxy): namespace rust-gateway routes under /v1/rust_control_plane
ishaan-berri Jun 24, 2026
abd3db5
refactor(litellm-rust): POST logs to /v1/rust_control_plane/logs
ishaan-berri Jun 24, 2026
9cee1c1
docs(litellm-rust): update README to /v1/rust_control_plane/logs
ishaan-berri Jun 24, 2026
1e2a127
chore(ui): regenerate schema.d.ts for /v1/rust_control_plane/logs
ishaan-berri Jun 24, 2026
a76a47d
refactor(litellm-rust): collapse env_positive_{usize,u64} into one ge…
ishaan-berri Jun 24, 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
1 change: 1 addition & 0 deletions .github/workflows/test-unit-proxy-infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
tests/test_litellm/proxy/_experimental
tests/test_litellm/proxy/experimental
tests/test_litellm/proxy/common_utils
tests/test_litellm/proxy/logging_endpoints
tests/test_litellm/proxy/test_*.py
workers: 2
reruns: 2
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,6 @@ crash.*.log
# and should be committed.
.vscode
.pin_list.txt

# pytest coverage data
.coverage
2 changes: 2 additions & 0 deletions backend/routes/allowlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
"/active/callbacks",
"/callbacks",
"/team_callback",
# Rust data-plane gateway → proxy control-plane API (logging today, auth later)
"/v1/rust_control_plane/",
# Alerting / email / IP allowlist
"/alerting/",
"/email/",
Expand Down
14 changes: 14 additions & 0 deletions litellm-rust/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ such as `ai-gateway`, router hosts, or standalone servers:
- Avoid `expect`/`unwrap` in server startup and request paths unless the panic is
impossible by construction and documented.

## Constants

Magic numbers and fixed strings go in a crate-level `constants.rs`, never
hardcoded inline — the Rust mirror of Python's `litellm/constants.py`.

- Each crate that needs them has `src/constants.rs` (declared `mod constants;`);
import from it (`use crate::constants::...`). Don't scatter `const` values at
the top of feature modules.
- An env-overridable tunable still lives in `constants.rs` as its `DEFAULT_*`
value; the env read (with fallback to that default) happens at the host/config
resolution layer, not in `core`/`providers`.
- Exception: a value that is purely local to one function and has no meaning
elsewhere may stay inline, but prefer `constants.rs` when in doubt.

## Checks

Run these before pushing Rust changes. The same checks run in GitHub Actions
Expand Down
12 changes: 12 additions & 0 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 @@ -20,6 +20,7 @@ rand = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
subtle = "2"
thiserror = "2.0"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
Expand Down
12 changes: 12 additions & 0 deletions litellm-rust/crates/ai-gateway/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ai-gateway architecture

The Rust ai-gateway does LLM inference (realtime WebSocket). Spend tracking is an
API callback: it POSTs each finished session to the LiteLLM proxy, which records
spend and runs the usual callbacks.

```mermaid
flowchart LR
C[client] <--> G[Rust ai-gateway<br/>LLM inference]
G <--> O[OpenAI realtime]
G -. spend tracking callback .-> P[litellm proxy]
```
10 changes: 8 additions & 2 deletions litellm-rust/crates/ai-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,25 @@ required-features = ["server"]

[dependencies]
litellm-core.workspace = true
# reqwest (rustls + json) is used by io/ocr and ships realtime logs to the
# Python proxy callbacks API.
reqwest.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "time"] }
# `sync` powers the bounded mpsc channel the realtime logger drains.
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "time", "sync"] }
tokio-tungstenite.workspace = true
futures-util.workspace = true
serde_json.workspace = true
axum = { workspace = true, features = ["ws"], optional = true }
serde = { workspace = true, optional = true }
subtle = { workspace = true, optional = true }
# sha2 hashes the master key into user_api_key_hash (matches the proxy's
# SHA-256 hash_token) so the plaintext credential never enters a log payload.
sha2 = { workspace = true, optional = true }
pyo3 = { workspace = true, features = ["auto-initialize"], optional = true }

[features]
default = []
server = ["dep:axum", "dep:subtle", "dep:serde"]
server = ["dep:axum", "dep:subtle", "dep:serde", "dep:sha2"]
# Build the gateway's config from the proxy YAML via an embedded Python
# interpreter (links libpython; requires `litellm` importable at runtime).
python-config = ["dep:pyo3"]
Expand Down
14 changes: 14 additions & 0 deletions litellm-rust/crates/ai-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Dependency direction (acyclic): litellm-core ← litellm-ai-gateway ← litellm-
- **Client endpoint:** `wss://<host>/v1/realtime?model=<model>` (WebSocket)
- **Auth:** `Authorization: Bearer $LITELLM_MASTER_KEY` (fails closed if unset)
- **Health:** `GET /health/readiness`, `GET /health/liveness`, `GET /health/gil`
- **Request logs:** POSTed to a LiteLLM proxy at `/v1/rust_control_plane/logs` (see [Request logging](#request-logging))

> **Realtime serving is pure Rust.** Python is used at **load time only** — to
> read the config once at boot. The realtime hot path never touches Python.
Expand Down Expand Up @@ -65,6 +66,7 @@ overridden at deploy time (e.g. a Render secret file mounted at the same path).
| `OPENAI_API_KEY` | yes | — | Upstream OpenAI key. Referenced by config.yaml as `os.environ/OPENAI_API_KEY` for the gateway→OpenAI dial. |
| `HOST` | no | `127.0.0.1` | **Set to `0.0.0.0` in any container/deploy** or external traffic is refused. |
| `PORT` | no | `4001` | Listen port. Render and most PaaS inject this automatically. |
| `LITELLM_PROXY_BASE_URL` | no | `http://localhost:4000` | LiteLLM proxy that request logs are POSTed to. See [Request logging](#request-logging). |

> Secrets (`LITELLM_MASTER_KEY`, `OPENAI_API_KEY`) are never baked into the image
> or `render.yaml` — inject them at deploy time only.
Expand All @@ -83,6 +85,18 @@ This mode links no libpython and needs no config file, but it only supports one
hard-coded OpenAI deployment. **config.yaml is the recommended path** — use the
stand-in only for the leanest possible build.

## Request logging

The gateway runs no spend logic. When a session ends it builds one
`StandardLoggingPayload` and POSTs it to `{LITELLM_PROXY_BASE_URL}/v1/rust_control_plane/logs`
(admin-only, bearer = `LITELLM_MASTER_KEY`), and the proxy replays it through its
normal callbacks (spend logs, Langfuse, etc.). The POST is non-blocking: a bounded
channel drained by a background worker, dropping with a counter if the proxy is
down. It sends one payload per session. Both env vars are in the table above.

Worker tuning, rarely needed: `LITELLM_LOG_CHANNEL_CAPACITY` (4096),
`LITELLM_LOG_BATCH_SIZE` (256), `LITELLM_LOG_FLUSH_INTERVAL_MS` (500).

## Build & run with Docker

The image is built `--features python-config` and installs litellm **from this
Expand Down
39 changes: 39 additions & 0 deletions litellm-rust/crates/ai-gateway/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,29 @@ use axum::extract::FromRequestParts;
use axum::http::header::AUTHORIZATION;
use axum::http::request::Parts;
use axum::http::StatusCode;
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;

use crate::state::AppState;

/// SHA-256 hex digest of a token — the exact transform the Python proxy applies
/// (`litellm.proxy.utils.hash_token`).
///
/// STRICT REQUIREMENT: a raw key (`LITELLM_MASTER_KEY`, a virtual key, …) must
/// **never** leave this gateway in a log payload. Spend logs and every callback
/// integration receive `user_api_key_hash`, so that field must be this hash, not
/// the credential. Hashing here also means the value matches the key's hash in
/// `LiteLLM_SpendLogs.api_key`, so realtime spend joins with the rest of LiteLLM.
pub fn hash_token(token: &str) -> String {
let digest = Sha256::digest(token.as_bytes());
let mut hex = String::with_capacity(digest.len() * 2);
for byte in digest {
use std::fmt::Write;
let _ = write!(hex, "{byte:02x}");
}
hex
}

/// Extractor that requires the configured master key as a bearer token.
///
/// Rejections: `500` when no master key is configured (permanent
Expand Down Expand Up @@ -52,3 +71,23 @@ impl FromRequestParts<AppState> for RequireMasterKey {
}
}
}

#[cfg(test)]
mod tests {
use super::hash_token;

#[test]
fn hash_token_matches_python_sha256_hexdigest() {
// Must equal hashlib.sha256("sk-1234".encode()).hexdigest() — the value
// the proxy stores in LiteLLM_SpendLogs.api_key.
assert_eq!(
hash_token("sk-1234"),
"88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b"
);
// 64 lowercase hex chars, and never the raw input.
let h = hash_token("sk-secret");
assert_eq!(h.len(), 64);
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
assert_ne!(h, "sk-secret");
}
}
29 changes: 29 additions & 0 deletions litellm-rust/crates/ai-gateway/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Crate-level constants for the ai-gateway.
//!
//! Per `litellm-rust/CLAUDE.md`, magic numbers and fixed strings live here
//! (the Rust mirror of Python's `litellm/constants.py`), not inline in feature
//! modules. Env-overridable tunables keep their `DEFAULT_*` value here; the env
//! read + fallback happens at the host/config layer.

/// Default LiteLLM control-plane base URL for request-log egress when
/// `LITELLM_PROXY_BASE_URL` is unset.
pub(crate) const DEFAULT_PROXY_BASE_URL: &str = "http://localhost:4000";

/// The logs ingest path appended to the proxy base. Not a tunable; it is the
/// proxy's API contract (the rust-control-plane router on the Python proxy).
pub(crate) const RUST_CONTROL_PLANE_LOGS_PATH: &str = "/v1/rust_control_plane/logs";

/// Default bounded channel depth for the log-egress worker.
/// Override: `LITELLM_LOG_CHANNEL_CAPACITY`.
pub(crate) const DEFAULT_CHANNEL_CAPACITY: usize = 4096;

/// Default max records POSTed per request to the control plane.
/// Override: `LITELLM_LOG_BATCH_SIZE`.
pub(crate) const DEFAULT_MAX_BATCH_SIZE: usize = 256;

/// Default partial-batch flush cadence, in ms.
/// Override: `LITELLM_LOG_FLUSH_INTERVAL_MS`.
pub(crate) const DEFAULT_FLUSH_INTERVAL_MS: u64 = 500;

/// Provider attributed to realtime sessions in the logging payload.
pub(crate) const DEFAULT_PROVIDER: &str = "openai";
24 changes: 24 additions & 0 deletions litellm-rust/crates/ai-gateway/src/integrations/custom_logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! The `CustomLogger` trait — the Rust mirror of Python
//! `litellm/integrations/custom_logger.py::CustomLogger`.
//!
//! Synchronous (no `async_trait`): callbacks are O(1) enqueue-and-return so the
//! realtime splice never blocks on a logger. Default bodies are no-ops so a
//! logger can implement only the events it cares about.

use crate::integrations::types::{LogError, LoggingError, StandardLoggingPayload};

pub trait CustomLogger: Send + Sync {
/// Record a successful call. Default: no-op.
fn log_success_event(&self, _payload: &StandardLoggingPayload) -> Result<(), LogError> {
Ok(())
}

/// Record a failed call. Default: no-op.
fn log_failure_event(
&self,
_payload: &StandardLoggingPayload,
_error: &LoggingError,
) -> Result<(), LogError> {
Ok(())
}
}
Loading
Loading