litellm/litellm-rust/crates/python-compat/benches/formats.rs
devin-ai-integration[bot] 05d7fb24bd
feat(rust): add python-compat crate for Python data formats (#42510)
* 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>
2026-09-22 12:16:56 -07:00

111 lines
4.7 KiB
Rust

//! Throughput of each format on a cached chat completion, and `literal_eval` cost by nesting.
//!
//! Run one group with `cargo bench -p litellm-python-compat -- cached_completion`, and compare
//! against a stored run with `--save-baseline <name>` / `--baseline <name>`.
//!
//! `literal_eval/nesting` guards against backtracking: the `py_literal` grammar this parser
//! replaced doubled its time per nested `[` or `{` (105 ms at depth 16), so cost must stay
//! linear in depth for every container shape.
use std::{hint::black_box, time::Duration};
use criterion::{
BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main,
measurement::WallTime,
};
use litellm_python_compat::{Value, json, literal::literal_eval, pickle, repr::repr};
/// `str(entry)` for the `{timestamp, response}` envelope Python's sync Redis path writes.
fn cached_completion() -> String {
let choices: Vec<String> = (0..4)
.map(|index| {
format!(
"{{'finish_reason': 'stop', 'index': {index}, 'message': {{'content': \
'Benchmarks compare the same workload under controlled conditions, so a \
change in time reflects the code rather than the environment. café 日本 \
{index}', 'role': 'assistant', 'tool_calls': None, 'function_call': None}}, \
'logprobs': None}}"
)
})
.collect();
format!(
"{{'timestamp': 1726000000.123, 'response': {{'id': 'chatcmpl-9x1', 'created': \
1726000000, 'model': 'gpt-4o-2024-08-06', 'object': 'chat.completion', \
'system_fingerprint': 'fp_1', 'choices': [{}], 'usage': {{'completion_tokens': 120, \
'prompt_tokens': 42, 'total_tokens': 162, 'completion_tokens_details': None}}}}}}",
choices.join(", ")
)
}
/// Every text format, measured against the source bytes it reads or writes.
fn text_formats(group: &mut BenchmarkGroup<'_, WallTime>, text: &str, value: &Value) {
group.throughput(Throughput::Bytes(text.len() as u64));
group.bench_function("literal_eval", |bencher| {
bencher.iter(|| literal_eval(black_box(text)))
});
group.bench_function("repr", |bencher| bencher.iter(|| repr(black_box(value))));
group.bench_function("json_dumps", |bencher| {
bencher.iter(|| json::dumps(black_box(value)))
});
group.bench_function("to_json", |bencher| {
bencher.iter(|| json::to_json(black_box(value)))
});
}
/// Pickle, measured against its own encoding rather than the source text.
fn binary_formats(group: &mut BenchmarkGroup<'_, WallTime>, value: &Value, pickled: &[u8]) {
group.throughput(Throughput::Bytes(pickled.len() as u64));
group.bench_function("pickle_dumps", |bencher| {
bencher.iter(|| pickle::dumps(black_box(value)))
});
group.bench_function("pickle_loads", |bencher| {
bencher.iter(|| pickle::loads(black_box(pickled)))
});
}
fn formats(c: &mut Criterion) {
let text = cached_completion();
let value = literal_eval(&text).expect("benchmark payload is a literal");
let pickled = pickle::dumps(&value).expect("benchmark payload pickles");
let dumped = json::dumps(&value).expect("benchmark payload is JSON serializable");
let mut group = c.benchmark_group("cached_completion");
text_formats(&mut group, &text, &value);
binary_formats(&mut group, &value, &pickled);
// `from_json` consumes its input, so each iteration gets a freshly parsed one.
group.throughput(Throughput::Bytes(dumped.len() as u64));
group.bench_function("from_json", |bencher| {
bencher.iter_batched(
|| serde_json::from_str::<serde_json::Value>(&dumped).expect("dumps output parses"),
json::from_json,
BatchSize::SmallInput,
)
});
group.finish();
}
/// One nesting level of each container shape, as `(name, open, close)`.
const SHAPES: [(&str, &str, &str); 3] = [
("list", "[", "]"),
("dict", "{'a': ", "}"),
("tuple", "(", ",)"),
];
fn literal_nesting(c: &mut Criterion) {
let mut group = c.benchmark_group("literal_eval/nesting");
group.sample_size(10);
group.measurement_time(Duration::from_secs(3));
for depth in [4, 16, 64, 128] {
for (shape, open, close) in SHAPES {
let text = format!("{}1{}", open.repeat(depth), close.repeat(depth));
group.throughput(Throughput::Bytes(text.len() as u64));
group.bench_with_input(BenchmarkId::new(shape, depth), &text, |bencher, text| {
bencher.iter(|| literal_eval(black_box(text)))
});
}
}
group.finish();
}
criterion_group!(benches, formats, literal_nesting);
criterion_main!(benches);