Pular para o conteúdo principal
T

dsh-offpeak

tetrassky/dsh-offpeak

Holds DeepSeek model calls for the rest of each peak-hour window you configure instead of sending them, so they are billed at off-peak rates; also pauses active goals while it waits and shows the account balance with an 8-hour spend chart.

Instalar

dsh plugin --profile web add github:tetrassky/dsh-offpeak

README

dsh-offpeak

Holds DeepSeek model calls during peak pricing hours so they are sent at off-peak rates. While a window is open, a request is simply not sent: the agent waits, and resumes on its own when the window ends.

This is a standalone plugin. It reads and writes only its own files and does not depend on any other plugin.

Requirements

  • DeepSeek Harness with the web profile.
  • DEEPSEEK_API_KEY stored through the Models page, only if you want the balance and the spend chart. Pausing works without it.

Install

# from GitHub
dsh plugin --profile web add github:TetraSsky/dsh-offpeak

# or from a local checkout
dsh plugin --profile web add <path-to-this-package>

Then add dsh-offpeak to dsh.profile.bundles in $DSH_HOME/profiles/web/package.json and restart. The package's cordis.patch.yml inserts its own row.

For local development, a directory junction into the profile's node_modules plus an insert row in $DSH_HOME/profiles/web/cordis.patch.yml also works and reloads live — see Development.

How the hold works

Three separate mechanisms, which together are what "paused" means here:

MechanismEffect
Request gatellm/stream is the waterfall every model call passes through. Inside a window the call is parked before it reaches the adapter, so nothing is sent and nothing is billed.
Goal freezeAny goal in the active phase is paused, so it stops making progress and reads as paused rather than busy. Goals you paused yourself are never touched.
Status tooloffpeak_status lets the model report why it is waiting. Read-only: it cannot change the schedule.

There is no per-request override and no per-window skip. The schedule is the single statement of when calls are held; to end a pause early, edit the schedule or turn the plugin off.

A held request is released when its window ends, when the request is aborted, when the schedule changes so it is no longer held, or when the plugin unloads. A restart resumes exactly the goals this plugin paused, using a record it keeps for that purpose.

While the window is open (or about to open) the header carries a notice with a Dismiss button. Dismissing it silences that window for the rest of the page session — switching chats does not bring it back, since the header entry is rebuilt for every session — while a reload starts fresh and warns again. A different window is a different occurrence and is always announced.

Schedule and time zones

Peak pricing is defined in Beijing time, so the plugin keeps two zones apart:

  • Schedule time zone — the zone the windows are written in. Part of the schedule's identity.
  • Show times in — a viewing preference only. Changing it re-labels the times; it can never change a window.

Because the state machine is evaluated in the schedule zone, the browser and the host always agree about the state, whatever zone the browser is in.

Apply DeepSeek peak hours converts Beijing 09:00-12:00 and 14:00-18:00 (weekdays, two-minute margin) into whichever schedule zone you have selected, and appends them. It never changes your zone and never discards windows you added.

If your zone observes daylight saving, the converted windows drift relative to Beijing peak twice a year, because Beijing does not move. The settings page says so and offers to express the schedule in Beijing time instead, which cannot drift.

Balance and spend chart

With Show API balance on, the host reads the DeepSeek account balance with your stored key and the header shows the figure. Hovering it breaks the spend down; clicking it opens eight hours of ten-minute bars.

  • The key never leaves the host. The browser asks for the derived figure over an authenticated route.
  • Spend is the sum of balance drops, so a top-up cannot cancel out real spend.
  • Samples are taken every five minutes whenever the plugin is enabled, not only while the balance is displayed, and are kept in $DSH_HOME/offpeak-history.json (288 samples, about 24 hours).
  • Spend is a derived quantity, so history begins when the plugin does. Nothing can reconstruct usage from before it was installed.

Which currency is shown

The balance API answers in the account's own currency — for some accounts USD, for others CNY — and both the figure and its symbol come from that answer.

Balance currency overrides the symbol: Follow the API (the default), ¥ CNY, or $ USD. On its own the override only relabels the API's figure, which would misstate the amount if the two currencies differ, so choosing one also reveals CNY per USD. Leave that at 0 to show the API's figure unchanged; set a rate and every amount — balance, hover breakdown, chart bars — is converted at it.

Neither knob follows the interface language: a Chinese UI showing a USD account still shows the API's dollar figure unless you say otherwise.

Settings

SettingMeaning
Enable off-peak pauseMaster switch.
Schedule time zoneZone the windows are written in. The picker lists 58 zones covering every whole-hour offset from -11 to +14 plus the half- and quarter-hour ones, each labelled with its offset and sorted by it.
Show times inViewing zone for the second, greyed time.
Pause windowspauseAt-resumeAt wall-clock ranges, each with its own weekdays.
Days that pauseWeekdays the schedule applies to. Weekends are off-peak, so excluded.
Warn before pauseHow long before a window the notice appears.
Applies toAll calls, or DeepSeek official routes only. Default is official routes, because peak pricing exists only there.
Show API balanceBalance figure and spend chart.
Balance currencySymbol to show, and the rate to convert with. See above.
Show the status in the headerHide the header entry entirely.

Language

Strings follow the harness's own language selection; there is no separate setting, so switching the harness between English and Chinese switches this plugin with it.

The harness ships exactly two locales (zh and en), and the plugin ships a dictionary for each — no more and no less. A dictionary for a locale the harness does not ship could never be selected, and a locale the plugin has no dictionary for falls back to English. A missing key also falls back to English, so a raw key is never shown.

Files

PathContents
$DSH_HOME/settings.yamlConfiguration, under the offpeak namespace, in the harness settings document.
$DSH_HOME/offpeak-history.jsonBalance samples for the chart.
$DSH_HOME/offpeak-paused.jsonGoals paused by this plugin, so a restart resumes only those.

Development

There is no build step for the host half: index.js and src/*.js are the source.

The browser half is generated, because a client bundle cannot import a relative file and the schedule logic must be shared verbatim with the host:

node scripts/build-client.mjs   # src/{core,spend,money,i18n,client}.js -> client.js
node tests/run.mjs              # 163 tests

tests/bundle.test.mjs fails if client.js drifts from its sources, so run the build after changing any of those five files.

When installing through a junction, the plugin is realpath'd out of the profile, so @deepseek-ai/* stops resolving from it. A node_modules/@deepseek-ai junction inside this directory pointing at the profile's own set fixes that; it is not needed for a copied install.

Three things that cost time to find

The client locale service has to be declared in inject. ctx.get('locale') reads as a tolerant lookup, but a service is only returned once its providing fiber is active — read it a moment too early and you get undefined, after which every string in the plugin quietly falls back to English while the rest of the harness switches language. Declaring locale parks activation until the service is live. The settings section label is projected by the settings shell rather than by this plugin, so it is registered as a thunk; a plain string keeps whatever language it was registered under.

Connection's generic RPC is unusable from another plugin. ctx.connection.rpc.handle() looks like the right way to serve host facts to your own browser half, but it registers the route through Connection's own fiber, which never injects webServer. The registration throws and the route silently never exists. Its other seat, intercept('/api', ...), is already claimed by the API Gateway. So this plugin registers its own webServer prefix route and calls connection.requestRejection() per request to keep the same Host/Origin fence and browser-session check.

A route registered before Connection activates has no fence. Connection can come up after the route is installed, so requestRejection is resolved per request rather than captured once. Capturing it at install time silently serves an unauthenticated endpoint.

Credits

This plugin exists because of dsh-save-money by zhu168, which is MIT licensed. Reading its source is how the shape of the problem was learned: the llm/stream waterfall as the place to hold a call, pausing goals rather than only their next request, the DeepSeek peak-hours preset, and deriving spend from balance samples. The state machine's three phases and the half-open, midnight-crossing window matching follow its design.

It was written fresh rather than forked, because several of its decisions could not be kept: configuration resolved from live session state, a balance history keyed by API-key fingerprint, unauthenticated HTTP routes, and a model-facing tool that could rewrite the schedule. Those are documented in the commit history rather than copied here.

If you want the plugin this one replaces, use theirs. If you want the fixes, use this.

Licence

MIT.

Plugins relacionados