omp-peak-gate0.3.0
Gate remote DeepSeek LLM calls during DeepSeek peak-pricing windows, with opt-in fallback via retry.fallbackChains and automatic session resume
Built for omp
The author tagged this package oh-my-pi.
Reports 0
No reports yet.
Readme
omp-peak-gate
Park remote DeepSeek LLM calls while DeepSeek's peak-pricing window is open — with opt-in fallback to your configured next model.
DeepSeek bills full rate Mon–Fri 01:00–04:00 and 06:00–10:00 UTC. This
plugin engages the same process-global agentPauseGate the TUI /pause command
uses at those boundaries, so:
- in-flight provider streams finish (already billed),
- no new model call starts while the window is open,
- every agent loop parks instead of aborting — queued prompts and steering messages deliver normally once the window closes.
Scope: only remote DeepSeek models. The gate engages only when the session's
current model has deepseek in its provider, model id or name and is served
off this machine. Everything else keeps running during the window:
- other models (Claude, GPT, your default model, …) — never gated,
- DeepSeek checkpoints running locally — Ollama, llama.cpp, LM Studio, vLLM and
friends (loopback
baseUrlor a known local provider id) — never gated, they are not billed at DeepSeek peak rates.
/peak-gate pause still pauses every model regardless of scope.
Fallback (opt-in). If the gated model has an entry in oh-my-pi's own
retry.fallbackChains settings, the session switches to the next usable model
at the window's start instead of parking, and switches back when the window
closes. No chain configured → the gate parks as before; there is nothing to set
in this plugin. Candidates that are themselves remote DeepSeek models are
skipped (they would not escape peak pricing); a local DeepSeek checkpoint in the
chain is a valid, free target.
The gate decision is a pure function of the wall clock, the session model and a static window table, so suspend/resume, clock jumps and timer jitter cannot drift it. An exact edge timer engages/releases at the boundary; a 30-second heartbeat re-derives state (and notices model changes).
Windows are a static weekly UTC table. There is deliberately no holiday calendar — the subjectively-set, yearly Chinese holiday dates are not modeled. Every weekday listed in the table is subject to the gate.
Resuming after the pause
Parking only works while a loop is alive. A run can end while the gate holds it, and then nothing is left to wake when the gate opens. The plugin therefore covers the omp edge cases around that:
| Situation | What happens |
|---|---|
| Loop parked at a model/tool boundary | agentPauseGate.resume() wakes it; no prompt is injected (a live loop makes the check a no-op). |
| Run aborted while parked (host timeout, deadline, provider abort) | One continuation prompt is sent when the gate opens and the session is idle. |
wait tool timed out or returned during the pause |
The tool result wakes the loop; if the loop already settled, the aborted-run path above sends the prompt. |
Gate released manually while an interruptible wait is still in flight |
After a hold of at least 5s, the plugin sends an aside message that ends the wait, so the turn returns to the model instead of sitting out the 30-minute cap. |
| Steer/follow-up queued while an idle session was paused | The queue never starts a turn by itself; the plugin starts one on release so the queued batch drains. |
Process died mid-park, or a host deadline (--max-time) disposed the session before the gate opened |
A per-session marker (~/.omp/agent/peak-gate-resume.json, 24h TTL) causes one continuation prompt when that session is next resumed. Cleared on an ordinary graceful shutdown, kept when a resume is pending. |
| User pressed Esc / deliberately interrupted | Never auto-resumed; a user-interrupt abort is distinguished from a host/deadline abort. |
| Fallback model took over | The gate only pauses briefly (or not at all); no resume prompt is generated. |
Auto-resume is bounded to two injections per hold, never runs in subagent
sessions, and can be disabled entirely with OMP_PEAK_GATE_RESUME=0. The
injected prompt is attributed to the agent, not the user. /peak-gate status
shows whether a run is live, whether it observed a pause, whether a wait is
active, and whether a resume is armed.
Install
# local checkout / development (symlinked, edits are live)
omp plugin link /path/to/omp-peak-gate
# git
omp plugin install github:alosslessdev/omp-peak-gate-main
# marketplace (this repo ships .omp-plugin/marketplace.json)
/marketplace add alosslessdev/omp-peak-gate-main
/marketplace install peak-gate@omp-peak-gate
# npm
omp plugin install omp-peak-gate
Check it landed: omp plugin list, then /peak-gate status in a session.
Do not also keep a copy at ~/.omp/agent/extensions/peak-gate.ts — both paths
load, and the gate would be registered twice.
Commands
| Command | Effect |
|---|---|
/peak-gate |
Show state, windows, model scope, fallback, next boundary |
/peak-gate pause |
Hold LLM calls paused until the next window boundary |
/peak-gate resume |
Release now, skip the current window (restore a fallback) |
/peak-gate reload |
Re-read the config file and re-arm the timers |
While the gate is held, the status line shows ⏸ peak-gate until <UTC>; while
running on a fallback model it shows ⏸ peak-gate ↓<model> until <UTC>.
pause pauses every model regardless of scope; resume releases the gate and
switches back to the original DeepSeek model if a fallback was applied.
Configuration
~/.omp/agent/peak-gate.json (agent dir follows PI_CODING_AGENT_DIR):
{
"windows": [
{ "startMinute": 60, "endMinute": 240, "weekdays": [1, 2, 3, 4, 5] },
{ "startMinute": 360, "endMinute": 600, "weekdays": [1, 2, 3, 4, 5] }
]
}
startMinute/endMinuteare UTC minutes of day, half-open[start, end);startMinutemust be less thanendMinute(a midnight-crossing window is two entries).endMinute: 1440means midnight.weekdaysusesDate#getUTCDaynumbering (0 = Sunday). Omit it for daily.{ "enabled": false }disables the plugin without uninstalling it.
Fallback (opt-in, configured in omp itself)
Fallback uses oh-my-pi's standard retry.fallbackChains — nothing plugin-side
to configure. In ~/.omp/agent/config.yml (or project .omp/config.yml):
retry:
fallbackChains:
deepseek/deepseek-v4-flash:
- xiaomi/mimo-v2.6-flash:high
- anthropic/claude-sonnet-4
default:
- ollama/qwen3:8b
At the window's start the gated model's chain is walked (same resolution core
uses on provider errors: exact model key, provider wildcard, model role, then
default) and the first usable non-remote-DeepSeek model takes over. Without
an entry the gate parks instead — fallback stays opt-in.
Environment overrides:
| Variable | Effect |
|---|---|
OMP_PEAK_GATE=off |
Disable the gate for this process |
OMP_PEAK_GATE_WINDOWS=JSON |
Replace the window table ([{"startMinute":60,"endMinute":240}]) |
OMP_PEAK_GATE_QUIET=1 |
Suppress the "a call slipped through" warning |
OMP_PEAK_GATE_KEEPALIVE=0 |
Let a parked headless run exit instead of waiting out the window |
OMP_PEAK_GATE_RESUME=0 |
Disable auto-resume (prompt injection, wait interrupt, restart marker) |
OMP_PEAK_GATE_DEBUG=/path |
Append decision lines to a log file |
What is and is not gated
Gated (the gate is polled before each model call and each tool call): the main agent, in-process subagents, advisors, tool loops — i.e. everything that spends through an agent loop — but only while the session model is a remote DeepSeek model (see scope above). The gate itself is process-global: a session parked on DeepSeek holds every loop in the process until the window closes or the fallback switch releases it.
Not gated: a few side channels stream outside any agent loop (/btw side turns,
session title generation, remote compaction). before_provider_request cannot
veto them — that hook may only replace the payload and a thrown error is
swallowed — so they are surfaced as a throttled warning instead of being billed
silently. Use OMP_PEAK_GATE_QUIET=1 to silence it. Cache warming is stopped
for the duration of a hold (cache_warming_decision → stop), since it is a
replay outside the loop; it resumes on the next real request.
Never gated: non-DeepSeek models and locally served models — they do not match
the scope, so no window can pause them. To pause everything manually, use
/peak-gate pause; to change when the window applies, edit windows.
Tests
bun test
test/peak-gate.test.ts loads the real extension against a mocked omp runtime
(fake clock, timers, pause gate and session context) and covers the resume
watchdog, wait interruption, gate ownership across sessions, and the restart
marker.
Layout
package.json omp.extensions -> ./extensions/peak-gate.ts
extensions/peak-gate.ts the extension module
test/peak-gate.test.ts deterministic edge-case tests (bun test)
.omp-plugin/marketplace.json catalog so the repo is its own marketplace