Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
489 changes: 434 additions & 55 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ reqwest-middleware = { version = "0.4" }
reqwest-retry = { version = "0.7" }

# Python
pyo3 = { version = "0.20", features = ["extension-module"] }
pyo3 = { version = "0.24.1", features = ["extension-module"] }

# Dev
tokio-test = "0.4"
Expand All @@ -76,13 +76,13 @@ http = "1.1"
hyper = { version = "1.3" }

# Utilities
rand = { version = "0.8", features = ["std"] }
rand = { version = "0.9.3", features = ["std"] }
once_cell = "1.19"
parking_lot = "0.12"
arc-swap = "1.7"
pin-project = "1.1"
dashmap = "5"
lru = "0.12"
lru = "0.16.3"
smallvec = "1.13"
chrono = { version = "0.4", features = ["serde"] }
sha2 = "0.10"
Expand All @@ -96,7 +96,7 @@ aws-smithy-eventstream = { version = "0.60.20" }
aws-smithy-types = { version = "1.4.7" }

# Auth
jsonwebtoken = { version = "9.3" }
jsonwebtoken = { version = "10.3", features = ["rust_crypto"] }

# Tracing (optional)
tracing = { version = "0.1" }
Expand Down
6 changes: 3 additions & 3 deletions bindings/python/uv.lock

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

2 changes: 1 addition & 1 deletion crates/braintrust-llm-router/src/providers/vertex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ impl VertexProvider {
::serde_json::Value::Object(spec.extra.clone()),
) {
if !extra.locations.is_empty() {
let idx = rand::thread_rng().gen_range(0..extra.locations.len());
let idx = rand::rng().random_range(0..extra.locations.len());
return extra.locations[idx].clone();
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/braintrust-llm-router/src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl RetryPolicy {
RetryStrategy {
policy: self.clone(),
attempts: 0,
rng: self.jitter.then(StdRng::from_entropy),
rng: self.jitter.then(StdRng::from_os_rng),
}
}
}
Expand Down Expand Up @@ -71,7 +71,7 @@ impl RetryStrategy {
}

if let Some(rng) = &mut self.rng {
let jitter: f64 = rng.gen_range(0.5..1.5);
let jitter: f64 = rng.random_range(0.5..1.5);
delay = delay.mul_f64(jitter).min(self.policy.max_delay);
}

Expand Down
116 changes: 82 additions & 34 deletions crates/lingua/src/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::serde_json;
use crate::universal::{convert::TryFromLLM, Message};

/// Convert Python object to Rust type via JSON
fn py_to_rust<T>(py: Python, value: &PyAny) -> PyResult<T>
fn py_to_rust<'py, T>(py: Python<'py>, value: &Bound<'py, PyAny>) -> PyResult<T>
where
T: for<'de> Deserialize<'de>,
{
Expand All @@ -27,7 +27,7 @@ where
}

/// Convert Rust type to Python object via JSON
fn rust_to_py<T>(py: Python, value: &T) -> PyResult<PyObject>
fn rust_to_py<'py, T>(py: Python<'py>, value: &T) -> PyResult<Bound<'py, PyAny>>
where
T: Serialize,
{
Expand All @@ -39,12 +39,14 @@ where
// Convert JSON string to Python object
pyo3::types::PyModule::import(py, "json")?
.getattr("loads")?
.call1((json_str,))?
.extract()
.call1((json_str,))
}

/// Generic conversion from provider to Lingua
fn convert_to_lingua<T, U>(py: Python, value: &PyAny) -> PyResult<PyObject>
fn convert_to_lingua<'py, T, U>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>>
where
T: for<'de> Deserialize<'de>,
U: TryFromLLM<T> + Serialize,
Expand All @@ -58,7 +60,10 @@ where
}

/// Generic conversion from Lingua to provider
fn convert_from_lingua<T, U>(py: Python, value: &PyAny) -> PyResult<PyObject>
fn convert_from_lingua<'py, T, U>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>>
where
T: for<'de> Deserialize<'de>,
U: TryFromLLM<T> + Serialize,
Expand All @@ -77,49 +82,73 @@ where

/// Convert array of Chat Completions messages to Lingua Messages
#[pyfunction]
fn chat_completions_messages_to_lingua(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn chat_completions_messages_to_lingua<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
convert_to_lingua::<Vec<ChatCompletionRequestMessageExt>, Vec<Message>>(py, value)
}

/// Convert array of Lingua Messages to Chat Completions messages
#[pyfunction]
fn lingua_to_chat_completions_messages(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn lingua_to_chat_completions_messages<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
convert_from_lingua::<Vec<Message>, Vec<ChatCompletionRequestMessageExt>>(py, value)
}

/// Convert array of Responses API messages to Lingua Messages
#[pyfunction]
fn responses_messages_to_lingua(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn responses_messages_to_lingua<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
convert_to_lingua::<Vec<openai::InputItem>, Vec<Message>>(py, value)
}

/// Convert array of Lingua Messages to Responses API messages
#[pyfunction]
fn lingua_to_responses_messages(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn lingua_to_responses_messages<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
convert_from_lingua::<Vec<Message>, Vec<openai::InputItem>>(py, value)
}

/// Convert array of Anthropic messages to Lingua Messages
#[pyfunction]
fn anthropic_messages_to_lingua(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn anthropic_messages_to_lingua<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
convert_to_lingua::<Vec<anthropic::InputMessage>, Vec<Message>>(py, value)
}

/// Convert array of Lingua Messages to Anthropic messages
#[pyfunction]
fn lingua_to_anthropic_messages(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn lingua_to_anthropic_messages<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
convert_from_lingua::<Vec<Message>, Vec<anthropic::InputMessage>>(py, value)
}

/// Convert array of Google Content items to Lingua Messages
#[pyfunction]
fn google_contents_to_lingua(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn google_contents_to_lingua<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
convert_to_lingua::<Vec<google::Content>, Vec<Message>>(py, value)
}

/// Convert array of Lingua Messages to Google Content items
#[pyfunction]
fn lingua_to_google_contents(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn lingua_to_google_contents<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
convert_from_lingua::<Vec<Message>, Vec<google::Content>>(py, value)
}

Expand All @@ -129,7 +158,10 @@ fn lingua_to_google_contents(py: Python, value: &PyAny) -> PyResult<PyObject> {

/// Deduplicate messages based on role and content
#[pyfunction]
fn deduplicate_messages(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn deduplicate_messages<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
use crate::processing::dedup::deduplicate_messages as dedup;
use crate::universal::Message;

Expand All @@ -145,7 +177,10 @@ fn deduplicate_messages(py: Python, value: &PyAny) -> PyResult<PyObject> {

/// Import messages from spans
#[pyfunction]
fn import_messages_from_spans(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn import_messages_from_spans<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
use crate::processing::import::{import_messages_from_spans as import, Span};

// Convert Python value to Vec<Span>
Expand All @@ -160,7 +195,10 @@ fn import_messages_from_spans(py: Python, value: &PyAny) -> PyResult<PyObject> {

/// Import and deduplicate messages from spans in a single operation
#[pyfunction]
fn import_and_deduplicate_messages(py: Python, value: &PyAny) -> PyResult<PyObject> {
fn import_and_deduplicate_messages<'py>(
py: Python<'py>,
value: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
use crate::processing::import::{import_and_deduplicate_messages as import_dedup, Span};

// Convert Python value to Vec<Span>
Expand All @@ -180,7 +218,10 @@ fn import_and_deduplicate_messages(py: Python, value: &PyAny) -> PyResult<PyObje
/// Validate a JSON string as a Chat Completions request
#[pyfunction]
#[cfg(feature = "openai")]
fn validate_chat_completions_request(py: Python, json: &str) -> PyResult<PyObject> {
fn validate_chat_completions_request<'py>(
py: Python<'py>,
json: &str,
) -> PyResult<Bound<'py, PyAny>> {
use crate::validation::openai::validate_chat_completions_request as validate;
let result = validate(json)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
Expand All @@ -190,7 +231,10 @@ fn validate_chat_completions_request(py: Python, json: &str) -> PyResult<PyObjec
/// Validate a JSON string as a Chat Completions response
#[pyfunction]
#[cfg(feature = "openai")]
fn validate_chat_completions_response(py: Python, json: &str) -> PyResult<PyObject> {
fn validate_chat_completions_response<'py>(
py: Python<'py>,
json: &str,
) -> PyResult<Bound<'py, PyAny>> {
use crate::validation::openai::validate_chat_completions_response as validate;
let result = validate(json)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
Expand All @@ -200,7 +244,7 @@ fn validate_chat_completions_response(py: Python, json: &str) -> PyResult<PyObje
/// Validate a JSON string as a Responses API request
#[pyfunction]
#[cfg(feature = "openai")]
fn validate_responses_request(py: Python, json: &str) -> PyResult<PyObject> {
fn validate_responses_request<'py>(py: Python<'py>, json: &str) -> PyResult<Bound<'py, PyAny>> {
use crate::validation::openai::validate_responses_request as validate;
let result = validate(json)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
Expand All @@ -210,7 +254,7 @@ fn validate_responses_request(py: Python, json: &str) -> PyResult<PyObject> {
/// Validate a JSON string as a Responses API response
#[pyfunction]
#[cfg(feature = "openai")]
fn validate_responses_response(py: Python, json: &str) -> PyResult<PyObject> {
fn validate_responses_response<'py>(py: Python<'py>, json: &str) -> PyResult<Bound<'py, PyAny>> {
use crate::validation::openai::validate_responses_response as validate;
let result = validate(json)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
Expand All @@ -221,22 +265,22 @@ fn validate_responses_response(py: Python, json: &str) -> PyResult<PyObject> {
/// @deprecated Use validate_chat_completions_request instead
#[pyfunction]
#[cfg(feature = "openai")]
fn validate_openai_request(py: Python, json: &str) -> PyResult<PyObject> {
fn validate_openai_request<'py>(py: Python<'py>, json: &str) -> PyResult<Bound<'py, PyAny>> {
validate_chat_completions_request(py, json)
}

/// Validate a JSON string as an OpenAI response
/// @deprecated Use validate_chat_completions_response instead
#[pyfunction]
#[cfg(feature = "openai")]
fn validate_openai_response(py: Python, json: &str) -> PyResult<PyObject> {
fn validate_openai_response<'py>(py: Python<'py>, json: &str) -> PyResult<Bound<'py, PyAny>> {
validate_chat_completions_response(py, json)
}

/// Validate a JSON string as an Anthropic request
#[pyfunction]
#[cfg(feature = "anthropic")]
fn validate_anthropic_request(py: Python, json: &str) -> PyResult<PyObject> {
fn validate_anthropic_request<'py>(py: Python<'py>, json: &str) -> PyResult<Bound<'py, PyAny>> {
use crate::validation::anthropic::validate_anthropic_request as validate;
let result = validate(json)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
Expand All @@ -246,7 +290,7 @@ fn validate_anthropic_request(py: Python, json: &str) -> PyResult<PyObject> {
/// Validate a JSON string as an Anthropic response
#[pyfunction]
#[cfg(feature = "anthropic")]
fn validate_anthropic_response(py: Python, json: &str) -> PyResult<PyObject> {
fn validate_anthropic_response<'py>(py: Python<'py>, json: &str) -> PyResult<Bound<'py, PyAny>> {
use crate::validation::anthropic::validate_anthropic_response as validate;
let result = validate(json)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
Expand All @@ -267,12 +311,12 @@ fn validate_anthropic_response(py: Python, json: &str) -> PyResult<PyObject> {
/// - `{ "transformed": True, "data": ..., "source_format": "..." }` if transformed
#[pyfunction]
#[pyo3(signature = (json, target_format, model=None))]
fn transform_request(
py: Python,
fn transform_request<'py>(
py: Python<'py>,
json: &str,
target_format: &str,
model: Option<String>,
) -> PyResult<PyObject> {
) -> PyResult<Bound<'py, PyAny>> {
use crate::capabilities::ProviderFormat;
use crate::processing::transform::{transform_request as transform, TransformResult};
use bytes::Bytes;
Expand All @@ -299,7 +343,7 @@ fn transform_request(
let dict = pyo3::types::PyDict::new(py);
dict.set_item("pass_through", true)?;
dict.set_item("data", rust_to_py(py, &data)?)?;
Ok(dict.into())
Ok(dict.into_any())
}
TransformResult::Transformed {
bytes,
Expand All @@ -316,7 +360,7 @@ fn transform_request(
dict.set_item("transformed", true)?;
dict.set_item("data", rust_to_py(py, &data)?)?;
dict.set_item("source_format", source_format.to_string())?;
Ok(dict.into())
Ok(dict.into_any())
}
}
}
Expand All @@ -330,7 +374,11 @@ fn transform_request(
/// - `{ "pass_through": True, "data": ... }` if payload is already valid for target
/// - `{ "transformed": True, "data": ..., "source_format": "..." }` if transformed
#[pyfunction]
fn transform_response(py: Python, json: &str, target_format: &str) -> PyResult<PyObject> {
fn transform_response<'py>(
py: Python<'py>,
json: &str,
target_format: &str,
) -> PyResult<Bound<'py, PyAny>> {
use crate::capabilities::ProviderFormat;
use crate::processing::transform::{transform_response as transform, TransformResult};
use bytes::Bytes;
Expand All @@ -357,7 +405,7 @@ fn transform_response(py: Python, json: &str, target_format: &str) -> PyResult<P
let dict = pyo3::types::PyDict::new(py);
dict.set_item("pass_through", true)?;
dict.set_item("data", rust_to_py(py, &data)?)?;
Ok(dict.into())
Ok(dict.into_any())
}
TransformResult::Transformed {
bytes,
Expand All @@ -374,7 +422,7 @@ fn transform_response(py: Python, json: &str, target_format: &str) -> PyResult<P
dict.set_item("transformed", true)?;
dict.set_item("data", rust_to_py(py, &data)?)?;
dict.set_item("source_format", source_format.to_string())?;
Ok(dict.into())
Ok(dict.into_any())
}
}
}
Expand All @@ -395,7 +443,7 @@ fn extract_model(json: &str) -> Option<String> {

/// Python module for Lingua
#[pymodule]
fn _lingua(_py: Python, m: &PyModule) -> PyResult<()> {
fn _lingua(m: &Bound<'_, PyModule>) -> PyResult<()> {
// Conversion functions
m.add_function(wrap_pyfunction!(chat_completions_messages_to_lingua, m)?)?;
m.add_function(wrap_pyfunction!(lingua_to_chat_completions_messages, m)?)?;
Expand Down
Loading
Loading