← Back to Plugins
Integration

Default Model Enforcer

FilHouston By FilHouston 👁 522 views ▲ 0 votes

OpenClaw plugin that enforces a model rotation strategy per session — seed on primary, then switch to fallback. Saves API costs without sacrificing quality.

GitHub

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

Loading comments...