Semantic Routing Plugins#
Two routing plugins are available for heuristic-based model selection in the LLM‑Router system.
Both activate when payload["model"] == "auto".
Table of Contents#
1. Simple Semantic Routing (Heuristic)#
The Simple Semantic Routing plugin (simple_semantic_routing) performs
two-stage heuristic model selection: it classifies the user's intent
(code, math, creative, general) via weighted keywords, multi-word phrases, and
regex patterns, then estimates input complexity (token count) to pick the most
appropriate model from a configured pool.
No embedding model is required — routing is a fast, pure-text classification.
1.1 Architecture#
Data Flow#
User Input
│
▼
classify_intent(text) ← keywords + phrases + regex patterns
│
▼
estimate_tokens(text) ← word_count × 1.25
│
▼
complexity_level(tokens) ← simple / medium / complex
│
▼
select_model(intent, complexity) ← weighted score → model index
│
▼
Updated payload["model"]
Intent Classification Algorithm#
Each intent is defined in simple_semantic.json with four complementary signal types:
a) Keywords — single words#
Each keyword has an optional weight. If no weight is specified, the default is 1.0.
"keywords": ["code", "debug", "funkcja"],
"weights": {"debug": 5, "kod": 4, "code": 2}
Matching "debug" adds 5 to the score; matching "code" adds 2.
b) Phrases — multi-word expressions#
Phrases use the format "text:weight". The default weight is 2.0 when omitted.
"phrases": ["write code:5", "fix bug:4", "napisz funkcję:4"]
Matching "write code" adds 5 to the score.
c) Regex Patterns — structural detection#
Patterns add a flat +3.0 per match, useful for detecting code structures, math formulas, and question patterns.
"patterns": [
"def\\s+\\w+\\s*\\(",
"class\\s+\\w+\\s*[(:]",
"\\b\\d+\\s*[+\\-*/^]\\s*\\d+\\b",
"\\bwhat\\s+(is|are|does)\\b"
]
d) Weights — keyword importance#
The weights object maps individual keywords to boost factors, allowing fine-grained control:
"weights": {
"debug": 5, // Very strong signal for code intent
"błąd": 5, // Very strong signal for code intent
"kod": 2, // Moderate signal
"python": 2 // Moderate signal
}
1.2 Intent Categories#
| Intent | Description | Examples |
|---|---|---|
code |
Programming, debugging, implementation | "napisz funkcję", "fix bug", "debug code", "git commit" |
math |
Mathematics, calculations, statistics | "oblicz prawdopodobieństwo", "solve equation", "calculate mean" |
creative |
Creative writing, editing, brainstorming | "napisz opowiadanie", "write a story", "brainstorm" |
general |
General questions, explanations, comparisons | "wyjaśnij jak", "what is the difference", "compare" |
none |
No specific intent (greetings, etc.) | "cześć", "hello", "dziękuję" |
1.3 Model Selection Algorithm#
1. complexity_map = {"simple": 0, "medium": n//2, "complex": n-1}
2. idx = complexity_map[complexity]
3. if intent has a target in intent_adjustment:
intent_idx = model_index_for(intent.target)
idx = max(idx, intent_idx) // boost toward intent's target
else:
idx -= 1 // demote (no intent)
4. idx = clamp(idx, 0, n-1)
5. return models[idx]
Boost / Demote Examples:
| Intent | Complexity | Base Index | Intent Adjustment | Result |
|---|---|---|---|---|
code → medium |
simple (idx=0) | 0 | intent targets index 1 | max(0, 1) = 1 |
none |
complex (idx=2) | 2 | no target → demote | 2-1 = 1 |
math → medium |
complex (idx=3) | 3 | intent targets index 1 | max(3, 1) = 3 |
creative → simple |
medium (idx=1) | 1 | intent targets index 0 | max(1, 0) = 1 |
1.4 Configuration#
JSON Config (simple_semantic.json)#
All configuration lives in
llm_router_plugins/resources/routing/simple_semantic.json.
{
"settings": {
"len_thresholds_max": {
"simple": 25,
"medium": 150
},
"default_models": {
"simple": "gpt-oss:120b",
"medium": "qwen3.6:35b"
},
"intent_adjustment": {
"code": "medium",
"math": "medium",
"creative": "simple",
"general": "simple",
"none": ""
}
},
"intents": {
"code": {
"keywords": [
"code",
"debug",
"funkcja"
],
"phrases": [
"write code:5",
"fix bug:4"
],
"patterns": [
"def\\s+\\w+\\s*\\(",
"class\\s+\\w+"
],
"weights": {
"debug": 5,
"błąd": 4
}
},
"math": {
"keywords": [
"calculate",
"equation"
],
"phrases": [
"calculate:4",
"solve equation:4"
],
"patterns": [
"\\b\\d+\\s*[+\\-*/^]\\s*\\d+\\b"
],
"weights": {
"calculate": 4,
"solve": 4
}
},
"creative": {
"keywords": [
"write",
"story",
"napisz"
],
"phrases": [
"write a story:4",
"napisz wiersz:4"
],
"patterns": [
"napisz\\s+(mi|ci|go|ją)"
],
"weights": {
"napisz": 4,
"write": 3
}
},
"general": {
"keywords": [
"explain",
"difference",
"wyjaśnij"
],
"phrases": [
"what is:4",
"how to:4"
],
"patterns": [
"\\bwhat\\s+(is|are|does)\\b"
],
"weights": {
"wyjaśnij": 3,
"help": 2
}
}
},
"none": {
"keywords": [
"hello",
"cześć",
"thanks"
],
"phrases": [
"hello:1",
"thanks:1"
],
"patterns": [
"^\\b(hello|hi|cześć)\\b"
],
"weights": {}
}
}
Configurable Values#
| Setting | Description | Default |
|---|---|---|
len_thresholds_max.simple |
Max tokens for "simple" complexity | 25 |
len_thresholds_max.medium |
Max tokens for "medium" complexity | 150 |
default_models.simple |
Model for simple complexity | gpt-oss:120b |
default_models.medium |
Model for medium complexity | qwen3.6:35b |
intent_adjustment.code |
Target complexity for code intent | medium |
intent_adjustment.math |
Target complexity for math intent | medium |
intent_adjustment.creative |
Target complexity for creative intent | simple |
intent_adjustment.general |
Target complexity for general intent | simple |
1.5 Environment Variable Overrides#
All environment variables are prefixed with LLM_ROUTER_ROUTING_.
| Variable | Format | Description |
|---|---|---|
LLM_ROUTER_ROUTING_MODELS |
model-a\|model-b\|model-c |
Comma-separated model pool (pipe-delimited) |
LLM_ROUTER_ROUTING_COMPLEXITY_THRESHOLDS |
simple_threshold\|medium_threshold |
Token count thresholds (e.g. 10\|50) |
LLM_ROUTER_ROUTING_DEFAULT_MODEL |
model-name |
Fallback model when no text content is found |
LLM_ROUTER_ROUTING_INTENT_<CATEGORY> |
kw1\|kw2:5\|phrase:3 |
Override intent keywords/phrases for a category |
Example:
export LLM_ROUTER_ROUTING_MODELS="tiny-model\|medium-model\|large-model"
export LLM_ROUTER_ROUTING_COMPLEXITY_THRESHOLDS="10\|50"
export LLM_ROUTER_ROUTING_INTENT_CODE="code\|debug\|implement"
export LLM_ROUTER_ROUTING_DEFAULT_MODEL="fallback-model"
1.6 Usage Examples#
Basic Usage#
from llm_router_plugins.utils.routing.simple_semantic.simple_semantic_routing import SimpleSemanticRoutingPlugin
plugin = SimpleSemanticRoutingPlugin()
payload = {
"model": "auto",
"messages": [{"role": "user", "content": "Napisz funkcję w Pythonie"}]
}
result = plugin.apply(payload)
# result["model"] → selected model (e.g., "qwen3.6:35b")
Payload Text Sources#
The plugin extracts text from the payload in this priority order:
payload["messages"][-1]["content"]— last message contentpayload["user_last_statement"]payload["query"]payload["prompt"]payload["input"]
Logging#
When a logger is provided, the plugin logs routing decisions:
import logging
logger = logging.getLogger("llm_router")
plugin = SimpleSemanticRoutingPlugin(logger=logger)
# On apply:
# INFO: Semantic routing: intent=code, complexity=medium (12 tokens) -> qwen3.6:35b
1.7 Weight Tuning Guidelines#
| Weight Range | Meaning | Use Case |
|---|---|---|
| 1 | Weak / baseline | Generic terms that appear in many contexts |
| 2–3 | Moderate | Common but not definitive signals |
| 4–5 | Strong | Highly specific, unambiguous signals (e.g., "debug", "błąd" for code) |
| 1 (none) | Neutral | Greetings and filler phrases that shouldn't influence routing |
1.8 Phrase Format Guidelines#
| Format | Weight | Example |
|---|---|---|
"text:weight" |
Explicit | "write code:5" |
"text" |
Default (2.0) | "fix bug" |
1.9 Complexity Levels#
| Level | Token Range | Model Selection |
|---|---|---|
simple |
0 – thresholds[simple] |
Cheapest / simplest model (index 0) |
medium |
thresholds[simple] + 1 – thresholds[medium] |
Middle model (index n // 2) |
complex |
> thresholds[medium] |
Strongest model (index n - 1) |
Token estimation: round(word_count × 1.25)
1.10 Built-in Patterns Reference#
Code Patterns#
| Pattern | Matches |
|---|---|
def\s+\w+\s*\( |
Python function definitions |
class\s+\w+\s*[(:] |
Python/class definitions |
import\s+\w+ |
Import statements |
try\s*:, except\s*: |
Try/except blocks |
git\s+(commit\|push\|pull\|branch\|merge) |
Git commands |
npm\s+(install\|run\|start\|build) |
NPM commands |
docker\s+(build\|run\|compose) |
Docker commands |
SELECT\s+.*\s+FROM |
SQL SELECT queries |
Math Patterns#
| Pattern | Matches |
|---|---|
\b\d+\s*[+\-*/^]\s*\d+\b |
Arithmetic expressions |
\bsin\s*\(, \bcos\s*\(, \btan\s*\( |
Trig functions |
\bsqrt\s*\(, \blog\s*\( |
Math functions |
\bf\(x\)\s*= |
Function notation |
\b∑\b, \b∫\b |
Sigma / integral symbols |
General Question Patterns#
| Pattern | Matches |
|---|---|
\bwhat\s+(is\|are\|does)\b |
What questions |
\bhow\s+(to\|does\|do)\b |
How questions |
\bwhy\s+(is\|does)\b |
Why questions |
\bporównaj\s+ |
Polish comparison questions |
1.11 Adding New Intents#
To add a new intent category:
- Add the intent to
simple_semantic.json:
{
"intents": {
"my_intent": {
"keywords": [
"keyword1",
"keyword2"
],
"phrases": [
"phrase one:4",
"phrase two:3"
],
"patterns": [
"pattern\\s+here"
],
"weights": {
"keyword1": 5,
"keyword2": 2
}
}
},
"settings": {
"intent_adjustment": {
"my_intent": "medium"
}
}
}
- Or override via environment variable:
export LLM_ROUTER_ROUTING_INTENT_MY_INTENT="keyword1\|keyword2:5\|phrase:4"
1.12 Running Tests#
pytest tests/test_simple_semantic_routing.py -v
2. Bi-Encoder Semantic Routing (Embedding-based)#
The Bi-Encoder routing plugin (semantic_biencoder_routing) uses a neural embedding model
(google/embeddinggemma-300m) to compute semantic embeddings for a set of pre-configured routing targets.
Each target has a name, a model_name (the model to route to), a description, and a list of examples.
At query time the user message is embedded and matched against all stored target embeddings using FAISS
(IndexFlatIP on L2-normalised vectors = cosine similarity). The best-matching target determines the selected model.
2.1 Index Building (on first load or when the persist directory is missing)#
- For each target, its
descriptionandexamplesare combined into text. - The text is split into overlapping token chunks using a sliding window (
chunk_sizetokens,chunk_overlaptokens overlap). - Each chunk is embedded via the BiEncoder model (e.g.
google/embeddinggemma-300m). - All embedding vectors are L2-normalised to unit length.
- Vectors are inserted into a
faiss.IndexFlatIPindex (inner product). - A docstore maps each FAISS doc ID to its target name (for reverse lookup).
2.2 Routing (query)#
- The user message is embedded and L2-normalised.
- FAISS performs a nearest-neighbor search returning the
top_kclosest chunks. - Scores are aggregated per target: the mean cosine similarity of all chunks belonging to the same target is computed.
- The target with the highest mean similarity wins and its
model_nameis returned.
2.3 Persistence#
The FAISS index and docstore are saved to disk (files index.faiss and docstore.pkl) under the configured persist
directory. On subsequent starts the index is loaded from disk — embeddings are not recomputed.
If the embedding model changes (different output dimension) the index is automatically rebuilt.
2.4 Configuration#
Configuration is loaded from
llm_router_plugins/resources/routing/semantic_biencoder.json.
Example JSON configuration:
{
"embedding_model": "google/embeddinggemma-300m",
"settings": {
"chunk_size": 256,
"chunk_overlap": 64,
"similarity_threshold": 0.0,
"top_k": 1,
"vector_store_path": ""
},
"routing_targets": [
{
"name": "code-generation",
"model_name": "qwen3.6:35b",
"description": "Model specialized for code-related tasks.",
"examples": [
"Write a Python function...",
"..."
]
},
{
"name": "math-reasoning",
"model_name": "gpt-oss:120b",
"description": "Model for mathematical reasoning and calculations.",
"examples": [
"Calculate the derivative of...",
"..."
]
}
]
}
Environment Variables#
| Variable | Purpose |
|---|---|
LLM_ROUTER_ROUTING_SEMANTIC_BIENCODER_MODEL |
Override the embedding model name |
LLM_ROUTER_ROUTING_SEMANTIC_BIENCODER_TARGETS |
Pipe-separated list of target names |
LLM_ROUTER_ROUTING_SEMANTIC_BIENCODER_CHUNK_SIZE |
Override chunk size |
LLM_ROUTER_ROUTING_SEMANTIC_BIENCODER_CHUNK_OVERLAP |
Override chunk overlap |
LLM_ROUTER_ROUTING_SEMANTIC_BIENCODER_PERSIST_DIR |
Directory for FAISS index persistence |
2.5 Usage Examples#
Basic Usage#
from llm_router_plugins.utils.routing.semantic_biencoder.semantic_biencoder_routing import
SemanticBiEncoderRoutingPlugin
plugin = SemanticBiEncoderRoutingPlugin()
payload = {
"model": "auto",
"messages": [{"role": "user", "content": "Napisz funkcję w Pythonie"}]
}
result = plugin.apply(payload)
# result["model"] → selected model (e.g., "qwen3.6:35b")
Target Examples with Embeddings#
Each routing target has description + examples. The plugin embeds both and computes the mean vector as the
target's embedding. When a user query arrives:
- Query text is embedded → vector Q (L2-normalised).
- FAISS searches for closest vectors to Q among all target embeddings.
- Best match → that target's
model_nameis selected.
Example targets:
| Target Name | Model | Description | Examples |
|---|---|---|---|
| code-generation | qwen3.6:35b | Write and debug code | "Write a Python function to sort a list", "Fix my CSS layout" |
| math-reasoning | gpt-oss:120b | Mathematical problems and calculations | "Calculate the integral of x²", "Solve for x in 2x+3=7" |
| creative-writing | gpt-oss:120b | Creative writing and storytelling | "Write a short story about...", "Create a poem about..." |
| general-knowledge | gpt-oss:120b | General questions and explanations | "What is the capital of France?", "How does DNA work?" |
2.6 Scoring Details#
For each user query, FAISS returns top_k closest chunks with similarity scores. These scores are aggregated per
target (mean of all matching chunk scores). The final score table looks like:
| Target | Mean Cosine Similarity | Selected? |
|---|---|---|
| code-generation | 0.85 | ✅ Yes |
| math-reasoning | 0.42 | |
| creative-writing | 0.31 | |
| general-knowledge | 0.28 |
The target with the highest mean similarity wins. If all scores fall below similarity_threshold (default 0.0),
the default model is used.
2.7 Running Tests#
pytest tests/test_semantic_biencoder_routing.py -v
3. Comparison: Which Plugin to Use?#
| Feature | Simple Semantic Routing | Bi-Encoder Semantic Routing |
|---|---|---|
| Approach | Heuristic (keyword/phrase) | Neural embeddings (FAISS) |
| Model Required | ❌ None | ✅ google/embeddinggemma-300m |
| Speed | Very fast (~0.1ms) | Slower (~50-200ms, model dependent) |
| Accuracy | Rule-based, limited context | Semantic understanding of meaning |
| Config Complexity | JSON keywords/phrases/patterns | JSON targets + examples |
| Scalability | Linear keyword search | FAISS index (efficient at scale) |
| Persistence | N/A | ✅ FAISS index saved to disk |
| Use Case | Fast, lightweight routing | High-quality semantic matching |
Recommendation#
-
Use Simple Semantic Routing when:
- You need fast, deterministic routing with no external model dependency.
- Your routing categories are well-defined by keywords and phrases.
- You want minimal infrastructure.
-
Use Bi-Encoder Semantic Routing when:
- You need semantic understanding beyond keywords (e.g., synonyms, paraphrasing).
- You have diverse, nuanced use cases that keyword matching can't capture.
- You can afford the embedding model latency and dependencies.
4. File Locations#
| File | Purpose |
|---|---|
llm_router_plugins/utils/routing/simple_semantic/ |
SimpleSemanticRoutingPlugin code |
llm_router_plugins/utils/routing/semantic_biencoder/ |
SemanticBiEncoderRoutingPlugin code |
llm_router_plugins/resources/routing/simple_semantic.json |
Intent definitions & config |
llm_router_plugins/resources/routing/semantic_biencoder.json |
Embedding routing config |
tests/test_simple_semantic_routing.py |
Unit tests (Simple) |
tests/test_semantic_biencoder_routing.py |
Unit tests (Bi-Encoder) |