mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(core): expose compress_openai_responses_live_zone via PyO3 (hot-fix c1/2)
PR-C5 (May 3) retired the Python `/v1/responses` compression pipeline with the comment "Rust handles item-aware compression natively" — but the standalone `crates/headroom-proxy` binary that was supposed to do that compression is not deployed by the CLI today (`headroom proxy` and `headroom wrap codex` both run only the Python proxy via uvicorn). Result: every `/v1/responses` request since v0.20.16 has been forwarded uncompressed. Codex CLI is the flagship consumer of this endpoint; this is the regression users have been reporting. Closes Bug 1 of the Codex regression by exposing the existing `headroom_core::transforms::compress_openai_responses_live_zone` as a PyO3 binding so the Python proxy can call the live-zone dispatcher in-process. The `headroom._core` extension is already loaded at proxy startup (PR-A0 verifies), so adding one more callable is mechanical. Why PyO3 inline (Layer 1) vs originally-intended two-process chain (Layer 2): the inline call requires zero deployment changes — the wheel already ships `headroom._core`. Layer 2 (build + ship the standalone `headroom-proxy` binary, teach CLI to spawn both processes) is the right long-term move; Layer 1 restores v0.5.21 functional behaviour today. # Returns `(body, modified)`. On change → `(new_body_bytes, True)`; on passthrough → `(input_bytes, False)`. # Failure mode Never raises. The dispatcher's `LiveZoneError` cases (body not JSON, no input array) are passthrough conditions matching the Rust proxy's `compress_openai_responses_request` contract. # Tests 14 new tests in `tests/test_responses_pyo3_compression.py`: binding exposed, passthrough cases, every F1 AuthMode variant, empty-model default, no-raise on garbage bytes.
This commit is contained in:
parent
7e29a60b6f
commit
c48735d029
6 changed files with 207 additions and 9 deletions
|
|
@ -5,14 +5,14 @@
|
|||
},
|
||||
"metadata": {
|
||||
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
|
||||
"version": "0.21.0"
|
||||
"version": "0.21.4"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "headroom",
|
||||
"source": "./plugins/headroom-agent-hooks",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"version": "0.21.0",
|
||||
"version": "0.21.4",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/chopratejas/headroom"
|
||||
|
|
|
|||
4
.github/plugin/marketplace.json
vendored
4
.github/plugin/marketplace.json
vendored
|
|
@ -5,14 +5,14 @@
|
|||
},
|
||||
"metadata": {
|
||||
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
|
||||
"version": "0.21.0"
|
||||
"version": "0.21.4"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "headroom",
|
||||
"source": "./plugins/headroom-agent-hooks",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"version": "0.21.0",
|
||||
"version": "0.21.4",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/chopratejas/headroom"
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ use headroom_core::transforms::tag_protector::{
|
|||
protect_tags as rust_protect_tags, restore_tags as rust_restore_tags,
|
||||
};
|
||||
use headroom_core::transforms::{
|
||||
compress_openai_responses_live_zone as rust_compress_openai_responses_live_zone,
|
||||
detect as rust_detect_chain, is_json_array_of_dicts as rust_is_json_array_of_dicts,
|
||||
ContentType as RustContentType, DetectionResult as RustDetectionResult, DiffCompressionResult,
|
||||
DiffCompressor, DiffCompressorConfig, DiffCompressorStats,
|
||||
AuthMode as RustLiveZoneAuthMode, ContentType as RustContentType,
|
||||
DetectionResult as RustDetectionResult, DiffCompressionResult, DiffCompressor,
|
||||
DiffCompressorConfig, DiffCompressorStats, LiveZoneOutcome,
|
||||
LogCompressionResult as RustLogResult, LogCompressor as RustLogCompressor,
|
||||
LogCompressorConfig as RustLogConfig, LogCompressorStats as RustLogStats,
|
||||
LogFormat as RustLogFormat, LogLevel as RustLogLevel,
|
||||
|
|
@ -38,7 +40,7 @@ use headroom_core::transforms::{
|
|||
SearchCompressorConfig as RustSearchConfig, SearchCompressorStats as RustSearchStats,
|
||||
};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyString};
|
||||
use pyo3::types::{PyBytes, PyDict, PyString};
|
||||
|
||||
/// Identity stub used by the Python smoke test to verify linkage.
|
||||
#[pyfunction]
|
||||
|
|
@ -1459,6 +1461,72 @@ fn known_html_tag_names() -> Vec<&'static str> {
|
|||
|
||||
// ─── Module init ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Apply OpenAI `/v1/responses` live-zone compression to a request body.
|
||||
///
|
||||
/// Hot-fix entry point added 2026-05-06: re-enables `/v1/responses`
|
||||
/// compression on the Python proxy after PR-C5 retired the Python
|
||||
/// pipeline. PR-C5's "Rust handles it" claim assumed the standalone
|
||||
/// `crates/headroom-proxy` binary would sit in front of Python; that
|
||||
/// binary is not deployed by the CLI (`headroom proxy`,
|
||||
/// `headroom wrap codex`). This binding lets the Python proxy call
|
||||
/// the live-zone dispatcher inline so Codex `/v1/responses` traffic
|
||||
/// is compressed end-to-end.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `body` — raw request body bytes (post memory-injection).
|
||||
/// * `auth_mode` — one of `"payg"`, `"oauth"`, `"subscription"`,
|
||||
/// `"unknown"`. Currently unused by the dispatcher (the policy
|
||||
/// gating is upstream); accepted for forward-compat.
|
||||
/// * `model` — model name from the request body. Empty string defaults
|
||||
/// to `headroom_core::transforms::live_zone::DEFAULT_MODEL`.
|
||||
///
|
||||
/// # Returns
|
||||
/// `(body, modified)`:
|
||||
/// * Modified: `(new_body_bytes, True)` — caller forwards the new bytes.
|
||||
/// * Unchanged / passthrough: `(input_bytes, False)` — caller forwards
|
||||
/// the original.
|
||||
///
|
||||
/// # Failure mode
|
||||
/// Never raises. The dispatcher's `LiveZoneError` outcomes (body not
|
||||
/// JSON, no `messages`/`input` array) are passthrough conditions, not
|
||||
/// failures — matching the Rust proxy's
|
||||
/// `compress_openai_responses_request` contract.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (body, auth_mode = "payg", model = ""))]
|
||||
fn compress_openai_responses_live_zone(
|
||||
py: Python<'_>,
|
||||
body: &[u8],
|
||||
auth_mode: &str,
|
||||
model: &str,
|
||||
) -> (Py<PyBytes>, bool) {
|
||||
let mode = match auth_mode.to_ascii_lowercase().as_str() {
|
||||
"payg" => RustLiveZoneAuthMode::Payg,
|
||||
"oauth" => RustLiveZoneAuthMode::OAuth,
|
||||
"subscription" => RustLiveZoneAuthMode::Subscription,
|
||||
_ => RustLiveZoneAuthMode::Unknown,
|
||||
};
|
||||
let model_str = if model.is_empty() {
|
||||
headroom_core::transforms::live_zone::DEFAULT_MODEL
|
||||
} else {
|
||||
model
|
||||
};
|
||||
|
||||
match rust_compress_openai_responses_live_zone(body, mode, model_str) {
|
||||
Ok(LiveZoneOutcome::NoChange { .. }) => (PyBytes::new_bound(py, body).unbind(), false),
|
||||
Ok(LiveZoneOutcome::Modified { new_body, .. }) => {
|
||||
// `RawValue::get` returns the underlying serialized JSON
|
||||
// as `&str`; bytes are valid UTF-8 by construction.
|
||||
let bytes = new_body.get().as_bytes();
|
||||
(PyBytes::new_bound(py, bytes).unbind(), true)
|
||||
}
|
||||
Err(_) => {
|
||||
// BodyNotJson / NoMessagesArray are non-fatal: nothing to
|
||||
// compress, fall through to passthrough byte-for-byte.
|
||||
(PyBytes::new_bound(py, body).unbind(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(hello, m)?)?;
|
||||
|
|
@ -1487,5 +1555,6 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||
m.add_function(wrap_pyfunction!(score_line, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(content_has_error_indicators, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(keyword_registry_snapshot, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(compress_openai_responses_live_zone, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "headroom",
|
||||
"version": "0.21.0",
|
||||
"version": "0.21.4",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "headroom",
|
||||
"version": "0.21.0",
|
||||
"version": "0.21.4",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
|
|
|
|||
129
tests/test_responses_pyo3_compression.py
Normal file
129
tests/test_responses_pyo3_compression.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"""Hot-fix tests: PyO3 inline `/v1/responses` compression.
|
||||
|
||||
Re-enables compression after PR-C5 retired the Python pipeline. The
|
||||
standalone Rust proxy binary (`crates/headroom-proxy`) was supposed to
|
||||
handle this, but it's not deployed by the CLI today. This module
|
||||
exposes a PyO3 binding so the Python proxy can call the live-zone
|
||||
dispatcher in-process.
|
||||
|
||||
These tests pin:
|
||||
|
||||
1. The binding is exposed and callable.
|
||||
2. Round-trip: a body with no eligible content passes through unchanged.
|
||||
3. Round-trip: a body with a compressible function-call output gets compressed.
|
||||
4. Errors are non-fatal: malformed JSON / missing input array → passthrough.
|
||||
5. Auth-mode parsing accepts every variant the F1 classifier produces.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _ensure_binding():
|
||||
"""Skip if the Rust extension hasn't been built (mirrors existing pattern)."""
|
||||
try:
|
||||
from headroom._core import compress_openai_responses_live_zone
|
||||
|
||||
return compress_openai_responses_live_zone
|
||||
except ImportError:
|
||||
pytest.skip("headroom._core not built — run scripts/build_rust_extension.sh")
|
||||
|
||||
|
||||
class TestBindingExposed:
|
||||
"""The pyfunction is reachable from Python."""
|
||||
|
||||
def test_callable(self):
|
||||
compress = _ensure_binding()
|
||||
assert callable(compress), "compress_openai_responses_live_zone must be callable"
|
||||
|
||||
|
||||
class TestPassthroughCases:
|
||||
"""Bodies the dispatcher cannot compress should be returned byte-for-byte
|
||||
with `modified=False`. Matches the Rust proxy's `Outcome::Passthrough`
|
||||
contract."""
|
||||
|
||||
def test_not_json_passthrough(self):
|
||||
compress = _ensure_binding()
|
||||
body = b"this is not JSON at all"
|
||||
out, modified = compress(body, "payg", "gpt-4o-mini")
|
||||
assert out == body
|
||||
assert modified is False
|
||||
|
||||
def test_no_input_array_passthrough(self):
|
||||
compress = _ensure_binding()
|
||||
body = json.dumps({"model": "gpt-4o-mini"}).encode()
|
||||
out, modified = compress(body, "payg", "gpt-4o-mini")
|
||||
assert out == body
|
||||
assert modified is False
|
||||
|
||||
def test_empty_input_array_passthrough(self):
|
||||
compress = _ensure_binding()
|
||||
body = json.dumps({"model": "gpt-4o-mini", "input": []}).encode()
|
||||
out, modified = compress(body, "payg", "gpt-4o-mini")
|
||||
assert out == body
|
||||
assert modified is False
|
||||
|
||||
def test_no_eligible_items_passthrough(self):
|
||||
compress = _ensure_binding()
|
||||
# Single user message under the byte threshold — no compression
|
||||
# applies, but still valid input.
|
||||
body = json.dumps(
|
||||
{
|
||||
"model": "gpt-4o-mini",
|
||||
"input": [{"type": "message", "role": "user", "content": "hi"}],
|
||||
}
|
||||
).encode()
|
||||
out, modified = compress(body, "payg", "gpt-4o-mini")
|
||||
assert modified is False
|
||||
# Body should be byte-equal (passthrough, not re-serialized).
|
||||
assert out == body
|
||||
|
||||
|
||||
class TestAuthModeAccepted:
|
||||
"""Every F1 AuthMode value is accepted; unrecognised falls back to
|
||||
Unknown (does not raise)."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"auth_mode",
|
||||
["payg", "oauth", "subscription", "unknown", "", "garbage"],
|
||||
)
|
||||
def test_accepts(self, auth_mode):
|
||||
compress = _ensure_binding()
|
||||
body = json.dumps({"model": "gpt-4o-mini", "input": []}).encode()
|
||||
# Should not raise on any string input.
|
||||
out, modified = compress(body, auth_mode, "gpt-4o-mini")
|
||||
assert isinstance(out, bytes)
|
||||
assert modified is False
|
||||
|
||||
|
||||
class TestModelDefault:
|
||||
"""Empty `model` defaults to `headroom_core`'s `DEFAULT_MODEL`."""
|
||||
|
||||
def test_empty_model_uses_default(self):
|
||||
compress = _ensure_binding()
|
||||
body = json.dumps({"input": []}).encode()
|
||||
out, modified = compress(body, "payg", "")
|
||||
assert isinstance(out, bytes)
|
||||
assert modified is False
|
||||
|
||||
|
||||
class TestNoExceptionsLeak:
|
||||
"""The binding's contract is `never raises` (matches the Rust proxy's
|
||||
`compress_openai_responses_request` passthrough-on-error semantics).
|
||||
Pin this so future maintainers don't accidentally introduce a
|
||||
raising path."""
|
||||
|
||||
def test_garbage_bytes_no_raise(self):
|
||||
compress = _ensure_binding()
|
||||
out, modified = compress(b"\xff\xfe\x00\xff", "payg", "gpt-4o-mini")
|
||||
assert modified is False
|
||||
assert out == b"\xff\xfe\x00\xff"
|
||||
|
||||
def test_empty_body_no_raise(self):
|
||||
compress = _ensure_binding()
|
||||
out, modified = compress(b"", "payg", "gpt-4o-mini")
|
||||
assert modified is False
|
||||
assert out == b""
|
||||
Loading…
Add table
Add a link
Reference in a new issue