This utility automatically adjusts the OpenClaw LLM model and thinking level (low, medium, high) based on the remaining Codex quota.
I was using OpenClaw with OpenAI OAuth and ran into quota limitations when selecting the most capable models and higher reasoning levels. OpenAI applies quota limits that reset both every five hours and weekly. Monitoring both quota windows and manually adjusting the LLM model configuration was repetitive and easy to overlook, so an automated approach felt worthwhile. I built this project to automate those policy changes and reduce the risk of running into quota limits. I purposely kept this optimizer outside the OpenClaw installation so it is easier to:
- back up separately
- move to another machine
- version independently
- refine without mixing watchdog files into OpenClaw state folders
- Automatically selects a target model and
thinkinglevel from configurable quota bands - Supports temporary reset-soon upgrades and a weekly low-balance safety override
- Runs as an external watchdog using
systemd --user, not as an OpenClaw patch or plugin - Optionally enforces the selected profile on recent matching direct sessions in addition to changing global defaults
- Can send optional notifications through OpenClaw when it actually applies a change
- Uses a local TOML policy file for reproducible behavior
- Includes an installer that validates prerequisites and renders machine-specific files
- Includes a matching uninstall script that safely removes the local
systemd --userwiring
The current implementation is intentionally narrow:
- it is designed for OpenClaw setups that use
openai-codexthrough OAuth - it expects OpenClaw usage snapshots to expose the
openai-codexprovider
The installer checks for this and stops with a clear message if the machine does not look like a compatible openai-codex OAuth-based setup.
The watchdog is an external automation made of:
- a
systemd --usertimer - a
systemd --userone-shot service - a Python script
- a local TOML policy file
- an installer script that renders machine-specific files
- optional outbound notifications sent through OpenClaw
OpenClaw does not run this code internally. The watchdog simply uses official OpenClaw commands and RPC methods from the outside.
The runtime architecture is:
- A
systemd --usertimer triggers the optimizer periodically. - The timer starts a
systemd --userone-shot service. - That service runs
python openclaw-model-cost-optimizer.py. - The Python script reads the local
config.toml, asks OpenClaw for the current usage snapshot, computes the target model/thinking profile, and only then applies changes if needed. - OpenClaw itself remains external to this project. The optimizer does not run inside OpenClaw and does not patch OpenClaw source code.
Inside the Python script, the logical flow is:
- Read
openclaw status --json --usageand extract:5h_balance5h_reset_in_minutesweekly_balanceweekly_reset_in_minutes
- Select the base band from
five_hour_balance.bandsusing only5h_balance. - Evaluate
reset_soon:- if the 5-hour reset is close enough
- and the configured safety conditions are satisfied
- then temporarily raise the selected band by the configured number of steps
- Evaluate the weekly override at the very end:
- if
weekly_balanceis belowweekly_balance.percentage_override_condition - and the weekly reset is still more than
weekly_balance.days_left_override_conditiondays away - then force band
1
- if
- Convert the final band into a target
model + thinkingprofile. - Compare that target profile with the current OpenClaw defaults and, if enabled, with relevant recent direct sessions.
- If a change is needed, apply it through official OpenClaw interfaces:
openclaw models set ...openclaw config set agents.defaults.thinkingDefault ...openclaw gateway call sessions.patch ...for relevant recent direct sessions
- If notifications are enabled and the optimizer actually changes OpenClaw during this run, send the message through OpenClaw.
In short, the timer/service layer is only the scheduler. The real decision logic lives in the Python script, and the Python script treats OpenClaw as an external system that it reads from and updates through supported commands.
-
openclaw-model-cost-optimizer.pyMain script. It reads usage, applies policy, and updates OpenClaw. -
config.example.tomlTemplate for the local policy file. The installer renders this intoconfig.toml. -
openclaw-model-cost-optimizer.service.templateTemplate for the localsystemd --userservice file. The installer renders this intoopenclaw-model-cost-optimizer.service. -
openclaw-model-cost-optimizer.timerTimer unit committed as-is. It triggers the service every 10 minutes. -
install.shSetup script. It validates prerequisites, renders local files, installs the user service/timer, and optionally starts them. -
uninstall.shTeardown script. It disables the user timer, removes the installed unit symlinks, removes the generated service file, and can optionally purge local runtime files. -
README.mdThis documentation.
Generated locally by the installer:
config.tomlopenclaw-model-cost-optimizer.service
Clone the repository into the directory where you want the watchdog to live, then run the installer from that directory.
Example:
git clone https://github.com/carlosrpi/openclaw-model-cost-optimizer.git ~/openclawModelCostOptimizer
cd ~/openclawModelCostOptimizer
chmod +x install.sh
./install.shWhat the installer does:
- Checks that
python3exists and is version 3.11 or newer. - Checks that
systemctlexists and then usessystemctl --user. - Finds the OpenClaw binary.
- Checks that OpenClaw config exists.
- Verifies the machine looks like an
openai-codexsetup. - Renders
config.tomlfromconfig.example.toml. - Renders
openclaw-model-cost-optimizer.servicefrom the service template. - Installs user-level
systemdsymlinks. - Reloads
systemd --user. - Enables and starts the timer by default.
Installer options:
./install.sh --helpSupported options:
-
--force-configRe-renderconfig.tomleven if it already exists. -
--no-enableInstall files but do not enable or start the timer. -
--openclaw-bin PATHOverride the OpenClaw binary path. -
--openclaw-config PATHOverride the OpenClaw config path.
Stop and remove the installed systemd --user wiring:
chmod +x uninstall.sh
./uninstall.shDefault uninstall behavior:
- Disables and stops the timer.
- Stops the one-shot service if it is currently running.
- Removes the installed user-level
systemdsymlinks when they still point to this project. - Removes the generated
openclaw-model-cost-optimizer.servicefile from the project directory. - Reloads
systemd --user.
By default, the uninstall script keeps:
config.toml
Use this if you also want to remove config.toml and any legacy local state file from older versions:
./uninstall.sh --purgeThe policy lives in config.toml.
TOML was chosen because it is:
- easy to read
- easy to comment
- easier to maintain than hardcoded thresholds in Python
- built into modern Python via
tomllib
The installer generates config.toml from config.example.toml, replacing the machine-specific paths.
Example rendered config:
[openclaw]
bin = "/home/example/.npm-global/bin/openclaw"
config_path = "/home/example/.openclaw/openclaw.json"
provider = "openai-codex"
[behavior]
manage_sessions = true
active_minutes = 1440
[five_hour_balance]
[[five_hour_balance.bands]]
rank = 1
min_pct = 0
max_pct = 30
model = "openai-codex/gpt-5.4-mini"
thinking = "high"
[[five_hour_balance.bands]]
rank = 2
min_pct = 30
max_pct = 60
model = "openai-codex/gpt-5.4"
thinking = "medium"
[[five_hour_balance.bands]]
rank = 3
min_pct = 60
max_pct = 100
model = "openai-codex/gpt-5.4"
thinking = "high"
[weekly_balance]
percentage_override_condition = 15
days_left_override_condition = 1
[reset_soon]
enabled = true
window_minutes = 10
min_weekly_pct = 45
min_five_pct = 15
max_upgrade_steps = 1
allow_highest_band = false
[notifications]
enabled = false
message_prefix = "ModelCostOptimizer"
include_reasons = true
[[notifications.destinations]]
channel = "telegram"
target = "123456789"
account = "default"
thread_id = "ops-room"
silent = falseImportant behavior that is not obvious from the example alone:
openclaw.providerdecides which provider block is read fromopenclaw status --json --usage.- The same
openclaw.providervalue is also used to normalize bare model names and to ignore sessions that belong to a different provider. behavior.manage_sessions = falsekeeps the optimizer at the global-default level only and skipssessions.patch.behavior.active_minutesis forwarded toopenclaw sessions --active ...so only recently active sessions are considered for enforcement.
The current decision policy has three layers, applied in this order:
- Choose a base band from
5h_balance. - Optionally raise that band with
reset_soon. - Optionally force band
1with the weekly override.
The base decision comes from five_hour_balance.bands.
Each band defines:
min_pctmax_pctmodelthinking
model can be written either as a full provider/model reference or as a bare model name. If the provider prefix is omitted, the script prepends openclaw.provider.
The script looks at 5h_balance and picks the band whose configured range contains that percentage.
This supports any number of bands, as long as:
- the first band starts at
0 - the last band ends at
100 - bands are contiguous, with no gaps or overlaps
Band 1 is treated as the lowest band. Higher rank numbers mean more permissive bands.
After the base band is selected, reset_soon may raise it temporarily.
That happens only if all configured safety conditions pass:
- the feature is enabled
- the 5-hour reset is within
reset_soon.window_minutes weekly_balanceis at leastreset_soon.min_weekly_pct5h_balanceis at leastreset_soon.min_five_pct
If those checks pass, the script raises the selected band by up to reset_soon.max_upgrade_steps.
If allow_highest_band = false, the reset-soon adjustment cannot reach the top configured band.
The weekly override runs last, after the base selection and after reset_soon.
If both conditions are true:
weekly_balance < weekly_balance.percentage_override_condition- the weekly reset is still more than
weekly_balance.days_left_override_conditiondays away
then the script forces the final result to band 1.
With the default example config, this means:
- if weekly balance drops below
15% - and there is still more than
1 dayleft before the weekly reset
then the optimizer ignores the previously selected band and uses band 1.
The watchdog can optionally send a notification when it actually changes OpenClaw model/thinking settings during a run.
This is useful both for quota-driven policy changes and for cases where the optimizer corrects an externally introduced mismatch.
Notifications are sent through OpenClaw itself:
openclaw message send --channel ... --target ... --message ...
That keeps the watchdog external to OpenClaw while still reusing the channels that OpenClaw already knows how to deliver to.
Each notification destination can also set:
accountto pick a non-default OpenClaw messaging accountthread_idto post into a specific thread when the channel supports itsilent = trueto request a quiet delivery
Recommended initial notification policy:
- notify every time the optimizer actually changes OpenClaw defaults or patches sessions
Example notification config:
[notifications]
enabled = true
message_prefix = "ModelCostOptimizer"
include_reasons = true
[[notifications.destinations]]
channel = "telegram"
target = "123456789"
account = "default"
thread_id = "ops-room"
silent = falseExample message:
.................................................
Model changed
From: openai-codex/gpt-5.4 High
To: openai-codex/gpt-5.4-mini High
Reason: 5h balance 28% selects band 1 (0%..30%)
.................................................
5h balance at 28% will be reset in 1 hour 58 minutes
Weekly balance at 61% will be reset in 4 days 7 hours 12 minutes
.................................................
Changing only agents.defaults.thinkingDefault is not always enough.
If a session already has a stored thinkingLevel or a stored model selection, that session may continue using its own stored value. Because of that, the watchdog does three things:
- Update the global OpenClaw default model.
- Update the global OpenClaw default thinking level.
- Patch relevant recent direct sessions when they differ from the rule-selected target profile.
More specifically, the current implementation only considers:
- direct sessions
- sessions whose provider matches
openclaw.provider - sessions returned by the optional
behavior.active_minutesfilter when that setting is enabled
If one of those sessions differs from the current target profile, the watchdog patches it. The optimizer does not keep a separate ownership history for sessions anymore.
The watchdog does not manually edit OpenClaw files. It uses official OpenClaw interfaces:
openclaw status --json --usageopenclaw models set ...openclaw config set agents.defaults.thinkingDefault ...openclaw gateway call sessions.patch ...openclaw message send ...for optional notifications
Those operations end up changing OpenClaw state such as:
~/.openclaw/openclaw.json~/.openclaw/agents/main/sessions/sessions.json
No special OpenClaw integration is required.
OpenClaw does not need a plugin, hook, or extra registration step for this watchdog to work. OpenClaw only needs to keep exposing the official capabilities that the watchdog already uses:
openclaw status --json --usageopenclaw models setopenclaw config setopenclaw gateway call sessions.patch
So OpenClaw works normally even if this project does not exist.
From OpenClaw's point of view, the watchdog is just an external actor that sometimes changes:
- the global default model
- the global
thinkingDefault - selected session-level
model/thinkingLevelvalues
Run one manual pass:
python3 ./openclaw-model-cost-optimizer.py --settings-file ./config.tomlRun a simulation without changing anything:
python3 ./openclaw-model-cost-optimizer.py --settings-file ./config.toml --dry-runSend a test notification without waiting for a quota-driven level change:
python3 ./openclaw-model-cost-optimizer.py --settings-file ./config.toml --test-notificationCheck timer status:
systemctl --user status openclaw-model-cost-optimizer.timerCheck the latest service logs:
journalctl --user -u openclaw-model-cost-optimizer.service -n 50 --no-pagerTrigger an immediate run:
systemctl --user start openclaw-model-cost-optimizer.serviceReload systemd after editing unit files:
systemctl --user daemon-reload
systemctl --user restart openclaw-model-cost-optimizer.timerThe current version includes a few defensive checks aimed at making the project safer to reuse:
- config values are validated before use
- percentage thresholds must stay within
0..100 - 5-hour bands must be contiguous and cover
0..100 - invalid reset-soon settings fail early with a readable error
- the installer validates prerequisites before wiring the timer
Likely next improvements:
- add optional include/exclude rules for which sessions should be patched
- support more nuanced reset logic
- add optional metrics/history output