Tools
Cycles Openclaw Budget Guard
OpenClaw plugin for budget-aware model and tool execution using Cycles.
Install
npm install
#
Configuration Example
{
"plugins": {
"entries": {
"cycles-openclaw-budget-guard": {
"cyclesBaseUrl": "https://cycles.example.com",
"cyclesApiKey": "cyc_your_api_key_here",
"tenant": "my-org"
}
}
}
}
README
# cycles-openclaw-budget-guard
OpenClaw plugin for budget-aware model and tool execution using [Cycles](https://github.com/runcycles).
## What This Is (Phase 1)
A thin OpenClaw plugin that integrates with a live Cycles server to enforce budget boundaries during agent execution. It hooks into the OpenClaw plugin lifecycle to:
- **Downgrade models** when budget is low (configurable fallback map)
- **Block execution** when budget is exhausted (fail-closed by default)
- **Reserve budget** before tool calls and commit usage afterward
- **Inject budget hints** into prompts so the model is budget-aware
- **Emit a budget summary** at the end of each agent session
The plugin uses the [`runcycles`](https://github.com/runcycles/cycles-client-typescript) TypeScript client to communicate with a Cycles server via the reserve → commit → release protocol.
## What This Does Not Do (Yet)
- **No per-token LLM enforcement** — there is no proxy or provider layer. Token-level metering requires a gateway-level integration (planned for phase 2).
- **No streaming cost tracking** — tool costs are estimated upfront via the `toolBaseCosts` config map.
- **No multi-currency support** — a single `currency` unit is used for all reservations.
## Prerequisites
- **OpenClaw** >= 0.1.0 with plugin support
- **Node.js** >= 20.0.0
- A running **Cycles server** with:
- A base URL (e.g. `https://cycles.example.com`)
- An API key
- A tenant configured with a budget scope
If you don't have a Cycles server yet, see the [Cycles quickstart](https://github.com/runcycles) to set one up.
## Quick Start
### 1. Install the plugin
```bash
openclaw plugins install @runcycles/openclaw-budget-guard
```
This fetches the package from npm, extracts it into `~/.openclaw/extensions/cycles-openclaw-budget-guard/`, and registers it with OpenClaw.
For local development, install from a checkout instead:
```bash
openclaw plugins install -l ./cycles-openclaw-budget-guard
```
### 2. Enable the plugin in OpenClaw
```bash
openclaw plugins enable cycles-openclaw-budget-guard
```
### 3. Add minimal configuration
Add the plugin to your OpenClaw configuration file (typically `openclaw.config.json` or the `plugins` section of your project config):
```json
{
"plugins": {
"entries": {
"cycles-openclaw-budget-guard": {
"cyclesBaseUrl": "https://cycles.example.com",
"cyclesApiKey": "cyc_your_api_key_here",
"tenant": "my-org"
}
}
}
}
```
That's it — the plugin uses sensible defaults for everything else. The agent will now enforce budget limits on every run.
### 4. Verify it's working
Run an OpenClaw agent with `logLevel: "debug"` to see budget guard activity:
```json
{
"plugins": {
"entries": {
"cycles-openclaw-budget-guard": {
"cyclesBaseUrl": "https://cycles.example.com",
"cyclesApiKey": "cyc_your_api_key_here",
"tenant": "my-org",
"logLevel": "debug"
}
}
}
}
```
You should see log lines prefixed with `[cycles-budget-guard]`:
```
[cycles-budget-guard] Plugin initialized { tenant: 'my-org' }
[cycles-budget-guard] before_model_resolve: model=claude-sonnet-4-20250514 level=healthy
[cycles-budget-guard] before_prompt_build: injecting hint (142 chars)
[cycles-budget-guard] before_tool_call: tool=web_search callId=abc123 estimate=500000
[cycles-budget-guard] after_tool_call: committed 500000 for tool=web_search
[cycles-budget-guard] Agent session budget summary: { remaining: 9500000, spent: 500000, totalReservationsMade: 1 }
```
### 5. (Optional) Use environment variables for secrets
Instead of putting API credentials in your config file, set them as environment variables:
```bash
export CYCLES_BASE_URL="https://cycles.example.com"
export CYCLES_API_KEY="cyc_your_api_key_here"
```
Then your config only needs:
```json
{
"plugins": {
"entries": {
"cycles-openclaw-budget-guard": {
"tenant": "my-org"
}
}
}
}
```
## Full Configuration Example
```json
{
"plugins": {
"entries": {
"cycles-openclaw-budget-guard": {
"enabled": true,
"cyclesBaseUrl": "https://cycles.example.com",
"cyclesApiKey": "cyc_your_api_key_here",
"tenant": "my-org",
"budgetId": "my-app",
"currency": "USD_MICROCENTS",
"lowBudgetThreshold": 10000000,
"exhaustedThreshold": 0,
"modelFallbacks": {
"claude-opus-4-20250514": "claude-sonnet-4-20250514",
"gpt-4o": "gpt-4o-mini"
},
"toolBaseCosts": {
"web_search": 500000,
"code_execution": 1000000
},
"injectPromptBudgetHint": true,
"maxPromptHintChars": 200,
"failClosed": true,
"logLevel": "info"
}
}
}
}
```
### Config Reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `true` | Master switch — set to `false` to disable the plugin without removing config |
| `cyclesBaseUrl` | string | — | Cycles server URL (required, or set `CYCLES_BASE_URL` env var) |
| `cyclesApiKey` | string | — | Cycles API key (required, or set `CYCLES_API_KEY` env var) |
| `tenant` | string | — | Cycles tenant identifier (required) |
| `budgetId` | string | — | Optional app-level scope — maps to the `app` field in the Cycles subject for both balance queries and reservations |
| `currency` | string | `USD_MICROCENTS` | Budget unit used for all reservations |
| `defaultModelActionKind` | string | `llm.completion` | Reserved for phase 2 — action kind for model call reservations (not used in phase 1) |
| `defaultToolActionKindPrefix` | string | `tool.` | Prefix prepended to tool names to form the action kind (e.g. `tool.` + `web_search` → `tool.web_search`) |
| `lowBudgetThreshold` | number | `10000000` | Remaining budget below this triggers model downgrade |
| `exhaustedThreshold` | number | `0` | Remaining budget at or below this blocks execution |
| `modelFallbacks` | object | `{}` | Map: expensive model → cheaper fallback model |
| `toolBaseCosts` | object | `{}` | Map: tool name → estimated cost in currency units |
| `injectPromptBudgetHint` | boolean | `true` | Inject budget status into the system prompt |
| `maxPromptHintChars` | number | `200` | Max characters for the injected budget hint |
| `failClosed` | boolean | `true` | Block on exhausted budget (`false` = warn and continue) |
| `logLevel` | string | `info` | `debug` / `info` / `warn` / `error` |
> **Note:** `exhaustedThreshold` must be strictly less than `lowBudgetThreshold`. The plugin validates this at startup and throws an error if misconfigured.
## How It Works
### Hook Priorities
All hooks register at priority 10 except `agent_end` which uses priority 100 so that other plugins finish their cleanup before the budget summary runs.
### Budget Levels
The plugin classifies budget into three levels based on the remaining balance from the Cycles server:
| Level | Condition | What Happens |
|-------|-----------|--------------|
| **healthy** | `remaining > lowBudgetThreshold` | Pass through — no intervention |
| **low** | `exhaustedThreshold < remaining <= lowBudgetThreshold` | Downgrade models via `modelFallbacks`, inject budget warning into prompts |
| **exhausted** | `remaining <= exhaustedThreshold` | Block execution (`failClosed=true`) or warn and continue (`failClosed=false`) |
### Hook: `before_model_resolve`
Fetches budget state from the Cycles `/v1/balances` endpoint. If budget is healthy, passes through. If low, checks `modelFallbacks` for a cheaper alternative and returns `{ modelOverride }`. If exhausted and `failClosed` is true, throws `BudgetExhaustedError`.
### Hook: `before_prompt_build`
When `injectPromptBudgetHint` is enabled, returns `{ prependSystemContext }` with a compact deterministic hint. Example:
```
Budget: 5000000 USD_MICROCENTS remaining. Budget is low — prefer cheaper models and avoid expensive tools. 50% of budget remaining.
```
### Hook: `before_tool_call`
Looks up the tool's estimated cost from `toolBaseCosts` (defaults to 100,000 units if not configured). Creates a Cycles reservation via `POST /v1/reservations` with action kind `{defaultToolActionKindPrefix}{toolName}`. If the reservation is denied (`DENY` decision), returns `{ block: true, blockReason: "..." }` to block the tool call. `ALLOW` and `ALLOW_WITH_CAPS` decisions both permit the call. Otherwise stores the reservation for settlement in `after_tool_call`.
### Hook: `after_tool_call`
Commits the reserved amount as actual usage via `POST /v1/reservations/{id}/commit`. In phase 1, actual cost equals the estimate since there is no proxy to measure real token usage. Commit is best-effort — failures are logged but never block execution.
### Hook: `agent_end`
Releases any orphaned reservations (defensive cleanup), fetches final budget state, and logs a session summary including total spent, remaining balance, and number of reservations made. Attaches the summary to `ctx.metadata["cycles-budget-guard"]`.
### Error Handling
The plugin exports two structured error types that can be imported for error handling:
```typescript
import { BudgetExhaustedError, ToolBudgetDeniedError } from "@runcycles/openclaw-budget-guard";
```
- **`BudgetExhaustedError`** (`code: "BUDGET_EXHAUSTED"`, `remaining: number`) — thrown by `before_model_resolve` when budget is exhausted and `failClosed` is true.
- **`ToolBudgetDeniedError`** (`code: "TOOL_BUDGET_DENIED"`, `toolName: string`) — available as a structured error type. The `before_tool_call` hook returns `{ block: true, blockReason }` to OpenClaw when a reservation is denied.
### Fail-Open Behavior
- If the Cycles server is **unreachable** during a balance check, the plugin assumes healthy budget (fail-open) to avoid blocking agents on transient infrastructure issues.
- If a **commit fails** in `after_tool_call`, the failure is logged but execution continues.
- The `failClosed` config only
... (truncated)
tools
Comments
Sign in to leave a comment