Intelligent temperature routing for LLM queries using local embeddings
Project description
multi-temp-lm
Intelligent temperature routing for LLM queries using local embeddings.
multi-temp-lm provides a callable MultiTempRouter object that automatically classifies query intent and selects the optimal temperature value for any LLM SDK. Zero configuration, zero API calls, and universal SDK compatibility.
Quick Start
from multi_temp_lm import MultiTempRouter
import litellm
router = MultiTempRouter()
response = litellm.completion(
model="gpt-4",
messages=[{"role": "user", "content": "Write a poem about stars"}],
temperature=router, # Auto-selects optimal temperature
)
Installation
pip install multi-temp-lm
Dependencies:
sentence-transformers(for embeddings; model downloads on first use ~22MB with ONNX)numpyonnxruntime(optional, for quantized inference)
Features
- Intent-aware temperature routing — Automatically classifies query type and selects optimal temperature
- Local embeddings — Uses
sentence-transformerswith ONNX quantization (~22MB), no API calls - Universal SDK compatibility — Works with LiteLLM, OpenAI, Anthropic, Google, Cohere, Ollama, and more
- Async support — Non-blocking with
asyncio.to_thread() - LRU cache — Repeated queries return instantly
- Customizable — Override temperatures, add categories, or plug in your own classifier
- Type annotated — Ships with
py.typedmarker for full mypy support
How It Works
multi-temp-lm uses a small, local sentence-transformer model (all-MiniLM-L6-v2) to classify the intent of every user query. Based on the detected category, it returns a temperature value optimized for that task:
- Factual / analytical queries → low temperature (deterministic, precise)
- Creative / coding queries → moderate temperature (balanced coherence and diversity)
- Open-ended chat → default temperature
The router runs entirely on your machine. It does not send any data to external APIs for classification.
Default Temperatures
| Category | Temperature | Rationale |
|---|---|---|
factual |
0.2 | High determinism for facts and recall |
analytical |
0.2 | Low variance for structured analysis |
creative |
0.9 | High diversity for poems, stories, and brainstorming |
coding |
0.3 | Focused output for code generation and debugging |
reasoning |
0.4 | Step-by-step logic with controlled creativity |
summarization |
0.3 | Consistent, concise summaries |
conversational |
0.7 | Natural, balanced dialogue |
default |
0.7 | Fallback for unrecognized queries |
Override any of these with the temperature_map constructor argument.
Usage Examples
LiteLLM (sync)
import litellm
from multi_temp_lm import MultiTempRouter
router = MultiTempRouter()
response = litellm.completion(
model="gpt-4",
messages=[{"role": "user", "content": "Explain quantum mechanics."}],
temperature=router,
)
LiteLLM (async)
response = await litellm.acompletion(
model="gpt-4",
messages=messages,
temperature=await router.aroute(messages),
)
OpenAI SDK (sync)
from openai import OpenAI
from multi_temp_lm import MultiTempRouter
client = OpenAI()
router = MultiTempRouter()
response = client.chat.completions.create(
model="gpt-4",
messages=messages,
temperature=router,
)
OpenAI SDK (async)
from openai import AsyncOpenAI
client = AsyncOpenAI()
response = await client.chat.completions.create(
model="gpt-4",
messages=messages,
temperature=await router.aroute(messages),
)
Anthropic
The Anthropic SDK does not accept callable temperature values. Use router.for_messages(messages) for explicit pre-computation:
import anthropic
from multi_temp_lm import MultiTempRouter
client = anthropic.Anthropic()
router = MultiTempRouter()
response = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
messages=messages,
temperature=router.for_messages(messages),
)
Google Vertex AI
import vertexai
from vertexai.generative_models import GenerativeModel
from multi_temp_lm import MultiTempRouter
router = MultiTempRouter()
model = GenerativeModel("gemini-1.5-flash")
response = model.generate_content(
messages,
generation_config={"temperature": router.for_messages(messages)},
)
Cohere
import cohere
from multi_temp_lm import MultiTempRouter
client = cohere.Client()
router = MultiTempRouter()
response = client.chat(
model="command-r",
message="Explain quantum mechanics.",
temperature=router.for_messages(messages),
)
Ollama
import ollama
from multi_temp_lm import MultiTempRouter
router = MultiTempRouter()
response = ollama.chat(
model="llama3",
messages=messages,
options={"temperature": router.for_messages(messages)},
)
Generic for_messages() Pattern
If your SDK does not support callable temperature, use the explicit pre-computation pattern. This works with any SDK that accepts a numeric temperature:
from multi_temp_lm import MultiTempRouter
router = MultiTempRouter()
temperature = router.for_messages(messages)
# Pass `temperature` to any SDK call
API Reference
MultiTempRouter.__init__
MultiTempRouter(
model_name: str = "all-MiniLM-L6-v2",
temperature_map: Optional[Dict[str, float]] = None,
references: Optional[List[ReferenceExample]] = None,
classifier: Optional[Callable[[str], str]] = None,
default_temperature: float = 0.7,
override_temperature: Optional[float] = None,
debug: bool = False,
cache_size: int = 128,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
model_name |
str |
"all-MiniLM-L6-v2" |
Sentence-transformers model to use for embeddings |
temperature_map |
Dict[str, float] |
None |
Override default category-to-temperature mapping |
references |
List[ReferenceExample] |
None |
Custom labeled reference examples for classification |
classifier |
Callable[[str], str] |
None |
Custom classifier callable replacing embeddings logic |
default_temperature |
float |
0.7 |
Fallback temperature for unknown categories and __float__ |
override_temperature |
float |
None |
If set, bypasses classification and always returns this value |
debug |
bool |
False |
If True, stores debug info on each call |
cache_size |
int |
128 |
Maximum size of the LRU query cache |
__call__(messages)
router(messages: List[Message]) -> float
Classifies the query in messages and returns the optimal temperature. This is the primary entry point for SDKs that accept callable temperature (e.g., LiteLLM, OpenAI).
for_messages(messages)
router.for_messages(messages: List[Message]) -> float
Explicit pre-computation. Returns a plain float that any SDK will accept. This is the universal fallback for SDKs that do not support callable temperature.
aroute(messages)
await router.aroute(messages: List[Message]) -> float
Async route using asyncio.to_thread(). Does not block the event loop under concurrent load. Use this for async SDK patterns.
__float__()
float(router) -> float
Returns default_temperature. Used as a last-resort fallback when an SDK only accepts numeric values and does not support callables.
classify(query_text)
router.classify(query_text: str) -> str
Classifies raw query text into an intent category (e.g., "factual", "creative", "coding"). Uses the embedding-based classifier if available, otherwise falls back to a keyword heuristic.
get_debug_info()
router.get_debug_info() -> Optional[DebugInfo]
Returns a DebugInfo dataclass with the last classification result if debug=True was passed to the constructor. Returns None otherwise.
DebugInfo fields:
query_text: str— The extracted user querydetected_category: str— The classified intent categorycategory_scores: Dict[str, float]— Average cosine similarity per categorychosen_temperature: float— The temperature that was returnedoverride_active: bool— Whether an override was active
set_override(temperature)
router.set_override(temperature: Optional[float]) -> None
Sets or clears the temperature override. When set, the router bypasses classification and always returns the given temperature. Pass None to clear.
Advanced Customization
Override Temperature Map
from multi_temp_lm import MultiTempRouter
router = MultiTempRouter(
temperature_map={
"factual": 0.1,
"creative": 1.0,
"coding": 0.2,
}
)
Add Custom Categories and References
from multi_temp_lm import MultiTempRouter, ReferenceExample
router = MultiTempRouter(
references=[
ReferenceExample("What is the capital of France?", "factual"),
ReferenceExample("Write a haiku about rain.", "creative"),
ReferenceExample("Refactor this Java class.", "coding"),
ReferenceExample("My custom domain query.", "custom_category"),
],
temperature_map={
"custom_category": 0.5,
},
)
Use a Custom Classifier
def my_classifier(query: str) -> str:
if "def " in query or "class " in query:
return "coding"
return "default"
router = MultiTempRouter(classifier=my_classifier)
When a custom classifier is provided, the embedding-based classifier is completely bypassed.
Adjust Cache Size
router = MultiTempRouter(cache_size=256)
Set cache_size=0 to disable caching entirely.
Override Mode
Force a fixed temperature regardless of classification:
router = MultiTempRouter()
router.set_override(0.5)
# Always returns 0.5 until override is cleared
router.set_override(None)
Debug Mode
Enable debug mode to inspect classification decisions:
router = MultiTempRouter(debug=True)
temperature = router.for_messages([{"role": "user", "content": "Write a poem about the ocean."}])
debug = router.get_debug_info()
print(debug.detected_category) # "creative"
print(debug.chosen_temperature) # 0.9
print(debug.category_scores) # {"factual": 0.12, "creative": 0.89, ...}
Performance
Classification latency depends on hardware and the embedding backend:
- ONNX runtime (default): typically 5–30 ms per query on a modern CPU
- PyTorch fallback: typically 20–80 ms per query on a modern CPU
- Cache hit: sub-millisecond for repeated queries
The first call downloads and caches the all-MiniLM-L6-v2 model (~22 MB with ONNX). Subsequent calls use the cached model.
Architecture Overview
User Code
|
| temperature=router
v
Any LLM SDK (LiteLLM / OpenAI / Anthropic / Google / Cohere / Ollama / ...)
|
| router(messages)
v
MultiTempRouter
|
+-- MessageExtractor → query_text
|
+-- QueryCache (LRU) → cache hit? return float
|
+-- EmbeddingClassifier → embedding + cosine similarity
| +-- ReferenceStore (pre-computed reference vectors)
|
+-- TemperatureMapper → category → float
|
v
float temperature
- Message Extractor pulls the user query text from the message list.
- Query Cache checks for an exact string match; if found, returns instantly.
- Embedding Classifier computes a local embedding for the query and compares it against pre-computed reference embeddings via cosine similarity.
- Temperature Mapper maps the best-matching category to a temperature value.
- The result is cached and returned to the SDK.
All classification is performed locally. No external API calls are made for routing.
Contributing
Contributions are welcome! Please open an issue or pull request on GitHub.
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Install dev dependencies:
pip install -e ".[dev]" - Run tests:
pytest - Run linting:
ruff check src testsandmypy src - Submit a pull request
License
This project is licensed under the MIT License. See LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file multi_temp_lm-0.1.0.tar.gz.
File metadata
- Download URL: multi_temp_lm-0.1.0.tar.gz
- Upload date:
- Size: 24.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac73309e4e4404c9c19ea80908ab63dd49dc41c8584ab232ce01636457c90e13
|
|
| MD5 |
23bfb8d28e9b33b5a67b0fe39ebfe968
|
|
| BLAKE2b-256 |
fe77a7c7a945774fec6bda3e47762b810bafcd48b8dcfa6da83d4aa8acf369ab
|
File details
Details for the file multi_temp_lm-0.1.0-py3-none-any.whl.
File metadata
- Download URL: multi_temp_lm-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ef5856e1dbe892ee29bfcb62df759051df5316419fb2fca220b879b82483a52
|
|
| MD5 |
86c85ead767887d72126c0d3b63660b7
|
|
| BLAKE2b-256 |
afaae1a60aba9971d75d23abd99f10ef2cbb2663a505c1c52de1b82e5a634ef8
|