Integration
Default Model Enforcer
OpenClaw plugin that enforces a model rotation strategy per session β seed on primary, then switch to fallback. Saves API costs without sacrificing quality.
Configuration Example
{
"plugins": {
"allow": ["default-model-enforcer"],
"entries": {
"default-model-enforcer": {
"enabled": true
}
}
}
}
README
<h1 align="center">π Default Model Enforcer</h1>
<p align="center">
<strong>An <a href="https://github.com/openclaw/openclaw">OpenClaw</a> plugin that enforces intelligent model rotation per session.</strong><br/>
Seed on your primary model, then automatically switch to your preferred reasoning model β zero manual intervention.
</p>
<p align="center">
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-007ACC?style=flat&logo=typescript&logoColor=white" />
<img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg" />
<img alt="OpenClaw Plugin" src="https://img.shields.io/badge/OpenClaw-Plugin-blueviolet" />
<img alt="Version" src="https://img.shields.io/badge/version-14.0.0-blue" />
</p>
<p align="center">
<a href="#features">Features</a> β’
<a href="#how-it-works">How It Works</a> β’
<a href="#installation">Installation</a> β’
<a href="#configuration">Configuration</a> β’
<a href="#architecture">Architecture</a> β’
<a href="#api-reference">API Reference</a> β’
<a href="#license">License</a>
</p>
---
## Why?
Running multi-agent systems means dealing with **different model strengths for different tasks.** Some models excel at fast context ingestion and structured bootstrapping, while others shine in complex reasoning and creative output. But manually switching models mid-session? Nobody has time for that.
The Default Model Enforcer (DME) automates this pattern: **use a fast, efficient model for session initialization** (system prompt processing, memory loading, context priming) β then **automatically rotate to your preferred reasoning model** for all subsequent interaction turns.
**Use cases:**
- ποΈ **Latency optimization** β Fast model for bootstrap, powerful model for conversation
- π° **Cost control** β Cheaper model handles the boilerplate first turn, premium model handles the real work
- π§ͺ **A/B model strategies** β Enforce consistent model assignment across sessions without manual intervention
- π€ **Multi-agent fleets** β Different agents can run different rotation strategies via their config
**Result:** Optimal model selection per conversation phase, fully automated, zero manual intervention.
## Features
- π **Automatic Model Rotation** β First turn uses `model.primary`, all subsequent turns use the first `model.fallbacks` entry
- π« **Heartbeat Awareness** β Heartbeat/cron polls always use the primary model and never advance the session phase
- π‘οΈ **Graceful Degradation** β If the primary model is unavailable and the runtime falls back internally, DME stays in `seed_pending` and retries next turn
- β **Manual Override Respected** β `/model` command sets a manual override; DME backs off entirely for that session
- π§Ή **Auto-Cleanup** β Stale session states (>24h) are pruned automatically
- π **Cross-Context State** β File-backed state (`/tmp/dme-state.json`) shared between gateway and plugin execution contexts
- β‘ **Atomic Writes** β State persistence uses write-to-temp + rename to prevent corruption
- π **Idempotent Registration** β Safe across hot-reloads and multi-context environments
## How It Works
```
Session Start
β
βΌ
βββββββββββββββββββ
β seed_pending β β First normal turn β force PRIMARY model
β β
β llm_output β β Verify: did primary actually run?
β confirms seed? β
ββββββββββ¬βββββββββ
β yes
βΌ
βββββββββββββββββββ
β seed_done β β All subsequent turns β force FALLBACK model
β β
βββββββββββββββββββ
Special transitions:
/model command β manual_override (DME hands off)
/new or /reset β state cleared (next session starts fresh)
heartbeat β always PRIMARY, never advances phase
```
### Phase Machine
| Phase | Behavior | Transition |
|-------|----------|------------|
| `seed_pending` | Force primary model | β `seed_done` when primary confirmed via `llm_output` |
| `seed_done` | Force first fallback model | Stays until session ends |
| `manual_override` | No intervention | User took control via `/model` |
### Seed Verification
DME doesn't blindly trust `before_model_resolve`. It **verifies** in the `llm_output` hook that the primary model actually ran. If the runtime silently fell back to a different model (e.g. due to rate limits or unavailability), DME stays in `seed_pending` and retries on the next turn. This ensures the rotation only triggers after a confirmed primary run.
## Installation
### Prerequisites
- [OpenClaw](https://github.com/openclaw/openclaw) v2026.2.x or later
- At least one agent with `model.primary` and `model.fallbacks` configured
### Setup
1. **Clone into your extensions directory:**
```bash
cd ~/.openclaw/extensions
git clone https://github.com/FilHouston/openclaw-default-model-enforcer.git default-model-enforcer
```
2. **Enable the plugin in `openclaw.json`:**
```jsonc
{
"plugins": {
"allow": ["default-model-enforcer"],
"entries": {
"default-model-enforcer": {
"enabled": true
}
}
}
}
```
3. **Restart the gateway:**
```bash
openclaw gateway restart
```
4. **Verify in logs:**
```
[DME] registered v14 (file-backed state)
```
## Configuration
DME reads model configuration from your agent's existing `model` block in `openclaw.json`. No additional plugin-specific configuration is required.
```jsonc
{
"agents": {
"list": [
{
"id": "main",
"model": {
"primary": "openai/gpt-4.1", // β Used for seed turn + heartbeats
"fallbacks": ["anthropic/claude-sonnet-4"] // β First entry used after seed
}
}
]
}
}
```
| Field | Purpose |
|-------|---------|
| `model.primary` | Model used for the first turn of each session and all heartbeats |
| `model.fallbacks[0]` | Model used for all subsequent normal turns after seed confirmation |
## Architecture
### State Persistence
**v14** uses file-backed state at `/tmp/dme-state.json` instead of in-memory maps. This solves a critical cross-context isolation issue where OpenClaw's `[gateway]` and `[plugins]` execution contexts maintained separate `globalThis` scopes.
```
ββββββββββββββββ ββββββββββββββββ
β [gateway] β β [plugins] β
β context β β context β
ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β β
β read/write β read/write
β β
βΌ βΌ
ββββββββββββββββββββββββββββββββ
β /tmp/dme-state.json β
β (atomic writes via rename) β
ββββββββββββββββββββββββββββββββ
```
### Session Key Strategy
Sessions are identified by a composite key with priority:
1. **Primary:** `agent:<agentId>:sid:<sessionId>` β most stable identifier
2. **Fallback:** `agent:<agentId>:skey:<sessionKey>` β used when `sessionId` is unavailable
On `session_start`, ambiguous fallback-only states for the same agent are cleaned up to prevent ghost entries.
### Hook Registration
Registration is idempotent per API instance using a `WeakSet` on `globalThis`. This prevents duplicate hook registration during hot-reloads while still allowing registration across multiple API contexts (gateway + embedded agents).
## API Reference
### Hooks Registered
| Hook | Purpose |
|------|---------|
| `session_start` | Initialize session state as `seed_pending`, prune stale entries |
| `before_model_resolve` | Override model based on current phase |
| `llm_output` | Verify seed completion, confirm or retry |
| `command:model` | Transition to `manual_override` |
| `command:new` | Clear session state |
| `command:reset` | Clear session state |
| `session_end` | Clear session state |
### Log Markers
All log entries are prefixed with `[DME]` for easy filtering:
```bash
# Watch DME activity in real-time
openclaw gateway logs | grep "\[DME\]"
```
Expected sequence for a healthy session:
```
[DME] session_start: agent=main key=agent:main:sid:abc123 phase=seed_pending
[DME] before_model_resolve: agent=main key=agent:main:sid:abc123 phase=seed_pending -> primary openai/gpt-4.1
[DME] llm_output: seed confirmed agent=main key=agent:main:sid:abc123 ... -> phase=seed_done
[DME] before_model_resolve: agent=main key=agent:main:sid:abc123 phase=seed_done -> fallback anthropic/claude-sonnet-4
```
## Troubleshooting
| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| DME not activating | Plugin not in `plugins.allow` | Add `"default-model-enforcer"` to the allow list |
| Always stays on primary | Primary model unavailable, seed never confirmed | Check model availability, review `llm_output` logs |
| State lost after restart | `/tmp` cleared on reboot | Expected behavior β sessions re-seed automatically |
| Duplicate registration warnings | Multiple hot-reloads | Harmless β idempotent guard prevents actual duplicates |
## Development
```bash
# Run with verbose logging
openclaw gateway restart
openclaw gateway logs --follow
# Inspect current state
cat /tmp/dme-state.json | python3 -m json.tool
# Clear all state (forces re-seed on next turn)
rm /tmp/dme-state.json
```
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history.
## License
[MIT](LICENSE) β do whatever you want with it.
## Author
**Philipp Just** β [GitHub](https://github.com/FilHouston)
Built as part of a multi-agent AI infrastructure running on [OpenClaw](https://github.com/openclaw/openclaw).
integration
Comments
Sign in to leave a comment