Skip to content

Commit 78c7042

Browse files
committed
Add multi-tier model routing for cost optimization in RLM
- Introduced multi-tier routing to allow the use of a powerful root model for reasoning and cost-effective sub-models for execution tasks. - Updated documentation to explain the new feature, including examples and cost comparisons. - Added a new example script demonstrating the multi-tier routing functionality.
1 parent 57740f2 commit 78c7042

4 files changed

Lines changed: 336 additions & 7 deletions

File tree

‎ARCHITECTURE.md‎

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,22 +179,50 @@ RLM
179179
**Configuration:**
180180
```python
181181
RLM(
182-
backend="openai", # LM client to use
182+
backend="openai", # LM client to use (root model at depth=0)
183183
backend_kwargs={"model_name": "..."},
184184
environment="local", # REPL type
185185
environment_kwargs={},
186186
depth=0, # Current depth (0=root)
187187
max_depth=1, # Recursion limit
188188
max_iterations=30, # Iteration limit
189189
custom_system_prompt=None, # Override default prompt
190-
other_backends=["openai"], # Additional clients for sub-calls
191-
other_backend_kwargs=[{...}],
190+
other_backends=["openai"], # Sub-model clients for depth-based routing
191+
other_backend_kwargs=[{...}], # Config for sub-models (currently supports 1)
192192
logger=None, # RLMLogger instance
193193
verbose=False, # Pretty console output
194194
persistent=False # Multi-turn conversations
195195
)
196196
```
197197

198+
**Multi-Tier Routing (`other_backends`):**
199+
200+
The `other_backends` parameter enables cost optimization by routing sub-queries (made via `llm_query()`) to different, typically cheaper models:
201+
202+
- **Root model (depth=0)**: Uses `backend` + `backend_kwargs` for main reasoning
203+
- **Sub-model (depth=1)**: Uses `other_backends[0]` + `other_backend_kwargs[0]` for `llm_query()` calls
204+
205+
**Example - Cost-Optimized Configuration:**
206+
```python
207+
rlm = RLM(
208+
# Powerful model for strategic planning
209+
backend="anthropic",
210+
backend_kwargs={"model_name": "claude-3-5-sonnet-20241022"},
211+
212+
# Cost-effective model for execution tasks
213+
other_backends=["anthropic"],
214+
other_backend_kwargs=[{"model_name": "claude-3-haiku-20240307"}],
215+
)
216+
# Sonnet plans, Haiku executes → 10x cost reduction on sub-queries!
217+
```
218+
219+
**When sub-model is used:**
220+
- When generated code calls `llm_query(prompt)` at depth=0
221+
- These calls are routed to depth=1 (the sub-model)
222+
- Useful for: batch processing, extraction, validation, simple transformations
223+
224+
**Note:** Currently limited to 1 sub-model. Future versions will support multiple tiers.
225+
198226
---
199227

200228
### LM Handler (`rlm/core/lm_handler.py`)
@@ -239,15 +267,61 @@ LMHandler
239267
}
240268
```
241269

242-
**Client Selection Logic:**
243-
1. If `model` specified → use registered client for that model
244-
2. If `depth == 1` and `other_backend_client` exists → use other backend
245-
3. Else → use default client
270+
**Client Selection Logic (Multi-Tier Routing):**
271+
272+
The handler routes requests to different LM clients based on explicit model name or depth:
273+
274+
1. **Explicit model name** → use registered client for that model (highest priority)
275+
2. **Depth-based routing** → if `depth == 1` and `other_backend_client` exists → use sub-model
276+
3. **Fallback** → use default (root) client
277+
278+
**Example Routing Flow:**
279+
```
280+
┌─────────────────────────────────────────────────────────┐
281+
│ Root RLM (depth=0) │
282+
│ Model: Claude Sonnet ($3/M in, $15/M out) │
283+
│ │
284+
│ Generates code: │
285+
│ results = [] │
286+
│ for item in data: │
287+
│ # This llm_query() triggers depth=1 routing │
288+
│ summary = llm_query(f"Summarize: {item}") │
289+
│ results.append(summary) │
290+
└────────────────────┬────────────────────────────────────┘
291+
│ llm_query() at depth=0
292+
▼
293+
┌─────────────────────────────────────────────────────────┐
294+
│ LM Handler │
295+
│ Receives: {"prompt": "Summarize: ...", "depth": 1} │
296+
│ │
297+
│ Routing decision: │
298+
│ • depth=1 detected │
299+
│ • other_backend_client exists │
300+
│ → Route to sub-model (Claude Haiku) │
301+
└────────────────────┬────────────────────────────────────┘
302+
│
303+
▼
304+
┌─────────────────────────────────────────────────────────┐
305+
│ Sub-Model (depth=1) │
306+
│ Model: Claude Haiku ($0.25/M in, $1.25/M out) │
307+
│ │
308+
│ Processes simple extraction/summarization │
309+
│ Returns result to root RLM │
310+
│ │
311+
│ Cost: ~90% cheaper for sub-queries! │
312+
└─────────────────────────────────────────────────────────┘
313+
```
314+
315+
**Cost Optimization Strategy:**
316+
- Root model: Use powerful model (Sonnet, GPT-4o) for planning/reasoning
317+
- Sub-model: Use cost-effective model (Haiku, GPT-4o-mini) for execution
318+
- Savings: 10-20x reduction on sub-query costs
246319

247320
**Key Methods:**
248321
- `start()` - Spawn server thread
249322
- `stop()` - Shutdown server
250323
- `register_client(model_name, client)` - Add client to registry
324+
- `get_client(model, depth)` - Route requests to appropriate client
251325
- `get_client(model, depth)` - Resolve which client to use
252326
- `completion(prompt)` - Direct completion call (used by RLM for root queries)
253327
- `get_usage_summary()` - Aggregate usage across all clients

‎README.md‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,73 @@ rlm = RLM(
155155
)
156156
```
157157

158+
## Multi-Tier Model Routing for Cost Optimization
159+
160+
RLM supports **multi-tier routing** where you can configure a powerful root model for high-level reasoning and delegate simpler execution tasks to more cost-effective sub-models. This can reduce costs by **10-20x** for tasks involving many sub-queries.
161+
162+
### How It Works
163+
164+
When the root model generates code containing `llm_query()` calls, those sub-queries are automatically routed to your configured sub-model (at depth=1):
165+
166+
```python
167+
from rlm import RLM
168+
169+
rlm = RLM(
170+
# Root model (depth=0): Handles main reasoning and planning
171+
backend="anthropic",
172+
backend_kwargs={"model_name": "claude-3-5-sonnet-20241022"},
173+
174+
# Sub-model (depth=1): Handles llm_query() calls
175+
other_backends=["anthropic"],
176+
other_backend_kwargs=[{"model_name": "claude-3-haiku-20240307"}],
177+
)
178+
179+
# The root model (Sonnet) will plan the approach
180+
# Any llm_query() calls will automatically use the sub-model (Haiku)
181+
result = rlm.completion("""
182+
Analyze this dataset by processing each item:
183+
1. Extract key fields from each record
184+
2. Summarize findings
185+
""")
186+
```
187+
188+
### Cost Comparison: Sonnet vs Haiku
189+
190+
| Model | Input Cost | Output Cost | Use Case |
191+
|-------|-----------|------------|----------|
192+
| Claude 3.5 Sonnet | $3/M tokens | $15/M tokens | Complex reasoning, planning |
193+
| Claude 3 Haiku | $0.25/M tokens | $1.25/M tokens | Simple extraction, formatting |
194+
195+
**Savings Example:**
196+
- Root model makes 1 planning call (Sonnet): ~10K tokens = $0.15
197+
- Sub-model makes 100 extraction calls (Haiku): ~500K tokens = $0.75
198+
- **Total: $0.90** vs **$9.00 all-Sonnet** (90% savings!)
199+
200+
### Recommended Model Pairs by Provider
201+
202+
| Provider | Root Model | Sub-Model | Cost Ratio |
203+
|----------|-----------|-----------|-----------|
204+
| **Anthropic** | `claude-3-5-sonnet-20241022` | `claude-3-haiku-20240307` | 10x cheaper |
205+
| **OpenAI** | `gpt-4o` | `gpt-4o-mini` | 20x cheaper |
206+
| **Bedrock** | `us.anthropic.claude-sonnet-4-*` | `anthropic.claude-3-5-haiku-*` | 10x cheaper + volume discounts |
207+
| **Gemini** | `gemini-2.0-pro` | `gemini-2.5-flash` | 5-10x cheaper |
208+
| **Azure OpenAI** | `gpt-4o` | `gpt-4o-mini` | 20x cheaper |
209+
210+
### When to Use Multi-Tier Routing
211+
212+
✅ **Good use cases:**
213+
- Processing large datasets with many sub-queries
214+
- Extract-transform-load (ETL) operations
215+
- Batch processing with simple per-item operations
216+
- Analysis requiring many lookups/validations
217+
218+
❌ **Not needed for:**
219+
- Single-shot completions without sub-queries
220+
- Tasks where every step requires advanced reasoning
221+
- Small-scale processing (<10 sub-queries)
222+
223+
See [examples/multi_tier_anthropic.py](examples/multi_tier_anthropic.py) for a complete working example.
224+
158225
## Relevant Reading
159226
* **[Dec '25]** [Recursive Language Models arXiv](https://arxiv.org/abs/2512.24601)
160227
* **[Oct '25]** [Recursive Language Models Blogpost](https://alexzhang13.github.io/blog/2025/rlm/)

‎docs/getting-started.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,59 @@ When your session hits the token limit:
116116

117117
---
118118

119+
## Multi-Tier Routing for Cost Optimization
120+
121+
{: .highlight }
122+
> **💰 Reduce costs by 10-20x** by using cheaper models for sub-queries while keeping powerful models for planning.
123+
124+
RLM supports **multi-tier routing** where you configure different models at different depths:
125+
- **Root model (depth=0)**: Handles main reasoning and planning
126+
- **Sub-model (depth=1)**: Handles `llm_query()` calls automatically
127+
128+
### Quick Example
129+
130+
```python
131+
from rlm import RLM
132+
133+
rlm = RLM(
134+
# Root model: Powerful reasoning
135+
backend="anthropic",
136+
backend_kwargs={"model_name": "claude-3-5-sonnet-20241022"},
137+
138+
# Sub-model: Cost-effective execution
139+
other_backends=["anthropic"],
140+
other_backend_kwargs=[{"model_name": "claude-3-haiku-20240307"}],
141+
)
142+
143+
# The root model (Sonnet) plans the approach
144+
# Any llm_query() calls automatically use the sub-model (Haiku)
145+
result = rlm.completion("Process 100 customer records and extract key fields")
146+
```
147+
148+
### Cost Breakdown
149+
150+
| Scenario | Root Model | Sub-Queries | Total Cost | Savings |
151+
|:---------|:-----------|:------------|:-----------|:--------|
152+
| **Single-tier** (all Sonnet) | Sonnet | Sonnet (100×) | ~$9.00 | - |
153+
| **Multi-tier** (Sonnet + Haiku) | Sonnet | Haiku (100×) | ~$0.90 | **90%** |
154+
155+
### When to Use Multi-Tier
156+
157+
✅ **Use multi-tier when:**
158+
- Processing many records with sub-queries
159+
- Extracting/transforming data in batches
160+
- Simple operations repeated many times
161+
- Budget is a concern
162+
163+
❌ **Single-tier is fine for:**
164+
- One-shot completions
165+
- Every step needs advanced reasoning
166+
- Fewer than 10 sub-queries
167+
168+
See the full guide in [README: Multi-Tier Model Routing](../README.md#multi-tier-model-routing-for-cost-optimization) and [examples/multi_tier_anthropic.py](../examples/multi_tier_anthropic.py).
169+
170+
---
171+
119172
## Your First RLM Call
120173

121174
### Step 1: Set Up API Keys

‎examples/multi_tier_example.py‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""
2+
Multi-Tier Model Routing Example: Claude Sonnet (root) + Haiku (sub)
3+
4+
This example demonstrates how to use multi-tier routing to optimize costs by:
5+
1. Using Claude Sonnet for high-level reasoning and planning (root model)
6+
2. Delegating simple execution tasks to Claude Haiku (sub-model) via llm_query()
7+
8+
Cost Comparison:
9+
- Claude 3.5 Sonnet: $3/M input + $15/M output tokens
10+
- Claude 3 Haiku: $0.25/M input + $1.25/M output tokens
11+
- Savings: ~10x cheaper for sub-queries
12+
13+
Without multi-tier: All calls use Sonnet → ~$9 for this example
14+
With multi-tier: Root uses Sonnet, 10 subs use Haiku → ~$1.50 (83% savings!)
15+
"""
16+
17+
from dotenv import load_dotenv
18+
19+
from rlm import RLM
20+
from rlm.logger import RLMLogger
21+
22+
load_dotenv()
23+
24+
# Setup logger to track which model handles which call
25+
logger = RLMLogger(log_dir="./logs")
26+
27+
# Configure multi-tier routing
28+
rlm = RLM(
29+
# Root model (depth=0): Claude Sonnet for strategic planning
30+
backend="bedrock",
31+
backend_kwargs={
32+
"model_name": "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
33+
"region_name": "us-east-1",
34+
"max_tokens": 8192,
35+
},
36+
# Sub-model (depth=1): Claude Haiku for simple execution tasks
37+
# Any llm_query() calls in generated code will automatically use Haiku
38+
other_backends=["bedrock"],
39+
other_backend_kwargs=[
40+
{
41+
"model_name": "us.anthropic.claude-3-5-haiku-20241022-v1:0",
42+
"region_name": "us-east-1",
43+
"max_tokens": 4096,
44+
}
45+
],
46+
environment="local",
47+
logger=logger,
48+
verbose=True,
49+
)
50+
51+
# Example task: Process a list of items
52+
# The root model (Sonnet) will strategize how to process the data
53+
# The sub-model (Haiku) will handle individual item processing
54+
prompt = """
55+
Given this list of employee records, extract the key information from each:
56+
57+
Records:
58+
1. John Doe, Software Engineer, joined 2020, salary $120k
59+
2. Jane Smith, Product Manager, joined 2019, salary $135k
60+
3. Bob Wilson, Designer, joined 2021, salary $105k
61+
4. Alice Brown, Data Scientist, joined 2022, salary $130k
62+
5. Charlie Davis, DevOps Engineer, joined 2020, salary $125k
63+
64+
For each record:
65+
1. Extract: name, role, join_year, salary
66+
2. Calculate years of experience (2026 - join_year)
67+
3. Classify seniority: <3 years = Junior, 3-5 years = Mid, >5 years = Senior
68+
69+
Use llm_query() to process each record individually for parallel processing.
70+
Create a list to collect all results, then use FINAL() to return the list.
71+
72+
Note: When constructing prompts for llm_query(), avoid using backslashes in f-string expressions.
73+
Instead of f"text{var}\n", use f"text{var}" + "\n" or template strings.
74+
"""
75+
76+
print("=" * 80)
77+
print("MULTI-TIER ROUTING EXAMPLE: Claude Sonnet (root) + Haiku (sub)")
78+
print("=" * 80)
79+
print("\nPrompt:", prompt)
80+
print("\n" + "=" * 80)
81+
82+
result = rlm.completion(prompt)
83+
84+
print("\n" + "=" * 80)
85+
print("RESULT:")
86+
print("=" * 80)
87+
print(result.response)
88+
89+
# Show cost breakdown
90+
print("\n" + "=" * 80)
91+
print("USAGE SUMMARY:")
92+
print("=" * 80)
93+
usage = result.usage_summary.to_dict()
94+
for model_name, stats in usage["model_usage_summaries"].items():
95+
print(f"\nModel: {model_name}")
96+
print(f" Calls: {stats['total_calls']}")
97+
print(f" Input tokens: {stats['total_input_tokens']:,}")
98+
print(f" Output tokens: {stats['total_output_tokens']:,}")
99+
total_tokens = stats["total_input_tokens"] + stats["total_output_tokens"]
100+
print(f" Total tokens: {total_tokens:,}")
101+
102+
# Calculate approximate cost
103+
total_cost = 0
104+
for model_name, stats in usage["model_usage_summaries"].items():
105+
if "sonnet" in model_name.lower():
106+
# Sonnet: $3/M input, $15/M output
107+
cost = (stats["total_input_tokens"] / 1_000_000 * 3) + (
108+
stats["total_output_tokens"] / 1_000_000 * 15
109+
)
110+
print(f"\n Sonnet Cost: ${cost:.4f}")
111+
elif "haiku" in model_name.lower():
112+
# Haiku: $0.25/M input, $1.25/M output
113+
cost = (stats["total_input_tokens"] / 1_000_000 * 0.25) + (
114+
stats["total_output_tokens"] / 1_000_000 * 1.25
115+
)
116+
print(f" Haiku Cost: ${cost:.4f}")
117+
total_cost += cost
118+
119+
print(f"\n{'=' * 80}")
120+
print(f"TOTAL ESTIMATED COST: ${total_cost:.4f}")
121+
print(f"{'=' * 80}")
122+
123+
# Calculate what cost would be with all-Sonnet
124+
all_sonnet_cost = 0
125+
for _model_name, stats in usage["model_usage_summaries"].items():
126+
all_sonnet_cost += (stats["total_input_tokens"] / 1_000_000 * 3) + (
127+
stats["total_output_tokens"] / 1_000_000 * 15
128+
)
129+
130+
savings = all_sonnet_cost - total_cost
131+
savings_pct = (savings / all_sonnet_cost * 100) if all_sonnet_cost > 0 else 0
132+
133+
print(f"\nCost if all-Sonnet: ${all_sonnet_cost:.4f}")
134+
print(f"Savings with multi-tier: ${savings:.4f} ({savings_pct:.1f}% reduction)")
135+
print("\n✓ Multi-tier routing example completed!")

0 commit comments

Comments
 (0)