Developer Guide: The Streaming Request Pipeline
This guide provides a comprehensive walkthrough of the lifecycle of a streaming request in WilmerAI, from its arrival at
an API endpoint to the final Server-Sent Event (SSE) being delivered to the client. Understanding this data flow is
essential for developers aiming to extend or debug the platform's real-time response capabilities.
The architecture is founded on a clear separation of concerns:
- The API Layer (
Middleware/api/) acts as the entry point, translating external request schemas and generating a
unique request_id for cancellation tracking.
- The Workflow Engine (
Middleware/workflows/) orchestrates the overall logic, executing a series of steps defined
in a JSON configuration.
- The LLM API Layer (
Middleware/llmapis/) abstracts communication with various backend LLMs, returning a raw but
standardized data stream that respects cancellation signals.
- The Stream Processing Layer (
Middleware/workflows/streaming/) is responsible for all final cleaning, formatting,
and conversion of the raw stream into a client-ready SSE format.
1. Architectural Flow: The Journey of a Streaming Request
Let's trace a request made to the OpenAI-compatible /v1/chat/completions endpoint with stream=true.
Step 1: API Ingress and Transformation
Request Arrival: An external client sends a POST request to /v1/chat/completions.
Routing: The ApiServer routes the request to the ChatCompletionsAPI MethodView
within openai_api_handler.py.
Initial Processing: The post() method is executed.
- It generates a unique
request_id (e.g., a UUID) and stores it in Flask's g context object. This ID is the
key to tracking the request for cancellation.
- It sets a global variable,
instance_global_variables.API_TYPE = "openaichatcompletion", to inform downstream
components which response schema to use.
- It transforms the incoming JSON payload into the standardized internal
messages list.
- It calls
handle_user_prompt(request_id, messages, stream=True) from workflow_gateway.py.
- It immediately returns a Flask
Response object, wrapping the generator returned by the gateway. This begins
sending the response headers to the client while the backend processes the request.
# In Middleware/api/handlers/impl/openai_api_handler.py
request_id = str(uuid.uuid4())
g.current_request_id = request_id
if stream:
# The generator from the engine is returned directly to Flask
return Response(
handle_user_prompt(request_id, transformed_messages, stream=True),
mimetype='text/event-stream'
)
Step 2: Workflow Initiation and Execution
- Gateway Handoff: The
handle_user_prompt function in workflow_gateway.py receives the request_id and acts as
the bridge to the logic engine, calling the $WorkflowManager$.
- Manager Setup: The
$WorkflowManager$ (workflow_manager.py) is instantiated. It loads the relevant workflow
JSON file and creates a registry mapping all valid node type strings to their corresponding handler class
instances.
- Processor Delegation: The manager creates an instance of
$WorkflowProcessor$ and delegates the execution to it,
passing along the request_id, messages, stream flag, and all necessary dependencies.
- Execution Loop: The
$WorkflowProcessor.execute() method iterates through the nodes defined in the workflow.
Before executing each node, it checks if the request_id has been cancelled.
- Context Creation: For each node, the processor assembles a new, comprehensive
$ExecutionContext$ object.
This dataclass contains the complete runtime state: the node's config, the full conversation history, outputs from
previous nodes, and service references.
- Responder Identification: The processor identifies the "responder" node (the one whose output is sent to the
user), which is typically marked with
"returnToUser": true.
- Handler Dispatch: For the responder node, the processor passes the
$ExecutionContext$ to the appropriate
node handler (e.g., $StandardNodeHandler$). The handler makes the final call to the LLM.
Step 3: LLM API Abstraction
- Service Call: The node handler calls the
$LlmApiService.get_response_from_llm() method, passing
the request_id.
- Handler Factory: The
$LlmApiService$ uses its create_api_handler() factory method to instantiate the
correct $LlmApiHandler$ (e.g., $OllamaChatHandler$) based on the endpoint's configuration.
- Streaming Request: The service calls the handler's
handle_streaming() method, passing the request_id. The
handler prepares the API-specific payload and uses the requests library to make the HTTP call with stream=True.
- Standardization: As raw data chunks arrive from the LLM, the handler's
_process_stream_data() method parses the
API-specific format (e.g., line-delimited JSON or SSE) and yields a **raw, standardized dictionary
**: {'token': str, 'finish_reason': str|None}. Before processing each chunk, the handler
checks cancellation_service.is_cancelled(request_id), allowing the stream to be interrupted mid-generation. This
uncleaned, standardized generator is the sole output of the llmapis layer.
Step 4: Final Stream Processing and Formatting
The raw dictionary generator travels back up to the $WorkflowProcessor$, which delegates it to the final stage.
Handoff to Handler: The processor passes the raw generator and the request_id to an instance
of $StreamingResponseHandler$.
# In Middleware/workflows/processors/workflows_processor.py
if self.stream and isinstance(llm_result, Generator):
stream_handler = StreamingResponseHandler(..., request_id=self.request_id)
# The final, client-facing generator is produced here
yield from stream_handler.process_stream(llm_result)
Optimized Stream Cleaning: The $StreamingResponseHandler.process_stream() method in response_handler.py
performs all user-facing stream cleaning using an optimistic prefix matching algorithm to minimize latency.
- Continuous Cleaning: Every chunk from the raw generator is first passed to
$StreamingThinkRemover$. This
stateful helper identifies and removes content within <think>...</think> tags in real-time.
- Optimistic Prefix Removal: The handler buffers the initial clean chunks. It continuously checks if the
buffered text could potentially match any known prefixes (e.g.,
"Assistant: "). If the incoming text makes it
impossible to match a prefix, the entire buffer is released immediately. This avoids unnecessary waiting and
delivers the initial tokens to the user faster than a fixed-buffer approach. The buffer is only held until the
stream ends or a buffer limit is reached if a prefix match remains possible.
JSON Construction: For each cleaned token, the handler calls api_helpers.build_response_json(). This helper
function acts as a dispatcher. It reads the globally set API_TYPE and calls the appropriate method on
the $ResponseBuilderService$ (e.g., build_openai_chat_completion_chunk()) to construct the schema-compliant JSON
chunk. For certain APIs like Ollama, the request_id is included in the chunk.
SSE Formatting: The resulting JSON string is passed to api_helpers.sse_format(), which prepends data: to
conform to the Server-Sent Event specification.
Final Yield: The handler yields the final, clean, SSE-formatted string. This travels all the way back to the
Flask Response object and is sent to the client.
2. Key Component Responsibilities
| Component |
File Location |
Key Responsibility in Streaming |
| API Handlers |
Middleware/api/handlers/impl/ |
Translate the client request, generate a unique request_id, set the global API_TYPE, and hand off to the workflow_gateway. |
$WorkflowProcessor$ |
workflows/processors/workflows_processor.py |
Orchestrate the workflow, create the $ExecutionContext$ for each node, and delegate the raw LLM stream to the $StreamingResponseHandler$. |
$LlmApiService$ |
llmapis/llm_api.py |
Act as a factory to select the correct LLM handler and return a raw, standardized generator of dictionaries that respects cancellation. |
$StreamingResponseHandler$ |
workflows/streaming/response_handler.py |
Perform all content cleaning on the stream using an optimistic prefix matching algorithm to minimize latency, removing thinking tags and prefixes. |
$ResponseBuilderService$ |
services/response_builder_service.py |
Act as the single source of truth for constructing the final JSON payload for each chunk, ensuring it matches the client's expected schema. |
3. How to Extend the Pipeline
The most common extension is adding a new text-cleaning rule to the stream. Because the logic is mirrored for streaming
and non-streaming responses, any new rule must be implemented in both places to ensure consistent behavior.
Example: Add a New [DIAGNOSTIC]: Prefix Removal Rule
Update Non-Streaming Logic:
- Open
Middleware/utilities/streaming_utils.py.
- Locate the
post_process_llm_output function.
- Add your new logic into the existing sequence of prefix removals.
# In streaming_utils.py -> post_process_llm_output()
# ... after existing custom prefix removal ...
# NEW RULE
if content.startswith("[DIAGNOSTIC]:"):
content = content[len("[DIAGNOSTIC]:"):].lstrip()
# ... before existing "Assistant:" prefix removal ...
Update Streaming Logic:
- Open
Middleware/workflows/streaming/response_handler.py.
- Locate the
_process_prefixes_from_buffer method, which contains the logic for stripping prefixes from the
initial buffered text.
- Add the identical logic to this method, ensuring it appears in the same order as in the non-streaming function.
# In response_handler.py -> _process_prefixes_from_buffer()
# ... after existing custom prefix removal ...
# NEW RULE (Identical logic)
if content.startswith("[DIAGNOSTIC]:"):
content = content[len("[DIAGNOSTIC]:"):].lstrip()
# ... before existing "Assistant:" prefix removal ...
By adding the rule to both locations, you ensure that the system will produce the same clean output regardless of
whether the user requested a streaming or non-streaming response.
1---2name: 2049-wilmer-prompt-flow-beginning-to-end-777dcc2d3description: **Developer Guide: The Streaming Request Pipeline**4---5### **Developer Guide: The Streaming Request Pipeline**67This guide provides a comprehensive walkthrough of the lifecycle of a streaming request in WilmerAI, from its arrival at8an API endpoint to the final Server-Sent Event (SSE) being delivered to the client. Understanding this data flow is9essential for developers aiming to extend or debug the platform's real-time response capabilities.1011The architecture is founded on a clear separation of concerns:1213* The **API Layer** (`Middleware/api/`) acts as the entry point, translating external request schemas and generating a14 unique **`request_id`** for cancellation tracking.15* The **Workflow Engine** (`Middleware/workflows/`) orchestrates the overall logic, executing a series of steps defined16 in a JSON configuration.17* The **LLM API Layer** (`Middleware/llmapis/`) abstracts communication with various backend LLMs, returning a raw but18 standardized data stream that respects cancellation signals.19* The **Stream Processing Layer** (`Middleware/workflows/streaming/`) is responsible for all final cleaning, formatting,20 and conversion of the raw stream into a client-ready SSE format.2122-----2324## 1\. Architectural Flow: The Journey of a Streaming Request2526Let's trace a request made to the OpenAI-compatible `/v1/chat/completions` endpoint with `stream=true`.2728### **Step 1: API Ingress and Transformation**29301. **Request Arrival:** An external client sends a POST request to `/v1/chat/completions`.31322. **Routing:** The `ApiServer` routes the request to the `ChatCompletionsAPI` `MethodView`33 within `openai_api_handler.py`.34353. **Initial Processing:** The `post()` method is executed.3637 * It generates a unique **`request_id`** (e.g., a UUID) and stores it in Flask's `g` context object. This ID is the38 key to tracking the request for cancellation.39 * It sets a global variable, `instance_global_variables.API_TYPE = "openaichatcompletion"`, to inform downstream40 components which response schema to use.41 * It transforms the incoming JSON payload into the standardized internal `messages` list.42 * It calls `handle_user_prompt(request_id, messages, stream=True)` from `workflow_gateway.py`.43 * It immediately returns a Flask `Response` object, wrapping the generator returned by the gateway. This begins44 sending the response headers to the client while the backend processes the request.4546 <!-- end list -->4748 ```python49 # In Middleware/api/handlers/impl/openai_api_handler.py50 request_id = str(uuid.uuid4())51 g.current_request_id = request_id5253 if stream:54 # The generator from the engine is returned directly to Flask55 return Response(56 handle_user_prompt(request_id, transformed_messages, stream=True),57 mimetype='text/event-stream'58 )59 ```6061### **Step 2: Workflow Initiation and Execution**62631. **Gateway Handoff:** The `handle_user_prompt` function in `workflow_gateway.py` receives the `request_id` and acts as64 the bridge to the logic engine, calling the `$WorkflowManager$`.652. **Manager Setup:** The `$WorkflowManager$` (`workflow_manager.py`) is instantiated. It loads the relevant workflow66 JSON file and creates a registry mapping all valid node `type` strings to their corresponding handler class67 instances.683. **Processor Delegation:** The manager creates an instance of `$WorkflowProcessor$` and delegates the execution to it,69 passing along the `request_id`, messages, stream flag, and all necessary dependencies.704. **Execution Loop:** The `$WorkflowProcessor.execute()` method iterates through the nodes defined in the workflow.71 Before executing each node, it checks if the `request_id` has been cancelled.72 * **Context Creation:** For **each node**, the processor assembles a new, comprehensive `$ExecutionContext$` object.73 This dataclass contains the complete runtime state: the node's config, the full conversation history, outputs from74 previous nodes, and service references.75 * **Responder Identification:** The processor identifies the "responder" node (the one whose output is sent to the76 user), which is typically marked with `"returnToUser": true`.77 * **Handler Dispatch:** For the responder node, the processor passes the `$ExecutionContext$` to the appropriate78 node handler (e.g., `$StandardNodeHandler$`). The handler makes the final call to the LLM.7980### **Step 3: LLM API Abstraction**81821. **Service Call:** The node handler calls the `$LlmApiService.get_response_from_llm()` method, passing83 the `request_id`.842. **Handler Factory:** The `$LlmApiService$` uses its `create_api_handler()` factory method to instantiate the85 correct `$LlmApiHandler$` (e.g., `$OllamaChatHandler$`) based on the endpoint's configuration.863. **Streaming Request:** The service calls the handler's `handle_streaming()` method, passing the `request_id`. The87 handler prepares the API-specific payload and uses the `requests` library to make the HTTP call with `stream=True`.884. **Standardization:** As raw data chunks arrive from the LLM, the handler's `_process_stream_data()` method parses the89 API-specific format (e.g., line-delimited JSON or SSE) and `yield`s a **raw, standardized dictionary90 **: `{'token': str, 'finish_reason': str|None}`. Before processing each chunk, the handler91 checks `cancellation_service.is_cancelled(request_id)`, allowing the stream to be interrupted mid-generation. This92 uncleaned, standardized generator is the sole output of the `llmapis` layer.9394### **Step 4: Final Stream Processing and Formatting**9596The raw dictionary generator travels back up to the `$WorkflowProcessor$`, which delegates it to the final stage.97981. **Handoff to Handler:** The processor passes the raw generator and the `request_id` to an instance99 of `$StreamingResponseHandler$`.100101 ```python102 # In Middleware/workflows/processors/workflows_processor.py103 if self.stream and isinstance(llm_result, Generator):104 stream_handler = StreamingResponseHandler(..., request_id=self.request_id)105 # The final, client-facing generator is produced here106 yield from stream_handler.process_stream(llm_result)107 ```1081092. **Optimized Stream Cleaning:** The `$StreamingResponseHandler.process_stream()` method in `response_handler.py`110 performs all user-facing stream cleaning using an **optimistic prefix matching** algorithm to minimize latency.111112 * **Continuous Cleaning:** Every chunk from the raw generator is first passed to `$StreamingThinkRemover$`. This113 stateful helper identifies and removes content within `<think>...</think>` tags in real-time.114 * **Optimistic Prefix Removal:** The handler buffers the initial clean chunks. It continuously checks if the115 buffered text could *potentially* match any known prefixes (e.g., `"Assistant: "`). If the incoming text makes it116 impossible to match a prefix, the entire buffer is released immediately. This avoids unnecessary waiting and117 delivers the initial tokens to the user faster than a fixed-buffer approach. The buffer is only held until the118 stream ends or a buffer limit is reached if a prefix match remains possible.1191203. **JSON Construction:** For each cleaned token, the handler calls `api_helpers.build_response_json()`. This helper121 function acts as a dispatcher. It reads the globally set `API_TYPE` and calls the appropriate method on122 the `$ResponseBuilderService$` (e.g., `build_openai_chat_completion_chunk()`) to construct the schema-compliant JSON123 chunk. For certain APIs like Ollama, the `request_id` is included in the chunk.1241254. **SSE Formatting:** The resulting JSON string is passed to `api_helpers.sse_format()`, which prepends ` data: ` to126 conform to the Server-Sent Event specification.1271285. **Final Yield:** The handler `yield`s the final, clean, SSE-formatted string. This travels all the way back to the129 Flask `Response` object and is sent to the client.130131-----132133## 2\. Key Component Responsibilities134135| Component | File Location | Key Responsibility in Streaming |136| :----------------------------- | :-------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |137| **API Handlers** | `Middleware/api/handlers/impl/` | Translate the client request, **generate a unique `request_id`**, set the global `API_TYPE`, and hand off to the `workflow_gateway`. |138| **`$WorkflowProcessor$`** | `workflows/processors/workflows_processor.py` | Orchestrate the workflow, create the `$ExecutionContext$` for each node, and delegate the raw LLM stream to the `$StreamingResponseHandler$`. |139| **`$LlmApiService$`** | `llmapis/llm_api.py` | Act as a factory to select the correct LLM handler and return a **raw, standardized generator of dictionaries** that respects cancellation. |140| **`$StreamingResponseHandler$`** | `workflows/streaming/response_handler.py` | Perform all **content cleaning** on the stream using an optimistic prefix matching algorithm to minimize latency, removing thinking tags and prefixes. |141| **`$ResponseBuilderService$`** | `services/response_builder_service.py` | Act as the single source of truth for **constructing the final JSON payload** for each chunk, ensuring it matches the client's expected schema. |142143-----144145## 3\. How to Extend the Pipeline146147The most common extension is adding a new text-cleaning rule to the stream. Because the logic is mirrored for streaming148and non-streaming responses, any new rule must be implemented in both places to ensure consistent behavior.149150### **Example: Add a New `[DIAGNOSTIC]:` Prefix Removal Rule**1511521. **Update Non-Streaming Logic:**153154 * Open `Middleware/utilities/streaming_utils.py`.155 * Locate the `post_process_llm_output` function.156 * Add your new logic into the existing sequence of prefix removals.157158 <!-- end list -->159160 ```python161 # In streaming_utils.py -> post_process_llm_output()162 # ... after existing custom prefix removal ...163164 # NEW RULE165 if content.startswith("[DIAGNOSTIC]:"):166 content = content[len("[DIAGNOSTIC]:"):].lstrip()167168 # ... before existing "Assistant:" prefix removal ...169 ```1701712. **Update Streaming Logic:**172173 * Open `Middleware/workflows/streaming/response_handler.py`.174 * Locate the `_process_prefixes_from_buffer` method, which contains the logic for stripping prefixes from the175 initial buffered text.176 * Add the identical logic to this method, ensuring it appears in the same order as in the non-streaming function.177178 <!-- end list -->179180 ```python181 # In response_handler.py -> _process_prefixes_from_buffer()182 # ... after existing custom prefix removal ...183184 # NEW RULE (Identical logic)185 if content.startswith("[DIAGNOSTIC]:"):186 content = content[len("[DIAGNOSTIC]:"):].lstrip()187188 # ... before existing "Assistant:" prefix removal ...189 ```190191By adding the rule to both locations, you ensure that the system will produce the same clean output regardless of192whether the user requested a streaming or non-streaming response.