mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
* feat(rust): add python-compat crate for Python data formats
Add litellm-python-compat, a PyO3-free crate that reproduces the Python
data formats LiteLLM persists, so Rust readers and writers can interoperate
with state written by the Python proxy:
- literal::literal_eval: a linear recursive-descent port of
ast.literal_eval (prefixes, escapes, implicit concatenation, numeric
underscores and radixes, single unary sign, real +/- complex with 3.14
mixed-mode rules, set(), Python-equality key dedup)
- repr::{repr, to_str}: byte-exact repr()/str(), with a printable table
generated from CPython's str.isprintable (Unicode 16.0.0)
- json::{dumps, from_json, to_json}: json.dumps defaults and the
json.loads mapping
- pickle::{loads, dumps}: plain-data pickles via serde-pickle's serde
interface, which keeps dict insertion order
- truthy::truthy: bool() for plain data
Tests replay fixtures generated by CPython 3.14 (values across every
format and pickle protocol 0-5, plus 154 literal_eval source texts).
Accepted divergences are pinned in a KNOWN table that fails once one
starts matching. A criterion bench covers each format and literal_eval
cost by nesting depth, guarding the linear parse: the py_literal grammar
doubled per nested bracket (105 ms at 16 nested dicts; 19 us at 128 now).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(rust): split python-compat modules and harden the pickle verifier
- Disable class resolution in scripts/verify_rust_pickles.py, and truncate
the export file once instead of removing and appending to it, so the
verifier cannot be pointed at a pre-created file whose rows execute code
through pickle.loads
- Move Error to error.rs and Value to value.rs, leaving lib.rs as the crate
overview, module list and MAX_DEPTH
- Move the generator and verifier to scripts/, beside the Unicode table
generator, leaving tests/ to the Rust tests
- Group the bench by measured surface, give every case a Throughput so
criterion reports bytes per second, and document baseline comparison
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
237 lines
7.5 KiB
Rust
237 lines
7.5 KiB
Rust
//! `repr(value)` and `str(value)` byte for byte.
|
|
//!
|
|
//! LiteLLM hashes `str(value)` into cache keys and writes `str(dict)` into Redis, so these
|
|
//! strings are persisted identifiers rather than display text: every quote choice, escape,
|
|
//! and float digit must match CPython.
|
|
|
|
use std::fmt::Write;
|
|
|
|
use crate::Value;
|
|
|
|
/// Generated by `scripts/generate_nonprintable.py`; see `generated/nonprintable.rs`.
|
|
mod nonprintable {
|
|
include!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/generated/nonprintable.rs"
|
|
));
|
|
}
|
|
|
|
/// Unicode version of the printable-character table, from the Python that generated it.
|
|
pub const UNICODE_VERSION: &str = nonprintable::UNICODE_VERSION;
|
|
|
|
/// `repr(value)`.
|
|
pub fn repr(value: &Value) -> String {
|
|
let mut out = String::new();
|
|
write_repr(&mut out, value);
|
|
out
|
|
}
|
|
|
|
/// `str(value)`: a string's own contents, and `repr` for every other value.
|
|
pub fn to_str(value: &Value) -> String {
|
|
match value {
|
|
Value::Str(text) => text.clone(),
|
|
value => repr(value),
|
|
}
|
|
}
|
|
|
|
/// `repr(float)`, the shortest round-trip form with CPython's exponent thresholds.
|
|
pub fn float_repr(value: f64) -> String {
|
|
format_float(value, true)
|
|
}
|
|
|
|
fn write_repr(out: &mut String, value: &Value) {
|
|
match value {
|
|
Value::None => out.push_str("None"),
|
|
Value::Bool(true) => out.push_str("True"),
|
|
Value::Bool(false) => out.push_str("False"),
|
|
Value::Int(value) => {
|
|
let _ = write!(out, "{value}");
|
|
}
|
|
Value::Float(value) => out.push_str(&float_repr(*value)),
|
|
Value::Complex { re, im } => write_complex(out, *re, *im),
|
|
Value::Str(text) => write_str(out, text),
|
|
Value::Bytes(bytes) => write_bytes(out, bytes),
|
|
Value::List(values) => write_sequence(out, '[', values, ']'),
|
|
Value::Tuple(values) if values.len() == 1 => {
|
|
out.push('(');
|
|
write_repr(out, &values[0]);
|
|
out.push_str(",)");
|
|
}
|
|
Value::Tuple(values) => write_sequence(out, '(', values, ')'),
|
|
Value::Set(values) if values.is_empty() => out.push_str("set()"),
|
|
Value::Set(values) => write_sequence(out, '{', values, '}'),
|
|
Value::Dict(entries) => {
|
|
out.push('{');
|
|
for (index, (key, value)) in entries.iter().enumerate() {
|
|
if index > 0 {
|
|
out.push_str(", ");
|
|
}
|
|
write_repr(out, key);
|
|
out.push_str(": ");
|
|
write_repr(out, value);
|
|
}
|
|
out.push('}');
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_sequence(out: &mut String, open: char, values: &[Value], close: char) {
|
|
out.push(open);
|
|
for (index, value) in values.iter().enumerate() {
|
|
if index > 0 {
|
|
out.push_str(", ");
|
|
}
|
|
write_repr(out, value);
|
|
}
|
|
out.push(close);
|
|
}
|
|
|
|
/// `complex.__repr__`: a `+0.0` real part prints only the imaginary part, without parens.
|
|
fn write_complex(out: &mut String, re: f64, im: f64) {
|
|
if re == 0.0 && re.is_sign_positive() {
|
|
out.push_str(&format_float(im, false));
|
|
out.push('j');
|
|
return;
|
|
}
|
|
out.push('(');
|
|
out.push_str(&format_float(re, false));
|
|
let im_text = format_float(im, false);
|
|
if !im_text.starts_with('-') {
|
|
out.push('+');
|
|
}
|
|
out.push_str(&im_text);
|
|
out.push_str("j)");
|
|
}
|
|
|
|
/// `PyOS_double_to_string(value, 'r', 0, flags)`: scientific notation below 1e-4 and from
|
|
/// 1e16 up, with a signed exponent of at least two digits. `add_dot_0` is
|
|
/// `Py_DTSF_ADD_DOT_0`, which `float` sets and `complex` does not.
|
|
fn format_float(value: f64, add_dot_0: bool) -> String {
|
|
if value.is_nan() {
|
|
return "nan".to_owned();
|
|
}
|
|
if value.is_infinite() {
|
|
return if value > 0.0 { "inf" } else { "-inf" }.to_owned();
|
|
}
|
|
// Rust's `{:e}` prints the shortest round-trip digits, like CPython's 'r' mode.
|
|
let scientific = format!("{value:e}");
|
|
let (mantissa, exponent) = scientific
|
|
.split_once('e')
|
|
.expect("`{:e}` always prints an exponent");
|
|
let exponent: i32 = exponent.parse().expect("`{:e}` exponent is an integer");
|
|
let (sign, mantissa) = match mantissa.strip_prefix('-') {
|
|
Some(mantissa) => ("-", mantissa),
|
|
None => ("", mantissa),
|
|
};
|
|
let digits: String = mantissa.chars().filter(|ch| *ch != '.').collect();
|
|
|
|
let mut out = String::from(sign);
|
|
if !(-4..16).contains(&exponent) {
|
|
out.push_str(&digits[..1]);
|
|
if digits.len() > 1 {
|
|
out.push('.');
|
|
out.push_str(&digits[1..]);
|
|
}
|
|
let _ = write!(
|
|
out,
|
|
"e{}{:02}",
|
|
if exponent < 0 { '-' } else { '+' },
|
|
exponent.unsigned_abs()
|
|
);
|
|
} else if exponent < 0 {
|
|
out.push_str("0.");
|
|
out.extend(std::iter::repeat_n('0', (-exponent - 1) as usize));
|
|
out.push_str(&digits);
|
|
} else {
|
|
let integer_digits = exponent as usize + 1;
|
|
if digits.len() > integer_digits {
|
|
out.push_str(&digits[..integer_digits]);
|
|
out.push('.');
|
|
out.push_str(&digits[integer_digits..]);
|
|
} else {
|
|
out.push_str(&digits);
|
|
out.extend(std::iter::repeat_n('0', integer_digits - digits.len()));
|
|
if add_dot_0 {
|
|
out.push_str(".0");
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// `unicode_repr`: single quotes unless the text has a `'` and no `"`. Printable non-ASCII
|
|
/// stays literal; everything `str.isprintable()` rejects is escaped.
|
|
fn write_str(out: &mut String, text: &str) {
|
|
let quote = if text.contains('\'') && !text.contains('"') {
|
|
'"'
|
|
} else {
|
|
'\''
|
|
};
|
|
out.push(quote);
|
|
for ch in text.chars() {
|
|
match ch {
|
|
'\\' => out.push_str("\\\\"),
|
|
'\t' => out.push_str("\\t"),
|
|
'\n' => out.push_str("\\n"),
|
|
'\r' => out.push_str("\\r"),
|
|
ch if ch == quote => {
|
|
out.push('\\');
|
|
out.push(ch);
|
|
}
|
|
ch if is_printable(ch) => out.push(ch),
|
|
ch => {
|
|
let code = ch as u32;
|
|
let _ = match code {
|
|
0..=0xff => write!(out, "\\x{code:02x}"),
|
|
0x100..=0xffff => write!(out, "\\u{code:04x}"),
|
|
_ => write!(out, "\\U{code:08x}"),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
out.push(quote);
|
|
}
|
|
|
|
/// `bytes.__repr__`: the same quote rule as `str`, with every byte outside printable ASCII
|
|
/// escaped as `\xhh`.
|
|
fn write_bytes(out: &mut String, bytes: &[u8]) {
|
|
let quote = if bytes.contains(&b'\'') && !bytes.contains(&b'"') {
|
|
b'"'
|
|
} else {
|
|
b'\''
|
|
};
|
|
out.push('b');
|
|
out.push(quote as char);
|
|
for &byte in bytes {
|
|
match byte {
|
|
b'\\' => out.push_str("\\\\"),
|
|
b'\t' => out.push_str("\\t"),
|
|
b'\n' => out.push_str("\\n"),
|
|
b'\r' => out.push_str("\\r"),
|
|
byte if byte == quote => {
|
|
out.push('\\');
|
|
out.push(byte as char);
|
|
}
|
|
0x20..=0x7e => out.push(byte as char),
|
|
byte => {
|
|
let _ = write!(out, "\\x{byte:02x}");
|
|
}
|
|
}
|
|
}
|
|
out.push(quote as char);
|
|
}
|
|
|
|
fn is_printable(ch: char) -> bool {
|
|
let code = ch as u32;
|
|
nonprintable::NONPRINTABLE
|
|
.binary_search_by(|&(low, high)| {
|
|
if high < code {
|
|
std::cmp::Ordering::Less
|
|
} else if low > code {
|
|
std::cmp::Ordering::Greater
|
|
} else {
|
|
std::cmp::Ordering::Equal
|
|
}
|
|
})
|
|
.is_err()
|
|
}
|