Developer Guide: LLM Response Cleaning and Formatting
This guide provides a deep dive into how WilmerAI processes and cleans the raw output from a Large Language Model (LLM) before it is returned to the user. This process is essential for removing diagnostic tags, boilerplate prefixes, and other unwanted artifacts from the final response.
The core principle is that all text cleaning occurs after the raw, unmodified response is received from the backing
LLM via the llmapis layer. The logic is implemented in parallel for non-streaming and streaming responses to ensure
functionally identical outcomes.
1. Handling Non-Streaming Responses
For a complete, non-streaming response, the entire block of text is processed at once in a sequential series of cleaning steps.
Location of Logic
- Primary Function:
post_process_llm_outputinMiddleware/utilities/streaming_utils.py. - Calling Context: This function is typically invoked by a higher-level workflow processor after a non-streaming node has executed.
Order of Operations
The post_process_llm_output function applies cleaning rules in a precise order. The sequence is as follows:
- Thinking Tag Removal: The text is first passed to
remove_thinking_from_text. This function's behavior depends on the endpoint configuration:- Standard Mode: It looks for a
startThinkTagwithin an initialopeningTagGracePeriod(e.g., the first 100 characters). If found, it then searches for the correspondingendThinkTag. If both are found, the entire block ( including tags) is removed. If the opening tag isn't in the grace period or the closing tag is missing, the * original text is returned unmodified*. expectOnlyClosingThinkTagMode: If this flag isTrue, the function searches for theendThinkTagonly. If found, it discards everything before it and returns only the content that follows. If the tag is not found, it returns an empty string.
- Standard Mode: It looks for a
- Leading Whitespace Stripped: The result is left-stripped to prepare it for prefix matching.
- Workflow-Level Custom Prefixes: It checks for prefixes defined by
removeCustomTextFromResponseStartandresponseStartTextToRemovein the workflow node's config. TheresponseStartTextToRemovevalue is an array of strings. The function iterates through this array and removes the first prefix that matches the beginning of the response. - Endpoint-Level Custom Prefixes: It performs the same array-based iteration for prefixes defined by
removeCustomTextFromResponseStartEndpointWideandresponseStartTextToRemoveEndpointWidein the endpoint's config. - Timestamp Text: It removes the hardcoded
[Sent less than a minute ago]prefix if the node'saddDiscussionIdTimestampsForLLMflag isTrue. - "Assistant:" Prefix: It removes the
Assistant:prefix based on a composite check of global settings (get_is_chat_complete_add_user_assistant()andget_is_chat_complete_add_missing_assistant()). - Final Trim: If
trimBeginningAndEndLineBreaksisTruein the endpoint config, a final.strip()is applied to remove any leading or trailing whitespace and line breaks.
2. Handling Streaming Responses
Cleaning a streaming response is more complex, as it must be done chunk-by-chunk. This process is orchestrated by the
$StreamingResponseHandler$ class, which uses a stateful helper for tag removal.
Location of Logic
- Primary Class:
$StreamingResponseHandler$inMiddleware/workflows/streaming/response_handler.py. - Key Dependency: It relies on the
$StreamingThinkRemover$class fromMiddleware/utilities/streaming_utils.pyfor stateful tag removal.
The Two-Stage Cleaning Process
$StreamingResponseHandler$ uses a two-stage process to clean the stream:
Stage 1: Thinking Tag Removal (Continuous)
Every delta chunk from the LLM is immediately passed to an instance of $StreamingThinkRemover$. This stateful class
uses an internal buffer to find and remove thinking blocks in real-time.
- Standard Mode: The remover buffers initial chunks up to the
openingTagGracePeriodlimit. If thestartThinkTagappears within this window, it enters a "thinking" state and discards all subsequent text until theendThinkTagis found. If the grace period passes without an opening tag, the remover stops looking and passes all future text through untouched. If the stream ends with an unterminated think block, the remover flushes its buffer, returning the opening tag and the buffered content. expectOnlyClosingThinkTagMode: The remover buffers and discards all text until it finds theendThinkTag. Once the tag is found, it discards the buffer and begins yielding all subsequent text.
The rest of the system only sees the "clean" stream output by $StreamingThinkRemover$.
Stage 2: Prefix Removal (One-Time at Start)
The $StreamingResponseHandler$ takes the cleaned stream from Stage 1 and performs a one-time buffering operation to
handle all other prefix removals.
- Buffering: It collects the initial chunks from
$StreamingThinkRemover$into its own_prefix_bufferuntil a sufficient amount of text is gathered to check for prefixes. No text is yielded to the client during this phase. - Processing: Once the buffer is ready, the
_strip_prefixes_from_buffermethod is called once. This method applies the exact same ordered logic as the non-streamingpost_process_llm_outputfunction, including iterating through the arrays of custom prefixes. - Streaming: The cleaned text from the buffer is yielded to the client. From this point on, the prefix buffer is
disabled, and all subsequent chunks from
$StreamingThinkRemover$are yielded directly to the client without delay.
3. How to Add a New Rule
To add a new hardcoded prefix removal rule, you must add it to both the non-streaming and streaming logic to ensure consistent behavior.
- Non-Streaming: Add your logic to the sequence in
post_process_llm_outputinMiddleware/utilities/streaming_utils.py. - Streaming: Add the identical logic to the
_strip_prefixes_from_buffermethod inMiddleware/workflows/streaming/response_handler.py.
Example: Remove a [DIAGNOSTIC]: prefix.
# In both post_process_llm_output and _strip_prefixes_from_buffer...
# ... (existing custom text and timestamp removals) ...
# NEW RULE: Add it here in the sequence
if content.startswith("[DIAGNOSTIC]:"):
content = content[len("[DIAGNOSTIC]:"):].lstrip()
# ... (existing "Assistant:" prefix removal) ...
4. Summary of Logic Locations
| Feature / Rule | File for Non-Streaming Logic | File for Streaming Logic |
|---|---|---|
| Thinking Tags (start/end) | streaming_utils.py (remove_thinking_from_text) |
streaming_utils.py (StreamingThinkRemover) |
| Workflow Custom Prefixes (Array) | streaming_utils.py (post_process_llm_output) |
response_handler.py (_strip_prefixes_from_buffer) |
| Endpoint Custom Prefixes (Array) | streaming_utils.py (post_process_llm_output) |
response_handler.py (_strip_prefixes_from_buffer) |
Timestamp ([Sent...ago]) |
streaming_utils.py (post_process_llm_output) |
response_handler.py (_strip_prefixes_from_buffer) |
| "Assistant:" Prefix | streaming_utils.py (post_process_llm_output) |
response_handler.py (_strip_prefixes_from_buffer) |
| Leading/Trailing Whitespace | streaming_utils.py (post_process_llm_output) |
response_handler.py (_strip_prefixes_from_buffer) |