Skip to main content

llm-io-normalizer

llm-io-normalizer Logo

PyPI version Python version License: MIT

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.

  1. Install the SDK
pip install llm-io-normalizer
  1. 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())
  1. 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

  1. Clone the repository
git clone https://github.com/wanghesong2019/llm-io-normalizer.git
cd llm-io-normalizer
  1. 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]"
  1. 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

llm-io-normalizer 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)

Source distribution for llm-io-normalizer 0.1.1
File Size Uploaded
llm_io_normalizer-0.1.1.tar.gz 20.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for llm-io-normalizer 0.1.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page