llm-io-normalizer
llm-io-normalizer is a lightweight, production-ready Model I/O normalization layer for OpenAI-compatible LLM APIs.
As reasoning models (like DeepSeek) become mainstream, the way providers return "thoughts" is highly fragmented. Some use native reasoning_content fields, while others mix <think>...</think> tags directly into the final output. Furthermore, stream interruptions and missing tags often leak private reasoning logic into user-facing answers.
llm-io-normalizer acts as an impenetrable shield. It provides a rock-solid, unified data contract so your business code never has to parse a <think> tag, handle broken streams, or guess provider-specific fields again.
This package is intentionally not a full API gateway. It does not implement authentication, rate limiting, or billing. It focuses purely on the reusable Python SDK layer for Model I/O normalization and reasoning extraction.
Quick Start
With llm-io-normalizer you can easily setup your LLM calls, reliably extract the reasoning process, and get a clean final answer without worrying about provider differences.
- Install the SDK
pip install llm-io-normalizer
- Setup your Gateway, Request, and generate responses
import asyncio
from llm_io_normalizer import LLMGateway, LLMRequest
async def main():
# 1. Initialize your Gateway
gateway = LLMGateway()
# 2. Make a request (Handles stream/non-stream & tag extraction automatically)
result = await gateway.generate(
LLMRequest(
model_name="deepseek-reasoner",
base_url="[https://api.your-provider.com/v1](https://api.your-provider.com/v1)",
api_key="YOUR_API_KEY",
messages=[{"role": "user", "content": "Explain quantum physics."}],
stream=True, # Streams by default
enable_thinking=True, # Enable reasoning if supported
)
)
# 3. Ensure the call was successful
result.require_ok()
# Get Clean Reasoning Output
print(f"Thoughts: {result.reasoning_text}")
# Get Clean Final Answer (Guaranteed no <think> tags!)
print(f"Answer: {result.answer_text}")
asyncio.run(main())
- Leverage the JSON output helper
from llm_io_normalizer.normalizers import extract_json_object
# Easily extract strict JSON from markdown-wrapped LLM responses
obj = extract_json_object('Here is the result: ```json\n{"score": 95}\n```')
assert obj == {"score": 95}
Project Structure
Usage
The public contract of LLMResult is intentionally small, stable, and predictable. Business code should depend on these normalized fields instead of reading raw provider fields directly.
result.answer_text # Pure, final answer content
result.reasoning_text # Extracted thinking process
result.ok # True if a valid answer was extracted
result.error_type # Categorized error (e.g., "EMPTY_ANSWER")
result.error_message # Detailed error description
Local Development
Below is a guide on setting up a local environment for contributing to or testing the llm-io-normalizer package.
Prerequisites and Dependencies
The project is developed using python. The minimum python version required is 3.10.
Setup
- Clone the repository
git clone https://github.com/wanghesong2019/llm-io-normalizer.git
cd llm-io-normalizer
- Install development dependencies It is recommended to use a virtual environment. Once activated, install the package in editable mode along with testing tools:
pip install -e ".[dev]"
- Run the test suite The project includes a robust suite of unit and mock tests to ensure the extraction engine and fallback mechanisms work flawlessly.
# Run linting
ruff check .
# Run tests
pytest -v
Configuration
llm-io-normalizer uses a flexible configuration system that allows you to adapt to any OpenAI-compatible provider without waiting for library updates.
Provider Field Mapping
Different proxy providers use different fields (reasoning_content, thoughts, reason) to return intermediate thoughts. You can seamlessly map any provider's custom schema by passing a ProviderFieldMapping during initialization:
from llm_io_normalizer import OpenAICompatibleGateway, ProviderFieldMapping
# Define custom fields for a new provider
custom_mapping = ProviderFieldMapping(
content_fields=("message_body", "text"),
reasoning_fields=("chain_of_thought", "deep_thought")
)
# Initialize gateway with custom mapping
gateway = OpenAICompatibleGateway(field_mapping=custom_mapping)
Architecture
Reasoning Extraction Engine
The internal normalizer algorithm is rigorously tested against edge cases to ensure private reasoning logic never leaks into user-facing answers. It handles:
- Multiple Tags: Alternating between thoughts and answers multiple times.
- Unclosed Tags: Stream interruptions or token limits that leave unclosed
<think>tags at the end of the text. - Redundant Data: Mixes of native provider reasoning fields with redundant text tags in the main content.
The engine processes these anomalies and produces clean answer_text and reasoning_text strings.
Resilience & Fallback
Network jitter and API quirks often result in dropped tokens or stuck states. llm-io-normalizer implements an automatic safety net (which can be toggled via LLMRequest parameters):
- Stream → Non-Stream Fallback (fallback_to_non_stream): If a stream ends prematurely leaving only thoughts and an EMPTY_ANSWER, it automatically retries with stream=False.
- Thinking → Non-Thinking Retry (retry_without_thinking_when_empty): If the model gets stuck in an infinite thought loop and consumes all tokens without giving a final answer, it can automatically retry with enable_thinking=False.
Contributing
We welcome contributions! Please feel free to submit a Pull Request or open an Issue for bug reports and feature requests. Ensure all tests pass (pytest) before submitting a PR.
License
This project is licensed under the MIT License.
Metadata
Release files for llm-io-normalizer 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| llm_io_normalizer-0.1.1.tar.gz | 20.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| llm_io_normalizer-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 36.3 kB
Release files / llm_io_normalizer-0.1.1.tar.gz
| Download URL | llm_io_normalizer-0.1.1.tar.gz |
|---|---|
| Size | 20.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a9275a58d1e20d394cfffeecfeb9877d184d72fafd0217c4cac576e08d75e0f9
|
|
BLAKE2b-256 checksum How to use checksums |
16a47f9a154240826131a129eb224a1f1b4b9c2bcf1f7c0c00172ecfc0516096
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 17, 2026.
Transparency logRelease files / llm_io_normalizer-0.1.1-py3-none-any.whl
| Download URL | llm_io_normalizer-0.1.1-py3-none-any.whl |
|---|---|
| Size | 16.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d29a07c7dcb46a24e723b1ad4e5f08caa5fe53bb0251f47c60d3b90730bb04cc
|
|
BLAKE2b-256 checksum How to use checksums |
483e5ad4d1363e2a2795c40897e4d3558125dc2d1b65fbcadd027c1f466147f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 17, 2026.
Transparency log