headroom/Cargo.toml
JD Davis 2c9eb7c5f1
feat(simulators): add provider simulator service (#2014)
## Description  
Adds a Rust-only `headroom-simulators` workspace crate: a deterministic
local upstream simulator service for Headroom proxy and pipeline
validation. It supplies configurable stubs plus bottled provider-shaped
responses for supported provider/path surfaces without calling real
LLMs.

## Type of Change  
- [x] Bug fix (non-breaking change that fixes an issue)  
- [x] New feature (non-breaking change that adds functionality)  
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update  
- [ ] Performance improvement  
- [ ] Code refactoring (no functional changes)

## Changes Made  
- Added `crates/headroom-simulators` Rust crate with library and
`headroom-simulators` binary.
- Added clean domain classification for supported surfaces: Anthropic
`/v1/messages`, OpenAI chat/responses/conversations, Bedrock
invoke/stream routes, Vertex raw/stream predict, health, and generic
fallback.
- Added JSON-configured stub matching by method, path, body substring,
and JSON pointer.
- Added bottled provider-shaped JSON, SSE, and Bedrock EventStream
responses for unconfigured requests.
- Added a container `Dockerfile` and README for local/GitHub Actions
usage.
- Added unit and HTTP integration tests for defaults, configured stubs,
SSE, Vertex, and Bedrock EventStream behavior.
- Added proxy-level simulator-backed E2E tests that run Headroom against
the simulator across Anthropic, OpenAI Chat, OpenAI Responses, OpenAI
Conversations, Bedrock invoke/converse/streaming, Vertex raw/stream
predict, and upstream health.
- Added simulator-backed provider error-path E2E coverage for OpenAI
429, Anthropic 529, Bedrock 502, and Vertex 503 responses flowing
through Headroom unchanged.
- Added Headroom-owned preflight error E2E coverage proving Bedrock
missing credentials and invalid Vertex envelopes stop inside the proxy
instead of silently falling through to the simulator/provider.
- Fixed direct Rust `headroom-core` binaries/tests on Windows so Magika
initializes ONNX Runtime via `ort::init_from` from an explicit pip
`onnxruntime` library path, with fail-fast fallback only when no safe
runtime is discoverable.
- Added a Rust CI `simulator-e2e` matrix for `ubuntu-latest`,
`macos-latest`, and `windows-latest` that runs `cargo test -p
headroom-proxy --test e2e_simulators`.
- Gated dynamic Magika `Path`/`PathBuf` imports to Windows and x86_64
macOS so Linux clippy does not see unused dynamic-ORT-only imports.

## Testing  
- [ ] Unit tests pass (`pytest`)  
- [ ] Linting passes (`ruff check .`)  
- [ ] Type checking passes (`mypy headroom`)  
- [x] New tests added for new functionality  
- [x] Manual testing performed

### Test Output  
cargo fmt --all -- --check  
# passed  

cargo clippy --workspace -- -D warnings  
# passed  

$env:ORT_DYLIB_PATH=$null  
cargo test -p headroom-core transforms::magika_detector::tests:: --lib  
# 17 passed, 0 failed; Magika initialized from discovered pip
onnxruntime DLL

$env:ORT_DYLIB_PATH=$null  
cargo test --workspace  
# passed  

gitleaks protect --staged --no-banner --redact  
# no leaks found  

gitleaks git --log-opts="headroomlabs/main..HEAD" --no-banner --redact  
# 5 commits scanned; no leaks found

## Real Behavior Proof  
- **Environment:** Windows PowerShell, Rust toolchain `1.95.0`, clean
worktree from `headroomlabs/main` at `9bacf481`.
- **Exact simulator command / steps:**  
  - `cargo run -p headroom-simulators -- --listen 127.0.0.1:8789`  
- Point Headroom proxy upstream at `http://127.0.0.1:8789` for local
deterministic provider responses.
- Use optional `--config path/to/simulator.json` to bind exact request
fixtures.
- **Observed simulator result:**  
  - OpenAI chat default returns `chat.completion` shape.  
  - OpenAI Responses stream returns named SSE events.  
  - Vertex raw predict returns Anthropic message shape.  
- Bedrock stream can return binary `application/vnd.amazon.eventstream`
bytes.
  - Configured stubs override bottled defaults.  
- **Observed Magika result:**  
- Direct Rust `headroom-core` tests pass with `ORT_DYLIB_PATH` unset.
- Magika discovers the installed pip `onnxruntime.dll`, loads it via
`ort::init_from`, and only falls back if no safe runtime is available.
- **Not tested:**  
- No live provider calls; simulator behavior is intentionally offline
and deterministic.

## Review Readiness  
- [x] I have performed a self-review  
- [x] This PR is ready for human review

## Checklist  
- [x] My code follows the project's style guidelines  
- [x] I have performed a self-review of my code  
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation  
- [x] My changes generate no new warnings  
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes  
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)  
N/A

## Additional Notes  
No CHANGELOG entry was added because this introduces a developer/CI
simulator crate plus a Windows direct-Rust Magika runtime fix, without
changing shipped Python package behavior. The simulator intentionally
does not include a lightweight fallback LLM in this slice; unbound
inputs receive deterministic bottled responses so tests stay
reproducible and offline.
2026-07-11 09:41:49 -07:00

130 lines
6.2 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[workspace]
resolver = "2"
members = [
"crates/headroom-core",
"crates/headroom-proxy",
"crates/headroom-simulators",
"crates/headroom-py",
"crates/headroom-parity",
]
# headroom-py is a Python extension module — it must be built via maturin, not
# plain cargo (the "extension-module" feature tells pyo3 not to link libpython,
# which is required for `import` to work). `cargo build --workspace` without
# explicit members skips it; `cargo test --workspace` still runs its tests
# because pyo3 can dynamically link here for the cdylib used by tests.
default-members = [
"crates/headroom-core",
"crates/headroom-proxy",
"crates/headroom-simulators",
"crates/headroom-parity",
]
[workspace.package]
edition = "2021"
rust-version = "1.80"
license = "Apache-2.0"
repository = "https://github.com/chopratejas/headroom"
authors = ["Headroom Maintainers"]
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
# `preserve_order` makes `serde_json::Value::Object` use IndexMap so JSON
# parse order is preserved through Value→string→Value round-trips. The
# smart_crusher port relies on this to match Python's `str(dict)` output,
# which preserves insertion order; otherwise BTreeMap's sorted-key default
# would diverge from Python on every multi-key object.
#
# `arbitrary_precision` keeps the literal numeric token from the source
# JSON intact: `Value::Number` becomes a wrapper around the original
# digit string, so `1.0` does NOT collapse to `1`, and `12345678901234567`
# does NOT lose precision through f64. Required by Realignment invariant
# I1 (byte-faithful passthrough on unmutated bytes; see REALIGNMENT/02-
# architecture.md §2.2) and PR-A4 (see REALIGNMENT/03-phase-A-lockdown.md).
#
# `raw_value` exposes `serde_json::value::RawValue`, the unparsed JSON
# fragment type. Phase B PR-B2 uses this to forward unmodified
# `messages[*]` entries as exact byte copies — the parser captures the
# original byte slice, so byte-for-byte round-trips work even with
# whitespace, key order, or escape preferences the producer chose.
# Enabled here in Phase A so PR-B2 can land as a pure consumer change.
serde_json = { version = "1", features = ["preserve_order", "arbitrary_precision", "raw_value"] }
bytes = "1"
thiserror = "2"
# `log` compat: when no tracing subscriber is active (the case inside the
# headroom-py cdylib), events are re-emitted as `log` records so pyo3-log
# can forward them to Python's logging. No effect on binaries that install
# a real subscriber.
tracing = { version = "0.1", features = ["log"] }
anyhow = "1"
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
axum = "0.7"
tower = "0.5"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
pyo3 = { version = "0.29", features = ["abi3-py310"] }
# Forwards Rust `log` records (incl. tracing events via the `log` compat
# feature above) into Python's `logging` inside the _core extension module.
pyo3-log = "0.13"
# Phase D PR-D1: AWS SigV4 signing for native Bedrock InvokeModel route.
# `aws-sigv4` provides the canonical-request + signing-key implementation;
# `aws-config` resolves credentials from the standard provider chain
# (env vars, profiles, IMDS, ECS task role, etc); `aws-credential-types`
# exposes `Credentials` so the signer accepts whatever the chain returned.
aws-sigv4 = { version = "1", default-features = false, features = ["sign-http", "http1"] }
aws-config = { version = "1", default-features = false, features = ["behavior-version-latest", "rustls", "rt-tokio", "sso"] }
aws-credential-types = { version = "1", default-features = false }
# `Identity` lives in aws-smithy-runtime-api; the SigV4 builder
# accepts `&Identity`. Pinning the version explicitly avoids a
# silent semver bump from the transitive dep tree.
aws-smithy-runtime-api = { version = "1", default-features = false, features = ["client"] }
# PR-D4: Vertex publisher path uses GCP Application Default Credentials
# (ADC) → bearer token for the `Authorization: Bearer <token>` header.
# `gcp_auth` resolves the chain (gcloud user creds, GCE/GKE metadata
# server, service-account JSON, workload-identity federation) without
# us baking provider-specific knowledge in. The token source is wrapped
# in a `TokenSource` trait so tests inject a static-token mock.
gcp_auth = "0.12"
# ── Release profile — wheel size optimization ───────────────────────
#
# PyPI imposes a 10 GB cumulative storage limit per project. We hit it
# at version 0.21.36 (191 versions × ~213 MB/release = 10.00 GB
# exactly). Recent wheels were ~16-18 MB each, of which ~6.4 MB was
# pure debug metadata (`.strtab` + `.symtab` ELF sections; uncovered
# by post-mortem inspection of an actual production wheel).
#
# This profile shrinks each Linux wheel from ~18 MB → ~10-11 MB by:
# * Stripping symbol/string tables (~6.4 MB direct savings)
# * Link-time optimization across crate boundaries (~5-10% .text
# savings via dead-code elim across the workspace)
# * Single codegen unit (better inlining + dead-code elim, at the
# cost of slightly slower release builds)
#
# We deliberately do NOT set ``panic = "abort"``. The proxy is a
# long-lived async process — a single misbehaving request triggering
# panic-abort would terminate the whole proxy and disconnect every
# concurrent client. Accept the smaller savings; keep unwind behaviour.
#
# Estimated impact: 213 MB/release → ~130 MB/release. Buys ~30+ more
# release slots within the 10 GB ceiling at the current release
# cadence. Per-PyPI-version savings AND faster downloads for end
# users. Tradeoff: release builds take ~30-50% longer due to
# `codegen-units = 1` + LTO; acceptable for the size win.
[profile.release]
strip = "symbols"
lto = "thin"
codegen-units = 1
# Fast-to-compile profile for CI test wheels. The shipped wheel uses
# `release` (lto + codegen-units=1) for runtime/size; CI only needs a working
# extension, so trade runtime perf for ~parallel, lto-free compilation. Used
# via `maturin build --profile ci`. Does NOT affect `--release` builds.
[profile.ci]
inherits = "release"
lto = false
codegen-units = 256
opt-level = 1
strip = "none"
debug = false
incremental = false