# Headroom Pipeline Lifecycle Stages and How to Hook Into Them

> Explore Headroom pipeline lifecycle stages from setup to response_received. Learn how to intercept each stage using PipelineExtension or HeadroomClient hooks for greater control.

- Repository: [Tejas Chopra/headroom](https://github.com/chopratejas/headroom)
- Tags: internals
- Published: 2026-06-09

---

**Headroom exposes eleven canonical pipeline lifecycle stages—from `setup` to `response_received`—and lets you intercept each one by implementing the `PipelineExtension` protocol or passing hooks directly to `HeadroomClient`.**

Headroom is an open-source LLM proxy that processes every request through a finite-state machine defined in [`headroom/pipeline.py`](https://github.com/chopratejas/headroom/blob/main/headroom/pipeline.py). Understanding the **Headroom pipeline lifecycle stages** is essential if you want to log traffic, mutate prompts, or skip transforms. The entire flow is expressed as the `PipelineStage` enum and enforced by the `CANONICAL_PIPELINE_STAGES` sequence.

## The 11 Canonical Headroom Pipeline Lifecycle Stages

The canonical pipeline is enumerated in [`headroom/pipeline.py`](https://github.com/chopratejas/headroom/blob/main/headroom/pipeline.py) as `CANONICAL_PIPELINE_STAGES` and covers the full lifespan of a proxy request. According to the `chopratejas/headroom` source code, the stages are:

- **`setup`** – Early initialization, such as loading configuration.
- **`pre_start`** – One-time warm-up work before the first request is accepted.
- **`post_start`** – The proxy is actively listening for inbound connections.
- **`input_received`** – Raw request payloads, messages, tools, and headers arrive.
- **`input_cached`** – The request is stored in the request cache when caching is enabled.
- **`input_routed`** – The request is routed to the designated LLM provider.
- **`input_compressed`** – Compression transforms are applied to the payload.
- **`input_remembered`** – The request (or its compressed form) is written to the memory store.
- **`pre_send`** – Final checkpoint just before dispatching to the LLM provider.
- **`post_send`** – The provider has returned a response, but post-processing has not yet run.
- **`response_received`** – The final response (messages, tools, metadata) is ready for the client.

## How to Hook Into Headroom Pipeline Stages

Headroom’s extensibility model is built on **pipeline extensions**. An extension is any object implementing the `PipelineExtension` protocol defined in [`headroom/pipeline.py`](https://github.com/chopratejas/headroom/blob/main/headroom/pipeline.py). Each extension must expose a single method, `on_pipeline_event`, which receives a `PipelineEvent` and returns either a modified event or `None`.

### Writing a Pipeline Extension

Below is a minimal extension that logs each stage and mutates metadata. Because the `PipelineEvent` dataclass is mutable, you can modify fields such as `messages` or `metadata` in place.

```python

# my_extension.py

from headroom.pipeline import PipelineEvent, PipelineStage

class LogStageExtension:
    """Example extension that logs each stage and can mutate the event."""
    def on_pipeline_event(self, event: PipelineEvent) -> PipelineEvent | None:
        print(f"[hook] {event.stage.value} – operation={event.operation}")
        # Example mutation: add a flag to metadata

        event.metadata["hooked"] = True
        return event          # returning a new PipelineEvent also works

```

### Registering Extensions via Python Entry Points

The recommended discovery mechanism uses Python entry points. The `PipelineExtensionManager` loads every distribution registered under the group `headroom.pipeline_extension`, which is stored in the `ENTRY_POINT_GROUP` constant in [`headroom/pipeline.py`](https://github.com/chopratejas/headroom/blob/main/headroom/pipeline.py).

Add the following to your package’s [`setup.cfg`](https://github.com/chopratejas/headroom/blob/main/setup.cfg):

```ini
[options.entry_points]
headroom.pipeline_extension =
    log_stage = my_extension:LogStageExtension

```

When the proxy starts, the manager automatically discovers and instantiates your extension.

### Manually Attaching Extensions to the Client

If you prefer explicit registration over auto-discovery, pass an extension instance directly to `HeadroomClient` via the `pipeline_extensions` argument. As implemented in [`headroom/client.py`](https://github.com/chopratejas/headroom/blob/main/headroom/client.py), the constructor forwards your hooks to a `PipelineExtensionManager` created under the hood.

```python
from headroom.client import HeadroomClient
from my_extension import LogStageExtension

client = HeadroomClient(
    # … other client arguments …

    pipeline_extensions=LogStageExtension(),   # or a list of extensions

)

```

## The PipelineEvent Object and Mutable Payloads

Every hook receives a `PipelineEvent` dataclass instance defined in [`headroom/pipeline.py`](https://github.com/chopratejas/headroom/blob/main/headroom/pipeline.py). This object carries the full context of the current request and is designed to be mutated in flight. Its key fields include:

- **`stage`** – The current `PipelineStage` enum member.
- **`operation`** – A string such as `"compress"` or `"chat"`.
- **`request_id`**, **`provider`**, **`model`** – Traceability identifiers.
- **`messages`**, **`tools`**, **`headers`** – Mutable request payloads.
- **`response`** – Populated only during later stages (`post_send`, `response_received`).
- **`metadata`** – A dictionary for arbitrary flags and data shared across extensions.

## Practical Pipeline Hook Examples

### Simple Logger

This extension prints every stage to stdout and returns the event unchanged.

```python

# logger_extension.py

from headroom.pipeline import PipelineEvent, PipelineStage

class SimpleLogger:
    def on_pipeline_event(self, event: PipelineEvent):
        print(f"Stage: {event.stage.value}")
        return event

```

### Suppressing Compression for Specific Requests

Extensions can override behavior by mutating the event. Because the `PipelineExtensionManager.emit` method is fail-open, any unhandled exception inside a hook is logged and the pipeline continues with the current event.

```python
class SkipCompression:
    def on_pipeline_event(self, event: PipelineEvent):
        if event.stage == PipelineStage.INPUT_COMPRESSED:
            # Pretend compression already happened

            event.messages = event.metadata.get("original_messages")
        return event

```

### Manual Client Integration

You can attach the logger directly without entry points by passing it to the client constructor.

```python
from headroom.client import HeadroomClient
from logger_extension import SimpleLogger

client = HeadroomClient(
    api_key="…",                     # (placeholder – not shown)

    pipeline_extensions=SimpleLogger(),
)
response = client.chat(messages=[{"role": "user", "content": "Hi"}])

```

## Fail-Open Error Handling

The `PipelineExtensionManager.emit` method wraps each extension call in a `try/except` block. If a hook raises an exception, Headroom logs the error and continues processing the request with the existing `PipelineEvent` state. This fail-open design guarantees that a misbehaving extension cannot crash the proxy.

## Summary

- Headroom defines **eleven canonical pipeline lifecycle stages** in [`headroom/pipeline.py`](https://github.com/chopratejas/headroom/blob/main/headroom/pipeline.py), ranging from `setup` through `response_received`.
- You hook into them by implementing the **`PipelineExtension` protocol** and its `on_pipeline_event` method.
- Extensions can be **auto-discovered** via the `headroom.pipeline_extension` Python entry-point group or **attached manually** through the `HeadroomClient` constructor.
- Each hook receives a mutable **`PipelineEvent`** containing payloads, metadata, and stage context.
- The pipeline is **fail-open**; exceptions in extensions are logged but never halt a request.

## Frequently Asked Questions

### What are the Headroom pipeline lifecycle stages?

The eleven stages are `setup`, `pre_start`, `post_start`, `input_received`, `input_cached`, `input_routed`, `input_compressed`, `input_remembered`, `pre_send`, `post_send`, and `response_received`. They represent the full journey of a request through the Headroom proxy, as defined by the `PipelineStage` enum in [`headroom/pipeline.py`](https://github.com/chopratejas/headroom/blob/main/headroom/pipeline.py).

### How do I register a custom Headroom pipeline extension?

Implement the `PipelineExtension` protocol—specifically the `on_pipeline_event` method—and register it under the Python entry-point group `headroom.pipeline_extension` in your package metadata. Headroom’s `PipelineExtensionManager` automatically discovers and loads it at startup. Alternatively, instantiate the extension and pass it directly to `HeadroomClient` via `pipeline_extensions`.

### Can a pipeline extension modify the request before it reaches the LLM provider?

Yes. The `PipelineEvent` object passed to `on_pipeline_event` contains mutable fields such as `messages`, `tools`, `headers`, and `metadata`. You can mutate these in place and return the event; Headroom will carry your changes forward to subsequent stages, including `pre_send` just before the provider call.

### What happens if my Headroom pipeline extension throws an exception?

The `PipelineExtensionManager.emit` method catches exceptions in a `try/except` block and logs them. Because the pipeline is designed to be fail-open, the error does not halt request processing; the proxy simply continues with the current `PipelineEvent` state.