llm-inference-engine
A unified Python interface for LLM inference across multiple backends. Separates model configuration (how a model behaves) from inference engines (where it runs), so you can swap backends without changing application code.
Table of Contents
- Installation
- Quick Start
- LLM Configs
- Inference Engines
- Chat Methods
- End-to-End Examples
- Message Logging
- Config Cheat Sheet
Installation
pip install llm-inference-engine
Then install the client library for your backend:
| Backend | Install |
|---|---|
| OpenAI / Azure OpenAI | pip install openai |
| vLLM (OpenAI-compatible) | pip install openai |
| SGLang (OpenAI-compatible) | pip install openai |
| OpenRouter | pip install openai |
| Ollama | pip install ollama |
| Hugging Face Hub | pip install huggingface-hub |
| LiteLLM | pip install litellm |
Quick Start
from llm_inference_engine import BasicLLMConfig, OpenAIInferenceEngine
config = BasicLLMConfig(max_new_tokens=1024, temperature=0.5)
engine = OpenAIInferenceEngine(model="gpt-4.1-mini", config=config)
response = engine.chat([{"role": "user", "content": "Hello!"}])
print(response["response"])
LLM Configs
Configs control model behavior: token limits, temperature, message preprocessing, and response postprocessing. Pick the config that matches your model type.
BasicLLMConfig
For standard non-reasoning models (GPT-4.1, Llama, Qwen3-instruct, etc.).
from llm_inference_engine import BasicLLMConfig
config = BasicLLMConfig(max_new_tokens=2048, temperature=0.7)
Response format: {"response": "...", "tool_calls": [...]}
ReasoningLLMConfig
For always-on reasoning models that wrap thinking in <think>...</think> tags (DeepSeek-R1, Qwen3.5-thinking, GPT-OSS, Gemma 4, etc.). Automatically parses thinking and response content.
from llm_inference_engine import ReasoningLLMConfig
config = ReasoningLLMConfig(max_new_tokens=4096, temperature=0.6)
Response format: {"reasoning": "...", "response": "...", "tool_calls": [...]}
Custom thinking tokens (if the model uses different delimiters):
config = ReasoningLLMConfig(thinking_token_start="<reasoning>", thinking_token_end="</reasoning>")
Qwen3LLMConfig
For Qwen3 hybrid thinking models. Appends /think or /no_think tokens to system and user messages to toggle thinking mode. This is the token-based approach that works across all serving engines.
from llm_inference_engine import Qwen3LLMConfig
# Enable thinking
config = Qwen3LLMConfig(thinking_mode=True, max_new_tokens=4096)
# Disable thinking for faster responses
config = Qwen3LLMConfig(thinking_mode=False, max_new_tokens=2048)
Response format: {"reasoning": "...", "response": "...", "tool_calls": [...]} (reasoning is empty when thinking is disabled)
OpenAIReasoningLLMConfig
For OpenAI o-series models (o4-mini etc.). Handles reasoning effort, removes unsupported temperature parameter, and concatenates system prompts into user prompts (required by these models).
from llm_inference_engine import OpenAIReasoningLLMConfig
config = OpenAIReasoningLLMConfig(reasoning_effort="low") # "low", "medium", or "high"
Response format: {"reasoning": "...", "response": "...", "tool_calls": [...]}
Passing extra_body for Hybrid Thinking Models
Some models (Gemma 4, Qwen3.5, etc.) use server-side thinking control via the extra_body parameter in the OpenAI chat completion API. Since extra_body format depends on the serving engine (vLLM, SGLang, etc.) rather than the model itself, this is handled through kwargs on any config class rather than a dedicated config.
All config classes accept arbitrary kwargs that get passed through to the API call:
Gemma 4 with thinking enabled (vLLM / SGLang):
from llm_inference_engine import ReasoningLLMConfig
config = ReasoningLLMConfig(
max_new_tokens=4096,
temperature=1.0,
extra_body={
"chat_template_kwargs": {"enable_thinking": True}
}
)
Gemma 4 with thinking disabled:
from llm_inference_engine import BasicLLMConfig
config = BasicLLMConfig(
max_new_tokens=2048,
extra_body={
"chat_template_kwargs": {"enable_thinking": False}
}
)
Qwen3.5 with thinking enabled (vLLM):
from llm_inference_engine import ReasoningLLMConfig
config = ReasoningLLMConfig(
max_new_tokens=4096,
extra_body={
"chat_template_kwargs": {"enable_thinking": True}
}
)
Qwen3.5 with thinking disabled (vLLM):
from llm_inference_engine import BasicLLMConfig
config = BasicLLMConfig(
max_new_tokens=2048,
extra_body={
"chat_template_kwargs": {"enable_thinking": False}
}
)
Tip: Use
ReasoningLLMConfigwhen thinking is enabled (it parses<think>tags in the response). UseBasicLLMConfigwhen thinking is disabled.
Inference Engines
Engines define where the model runs. Each engine maps config parameters to the backend's expected format (e.g., max_new_tokens becomes max_completion_tokens for OpenAI, num_predict for Ollama).
OpenAIInferenceEngine
from llm_inference_engine import OpenAIInferenceEngine
engine = OpenAIInferenceEngine(model="gpt-4.1-mini", config=config)
Set OPENAI_API_KEY environment variable, or pass api_key= directly.
AzureOpenAIInferenceEngine
from llm_inference_engine import AzureOpenAIInferenceEngine
engine = AzureOpenAIInferenceEngine(
model="gpt-4.1-mini",
api_version="2025-03-01-preview",
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key"
)
Set AZURE_OPENAI_API_KEY environment variable, or pass api_key= directly.
Set AZURE_OPENAI_ENDPOINT environment variable, or pass azure_endpoint= directly.
VLLMInferenceEngine
For models served via vLLM.
from llm_inference_engine import VLLMInferenceEngine
engine = VLLMInferenceEngine(
model="Qwen/Qwen3.5-35B-A3B",
api_key="", # optional for local servers
base_url="http://localhost:8000/v1" # default
)
SGLangInferenceEngine
For models served via SGLang.
from llm_inference_engine import SGLangInferenceEngine
engine = SGLangInferenceEngine(
model="google/gemma-4-26B-A4B-it",
api_key="", # optional for local servers
base_url="http://localhost:30000/v1" # default
)
OpenRouterInferenceEngine
from llm_inference_engine import OpenRouterInferenceEngine
engine = OpenRouterInferenceEngine(model="openai/gpt-oss-120b")
Set OPENROUTER_API_KEY environment variable, or pass api_key= directly.
Reasoning field key compatibility
OpenAI-compatible servers have exposed the model's reasoning/thinking text under
different JSON keys across versions (e.g. vLLM/SGLang historically used
reasoning_content, while OpenRouter and newer builds use reasoning). By
default each engine probes the known keys in order (reasoning_content,
reasoning, reason) and uses the first one present, so reasoning is picked up
regardless of server version. If your server exposes it under a non-standard key,
override the probe order per instance:
engine = VLLMInferenceEngine(
model="Qwen/Qwen3.5-35B-A3B",
reasoning_keys=["my_custom_reasoning_field", "reasoning_content"],
)
This applies to OpenAIInferenceEngine, AzureOpenAIInferenceEngine,
VLLMInferenceEngine, SGLangInferenceEngine, and OpenRouterInferenceEngine.
OllamaInferenceEngine
from llm_inference_engine import OllamaInferenceEngine
engine = OllamaInferenceEngine(
model_name="qwen3:27b",
num_ctx=4096, # context length
keep_alive=300 # seconds to hold model in memory
)
HuggingFaceHubInferenceEngine
from llm_inference_engine import HuggingFaceHubInferenceEngine
engine = HuggingFaceHubInferenceEngine(model="meta-llama/Llama-4-Scout-17B-16E-Instruct")
LiteLLMInferenceEngine
from llm_inference_engine import LiteLLMInferenceEngine
engine = LiteLLMInferenceEngine(
model="openai/gpt-4.1-mini",
api_key="your-api-key"
)
Chat Methods
All engines support four chat methods:
| Method | Sync/Async | Returns |
|---|---|---|
chat() |
Sync | dict |
chat_stream() |
Sync | Generator[dict] |
chat_async() |
Async | dict |
chat_async_stream() |
Async | AsyncGenerator[dict] |
Sync
# Non-streaming
response = engine.chat(messages)
print(response["response"])
# With verbose output (prints to terminal in real-time)
response = engine.chat(messages, verbose=True)
# Streaming
for chunk in engine.chat_stream(messages):
print(chunk["data"], end="", flush=True) # chunk["type"] is "reasoning" or "response"
Async
import asyncio
async def main():
# Non-streaming
response = await engine.chat_async(messages)
print(response["response"])
# Streaming
stream = await engine.chat_async_stream(messages)
async for chunk in stream:
print(chunk["data"], end="", flush=True)
asyncio.run(main())
Streaming Chunk Format
Streaming methods yield dicts with type and data keys:
{"type": "reasoning", "data": "Let me think..."} # thinking content
{"type": "response", "data": "The answer is 42."} # response content
{"type": "tool_calls", "data": [{"name": "...", "arguments": "..."}]} # tool calls
End-to-End Examples
GPT-4.1 via Azure OpenAI
from llm_inference_engine import BasicLLMConfig, AzureOpenAIInferenceEngine
config = BasicLLMConfig(max_new_tokens=1024, temperature=0.5)
engine = AzureOpenAIInferenceEngine(
model="gpt-4.1-mini",
api_version="2025-03-01-preview",
azure_endpoint="https://your-resource.openai.azure.com/"
)
response = engine.chat([{"role": "user", "content": "Summarize this document."}])
print(response["response"])
GPT-OSS Reasoning via OpenRouter
from llm_inference_engine import OpenAIReasoningLLMConfig, OpenRouterInferenceEngine
config = ReasoningLLMConfig(reasoning_effort="medium")
engine = OpenRouterInferenceEngine(model="openai/gpt-oss-120b", config=config)
response = engine.chat([
{"role": "system", "content": "You are a math tutor."},
{"role": "user", "content": "Prove that sqrt(2) is irrational."}
])
print(response["reasoning"]) # chain-of-thought
print(response["response"]) # final answer
GPT-5.4 via OpenAI
from llm_inference_engine import ReasoningLLMConfig, OpenAIInferenceEngine
config = ReasoningLLMConfig(max_new_tokens=4096, temperature=0.7)
engine = OpenAIInferenceEngine(model="gpt-5.4-mini", config=config)
response = engine.chat([{"role": "user", "content": "Write a Python function to merge two sorted lists."}])
print(response["response"])
Qwen3.5 with Thinking via vLLM
from llm_inference_engine import ReasoningLLMConfig, VLLMInferenceEngine
config = ReasoningLLMConfig(
max_new_tokens=4096,
temperature=0.6,
extra_body={"chat_template_kwargs": {"enable_thinking": True}}
)
engine = VLLMInferenceEngine(model="Qwen/Qwen3.5-35B-A3B", base_url="http://localhost:8000/v1")
response = engine.chat([{"role": "user", "content": "What causes tides?"}])
print(response["reasoning"])
print(response["response"])
Gemma 4 VLM with Thinking + High Resolution for OCR (vLLM)
For vision tasks with Gemma 4, use mm_processor_kwargs in extra_body to control the vision token budget per image. Supported values: 70, 140, 280 (default), 560, or 1120 tokens. Higher values preserve more detail for OCR (https://huggingface.co/google/gemma-4-E4B).
Note:
max_soft_tokenscan also be set at server launch via--mm-processor-kwargs '{"max_soft_tokens": 1120}'. Theextra_bodyapproach below overrides per request.
import base64
from llm_inference_engine import ReasoningLLMConfig, VLLMInferenceEngine
config = ReasoningLLMConfig(
max_new_tokens=4096,
temperature=1.0,
top_p=0.95,
extra_body={
"chat_template_kwargs": {"enable_thinking": False}, # OCR doesn't require thinking
"mm_processor_kwargs": {"max_soft_tokens": 1120} # max detail for OCR
"top_k": 64
}
)
engine = VLLMInferenceEngine(model="google/gemma-4-27b-it", base_url="http://localhost:8000/v1")
with open("document.png", "rb") as f:
b64_image = base64.b64encode(f.read()).decode()
messages = [
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64_image}"}},
{"type": "text", "text": "Extract all text from this document."}
]
}
]
response = engine.chat(messages)
print(response["reasoning"]) # Gemma's thinking process
print(response["response"]) # extracted text
Gemma 4 VLM via SGLang
from llm_inference_engine import ReasoningLLMConfig, SGLangInferenceEngine
config = ReasoningLLMConfig(
max_new_tokens=4096,
temperature=1.0,
extra_body={
"chat_template_kwargs": {"enable_thinking": True},
"mm_processor_kwargs": {"max_soft_tokens": 1120}
}
)
engine = SGLangInferenceEngine(model="google/gemma-4-27b-it", base_url="http://localhost:30000/v1")
Async Batch Processing with Rate Limiting
import asyncio
from llm_inference_engine import BasicLLMConfig, VLLMInferenceEngine
config = BasicLLMConfig(max_new_tokens=512)
engine = VLLMInferenceEngine(
model="Qwen/Qwen3.5-32B",
max_concurrent_requests=10,
max_requests_per_minute=60,
config=config
)
questions = ["What is Python?", "What is Rust?", "What is Go?"]
async def main():
tasks = [
engine.chat_async([{"role": "user", "content": q}])
for q in questions
]
responses = await asyncio.gather(*tasks)
for q, r in zip(questions, responses):
print(f"Q: {q}\nA: {r['response']}\n")
asyncio.run(main())
Message Logging
Use MessagesLogger to capture full conversation history (prompts + responses).
from llm_inference_engine import MessagesLogger
logger = MessagesLogger(store_images=False) # replaces image URLs with "[image]"
response = engine.chat(messages, messages_logger=logger)
# Retrieve logged conversations
for conversation in logger.get_messages_log():
for msg in conversation:
print(f"{msg['role']}: {msg['content'][:100]}")
Token counts and call metadata
The last message of each logged call is the assistant response. Besides role, content, and reasoning, it carries:
{"role": "assistant", "content": "...", "reasoning": "...",
"token_count": {"input": 18, "output": 28, "total": 46, "cached": 0},
"finish_reason": "stop", # e.g. "stop", "length", "tool_calls"
"timestamp": "2026-09-21T14:03:12.482Z", # UTC, when the request was sent
"latency": 1.70} # seconds to the complete response (streaming: to the final chunk)
Sum the counts across all logged calls:
logger.get_token_usage()
# {"input": 105, "output": 95, "total": 200, "cached": None}
- Counts come from the backend. A count the backend does not report is
None; nothing is estimated. outputincludes reasoning tokens. Backends do not split them consistently, so they are not logged separately.cachedis reported by OpenAI and Azure. vLLM reports it only when served with--enable-prompt-tokens-details, and SGLang only with--enable-cache-report. Ollama and HuggingFace Hub do not report it.
Streaming changes the request. Servers report token counts for a streamed call only when asked. When a
messages_loggeris passed to a streaming call (chat_stream,chat_async_stream, orchat(verbose=True)), OpenAI-schema engines addstream_options={"include_usage": True}to the request. The server then sends one extra final chunk that carries the counts. Requests made without a logger are unchanged. Ollama always reports counts on its final chunk, so its requests are never changed.
Config Cheat Sheet
| Model | Config | Key Settings |
|---|---|---|
| GPT-4.1, GPT-5.4 | BasicLLMConfig |
max_new_tokens, temperature |
| o4-mini, GPT-OSS | OpenAIReasoningLLMConfig |
reasoning_effort |
| Qwen3 (hybrid) | Qwen3LLMConfig |
thinking_mode |
| Qwen3.5 (hybrid, vLLM) | ReasoningLLMConfig |
extra_body={"chat_template_kwargs": {"enable_thinking": True}} |
| Gemma 4 (hybrid) | ReasoningLLMConfig |
extra_body={"chat_template_kwargs": {"enable_thinking": True}} |
| DeepSeek-R1 | ReasoningLLMConfig |
defaults |
| Llama, Mistral | BasicLLMConfig |
max_new_tokens, temperature |
Metadata
Release files for llm-inference-engine 0.1.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| llm_inference_engine-0.1.8.tar.gz | 24.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| llm_inference_engine-0.1.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.6 kB
Release files / llm_inference_engine-0.1.8.tar.gz
| Download URL | llm_inference_engine-0.1.8.tar.gz |
|---|---|
| Size | 24.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ea2391c70115b0f395b1da8b6161c5365bc482f541063cd260723ff3ec7bb589
|
|
BLAKE2b-256 checksum How to use checksums |
2447632da71259d37c8ac727bc29a63310b580165769587d41bdf6fff9854be0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/1.8.3 CPython/3.11.8 Linux/6.8.0-138-generic
|
Release files / llm_inference_engine-0.1.8-py3-none-any.whl
| Download URL | llm_inference_engine-0.1.8-py3-none-any.whl |
|---|---|
| Size | 22.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a10b28e748d37ae7ba47c927ecb67e909a0fae35202e0694f8adb00316a8fd41
|
|
BLAKE2b-256 checksum How to use checksums |
9b79efd5cab99e8f26d4d8bb0c8c2a7759ac9f9c1c1fa5b5344a113200b9e9fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/1.8.3 CPython/3.11.8 Linux/6.8.0-138-generic
|
Security for llm-inference-engine 0.1.8
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.