refactor(neuron): introduce InferenceEvent + wire projection layer
Some checks failed
build-prerelease / Resolve version stamps (push) Successful in 31s
CI / Format (push) Successful in 38s
CI / Clippy (push) Successful in 3m28s
build-prerelease / Build neuron-blackwell (push) Failing after 6m4s
build-prerelease / Build neuron-ampere (push) Failing after 7m20s
CI / Test (push) Successful in 7m29s
CI / Build cortex SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
build-prerelease / Build neuron-ada (push) Failing after 4m57s
build-prerelease / Package helexa-neuron-ada RPM (push) Has been skipped
build-prerelease / Package helexa-neuron-ampere RPM (push) Has been skipped
build-prerelease / Package helexa-neuron-blackwell RPM (push) Has been skipped
build-prerelease / Build cortex binary (push) Successful in 4m19s
build-prerelease / Package cortex RPM (push) Successful in 1m24s
build-prerelease / Publish to rpm.lair.cafe (unstable) (push) Has been skipped

Step 1 of the OpenAI Responses API rollout. Pure refactor — no new
endpoints, no behaviour change on the wire. Lays the seam for
emitting Responses-shaped streaming events from the same harness
output as chat completions in Step 2.

- New `neuron::wire` module tree:
  - `wire::event::InferenceEvent` — format-agnostic enum
    (Start, TextDelta, ReasoningDelta, Finish) the candle harness
    now emits as its native streaming currency.
  - `wire::event::FinishReason` — typed reason that maps cleanly
    onto OpenAI `finish_reason`, OpenAI Responses `status`, and
    Anthropic `stop_reason` strings.
  - `wire::openai_chat::project_chat_stream` — async task that
    consumes an InferenceEvent receiver and produces a
    ChatCompletionChunk receiver, stamping per-request metadata
    (id, created, model_id) onto every chunk. Output matches the
    pre-refactor wire shape bit-for-bit.

- candle.rs refactored to emit InferenceEvent on its internal
  channel through all three streaming paths (CPU
  run_inference_streaming, CUDA single-GPU stream_inference_via_worker,
  CUDA TP chat_completion_tp_stream). The streaming functions lost
  their id/created/model_id parameters since wire-format metadata
  now lives in the projector.

- emit_delta + emit_delta_blocking simplified to single-purpose
  TextDelta emitters with no wire-format coupling.

- chat_completion_stream wraps the InferenceEvent receiver in
  wire_chat::project_chat_stream before returning so the
  /v1/chat/completions HTTP handler keeps consuming
  ChatCompletionChunks unchanged. External signature preserved.

Also fixes a pre-existing helexa-acp test race (three modules each
declared their own static LOCK for HOME mutation, so cross-module
parallelism flaked tests that read HOME at runtime). Consolidated
onto a single crate-wide path_util::ENV_LOCK.

122 helexa-acp tests + 44 neuron tests pass (5 new wire projection
tests). fmt + clippy --workspace -- -D warnings clean. Ran helexa-acp
suite 3x to confirm the env race is closed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 11:30:17 +03:00
parent df0abfe4d4
commit 302ccfb982
7 changed files with 491 additions and 194 deletions

View File

@@ -26,6 +26,16 @@
use std::path::{Path, PathBuf};
/// Process-global lock for tests that mutate `HOME`. Anyone in the
/// crate touching `HOME` must hold this for the duration of the
/// read-modify-restore window — otherwise concurrent `cargo test`
/// workers race and flake.
///
/// Only built into the test binaries. Production code never mutates
/// env vars.
#[cfg(test)]
pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// Expand `~`, `~/`, `$HOME`, and `$HOME/` prefixes against the
/// current user's home directory. All other inputs pass through
/// unchanged.
@@ -56,13 +66,11 @@ mod tests {
use super::*;
/// Set HOME for the duration of the test. Tests using this run
/// serially under one mutex because env mutation isn't
/// thread-safe — `cargo test` parallel workers would race
/// without it.
/// serially under the crate-wide [`ENV_LOCK`] because env
/// mutation isn't thread-safe — `cargo test` parallel workers
/// would race without it.
fn with_home<F: FnOnce()>(home: &str, body: F) {
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
let _g = LOCK.lock().unwrap();
let _g = ENV_LOCK.lock().unwrap();
let prior = std::env::var("HOME").ok();
// SAFETY: tests touch process-global env. The mutex
// serialises access; sub-threads in other test modules
@@ -148,10 +156,10 @@ mod tests {
#[test]
fn no_home_env_passes_through() {
// Lock + clear HOME for this one.
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
let _g = LOCK.lock().unwrap();
// Share the same crate-wide lock as `with_home` — otherwise
// a parallel test setting HOME races this clear-and-assert
// window.
let _g = ENV_LOCK.lock().unwrap();
let prior = std::env::var("HOME").ok();
// SAFETY: serialised by LOCK above.
unsafe {

View File

@@ -1251,7 +1251,20 @@ mod tests {
}
#[tokio::test]
// Holds the env lock across an await — the await is the
// tool dispatch, which itself re-reads HOME via plan_dir_for.
// Releasing the lock would let another test mutate HOME
// between this test's setup and the gate's lookup.
#[allow(clippy::await_holding_lock)]
async fn plan_mode_allows_write_inside_plan_dir_without_permission() {
// Plan-mode gate calls store::plan_dir_for at runtime
// (which reads HOME). If a parallel test mutates HOME
// mid-flight, the gate's plan_dir would differ from the
// one we computed up here and the path check would fail.
// Share the crate-wide env lock so we and any HOME-mutator
// serialise.
let _g = crate::path_util::ENV_LOCK.lock().unwrap();
// Skip if we can't resolve a plan dir in this environment
// (would happen with no HOME / XDG_DATA_HOME — neither
// realistic in CI nor for an interactive run).
@@ -1321,11 +1334,9 @@ mod tests {
// correct *default*; this is the documented exception.
#[allow(clippy::await_holding_lock)]
async fn read_file_expands_tilde_before_dispatch() {
// HOME mutation is process-global; serialise tests that
// touch it under a single std::sync::Mutex.
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
let _g = LOCK.lock().unwrap();
// HOME mutation is process-global; share the crate-wide
// ENV_LOCK with path_util's tests so workers don't race.
let _g = crate::path_util::ENV_LOCK.lock().unwrap();
let prior = std::env::var("HOME").ok();
unsafe {
std::env::set_var("HOME", "/home/me");