@s-berdia/opencode-router 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -10
- package/USAGE.md +56 -17
- package/dist/bin.js +116 -44
- package/dist/bin.js.map +1 -1
- package/dist/index.js +111 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
**Switch models before you hit your [OpenCode](https://opencode.ai) limit.** Automatic
|
|
9
9
|
model routing and quota failover for the **Go**, **Go Plus**, and **Free** plans.
|
|
10
10
|
|
|
11
|
-

|
|
12
12
|
|
|
13
13
|
[Watch the demo in full quality (MP4, 25 seconds)](docs/router-demo.mp4?raw=true)
|
|
14
14
|
|
|
15
15
|
opencode-router gives each OpenCode agent the best model for its job and for your plan.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
All Go models share one usage allowance, and the router estimates how much of it you
|
|
17
|
+
used. When the allowance is near its limit, the router moves the agents to free models,
|
|
18
|
+
so your work does not stop. When the use falls again, the router moves them back.
|
|
19
19
|
|
|
20
20
|
For full step-by-step instructions, read the [usage guide](./USAGE.md). Before you use
|
|
21
21
|
the router, read its [known limits](./USAGE.md#11-known-limits).
|
|
@@ -29,8 +29,10 @@ the router, read its [known limits](./USAGE.md#11-known-limits).
|
|
|
29
29
|
monthly allowance of each model to calculate how many tokens the model can supply in
|
|
30
30
|
5 hours.
|
|
31
31
|
- **Automatic failover.** The `plan`, `build`, `general`, and `explore` agents each have a
|
|
32
|
-
ranked list of up to 4 models.
|
|
33
|
-
|
|
32
|
+
ranked list of up to 4 models. On Go and Go Plus, the last one is a free model. At 85%
|
|
33
|
+
of the shared Go allowance, or after a usage-limit error, the agents move to their free
|
|
34
|
+
model. After a rate-limit or overload error of one model, the agent moves to the next
|
|
35
|
+
model in its list.
|
|
34
36
|
- **Live apply.** The router changes the models in the running OpenCode session. You do
|
|
35
37
|
not have to restart OpenCode after a refresh.
|
|
36
38
|
- **Clear results.** A scorecard shows each model and its scores. With `--verbose`, it
|
|
@@ -92,7 +94,7 @@ See [Update the router](./USAGE.md#12-update-the-router).
|
|
|
92
94
|
| Command | What it does |
|
|
93
95
|
| :-- | :-- |
|
|
94
96
|
| `/refresh-routing` | Calculate the routing table again, apply it, and show the scorecard |
|
|
95
|
-
| `/routing-status` | Show the model in use for each agent (after failover) and
|
|
97
|
+
| `/routing-status` | Show the model in use for each agent (after failover) and the estimated use of the Go allowance |
|
|
96
98
|
| `/routing-plan go-plus` | Save a plan profile (`go`, `go-plus`, or `free`) and apply it immediately |
|
|
97
99
|
| `/routing-eval` | Send one test prompt to the `build` models and write a report |
|
|
98
100
|
| `/routing-update` | Look for a newer version and remove the old copy that OpenCode keeps. Then restart OpenCode |
|
|
@@ -150,8 +152,8 @@ or a value that is not valid, the router ignores it and shows a note in the scor
|
|
|
150
152
|
| Key | Default | Meaning |
|
|
151
153
|
| :-- | :-- | :-- |
|
|
152
154
|
| `plan` | detected from your catalog | `go`, `go-plus`, or `free` |
|
|
153
|
-
| `chainDepth` | 4 for agents, 3 for helpers | Number of models in each failover list. `roles.<role>.chainDepth` wins over it |
|
|
154
|
-
| `softThreshold` | `0.85` |
|
|
155
|
+
| `chainDepth` | 4 for agents, 3 for helpers | Number of models in each failover list, including the free model at the end. `roles.<role>.chainDepth` wins over it |
|
|
156
|
+
| `softThreshold` | `0.85` | Move the agents to their free model when the use of the shared Go allowance reaches this part of a window's limit (0.85 = 85%) |
|
|
155
157
|
| `privacy.freeModelsOk` | `true` | `false` keeps `plan`, `build`, and `general` off free models |
|
|
156
158
|
| `denyModels` | `[]` | Model IDs to never use. `*` and `?` wildcards are permitted |
|
|
157
159
|
| `allowModels` | `[]` | If not empty, use only these model IDs |
|
|
@@ -190,7 +192,7 @@ score = w_intelligence * intelligence
|
|
|
190
192
|
|
|
191
193
|
| File | Contents |
|
|
192
194
|
| :-- | :-- |
|
|
193
|
-
| `~/.config/opencode/router/ledger.json` | Spend and tokens per model
|
|
195
|
+
| `~/.config/opencode/router/ledger.json` | Spend and tokens per model, the use of the shared Go allowance in the last 30 days, and cooldowns |
|
|
194
196
|
| `~/.config/opencode/router/routing-table.json` | The last routing table |
|
|
195
197
|
| `~/.config/opencode/router/last-apply.json` | The result of the last live apply |
|
|
196
198
|
| `~/.config/opencode/router/eval-report.md` | The report of the last `/routing-eval` |
|
|
@@ -205,6 +207,8 @@ If `XDG_CONFIG_HOME` is set, the router uses `$XDG_CONFIG_HOME/opencode` in plac
|
|
|
205
207
|
Models with `-free` in their name can collect your prompts for training. The scorecard
|
|
206
208
|
marks them `free*`. To keep your main work off these models, set
|
|
207
209
|
`"privacy": { "freeModelsOk": false }`. To never use them, set `"denyModels": ["*-free"]`.
|
|
210
|
+
With either setting, the agents have no free model to move to, so they stop when the Go
|
|
211
|
+
allowance runs out.
|
|
208
212
|
|
|
209
213
|
Once each 24 hours, the router sends one request to `registry.npmjs.org` to get the
|
|
210
214
|
newest version number of the router. It sends no other data. To stop this, set
|
package/USAGE.md
CHANGED
|
@@ -157,7 +157,9 @@ message automatically.
|
|
|
157
157
|
## 4. Read the scorecard
|
|
158
158
|
|
|
159
159
|
The scorecard has one block of rows for each role. The first row of a block is the model
|
|
160
|
-
that the role uses. The other rows are the failover models, in sequence.
|
|
160
|
+
that the role uses. The other rows are the failover models, in sequence. On Go and Go
|
|
161
|
+
Plus, the last row of each agent is a free model (see
|
|
162
|
+
[How the router changes models](#how-the-router-changes-models)).
|
|
161
163
|
|
|
162
164
|
| Column | Meaning |
|
|
163
165
|
| :-- | :-- |
|
|
@@ -168,13 +170,38 @@ that the role uses. The other rows are the failover models, in sequence.
|
|
|
168
170
|
| Score | The total score, 0 to 100 |
|
|
169
171
|
| Intel | Intelligence score, 0 to 100 |
|
|
170
172
|
| Speed | Speed score, 0 to 100 |
|
|
171
|
-
| Endur | Endurance score, 0 to 100. A high value means that the model
|
|
172
|
-
| Tokens/5h | The estimated number of tokens
|
|
173
|
-
| Cap/mo | The monthly
|
|
173
|
+
| Endur | Endurance score, 0 to 100. A high value means that the model uses the Go allowance slowly |
|
|
174
|
+
| Tokens/5h | The estimated number of tokens in 5 hours if the agent uses only this model. `unlimited` means no limit |
|
|
175
|
+
| Cap/mo | The monthly limit of the model on your plan. `inf` means no limit |
|
|
174
176
|
| Class | `go` = included in your Go subscription. `free*` = free (your data can be collected). `paid` = Zen pay-as-you-go |
|
|
175
177
|
|
|
176
178
|
The **Notes** section under the table shows problems, for example a missing catalog.
|
|
177
179
|
|
|
180
|
+
### How the router changes models
|
|
181
|
+
|
|
182
|
+
- All `go` models share one usage allowance. Each message uses a part of it: the cost of
|
|
183
|
+
the message divided by the monthly limit of its model (`Cap/mo`). A model with a low
|
|
184
|
+
limit uses the allowance faster.
|
|
185
|
+
- The allowance has 3 windows: 5 hours (20% of the monthly allowance), 7 days (50%), and
|
|
186
|
+
30 days (100%). The router estimates the use in each window from the messages that it
|
|
187
|
+
records.
|
|
188
|
+
- When the use in a window reaches `softThreshold` (default 85%), the `plan`, `build`,
|
|
189
|
+
`general`, and `explore` agents use the free model at the end of their list. When the
|
|
190
|
+
use falls under the threshold again, they go back to their first model.
|
|
191
|
+
- After a usage-limit error from a `go` model, the router stops all `go` models for
|
|
192
|
+
1 hour. The agents use their free models in that time.
|
|
193
|
+
- After a different rate-limit or overload error, the router stops only that model for
|
|
194
|
+
1 hour. The agent uses the next model in its list.
|
|
195
|
+
|
|
196
|
+
`/routing-status` shows the estimated use of the allowance and the state of each agent:
|
|
197
|
+
|
|
198
|
+
| State | Meaning |
|
|
199
|
+
| :-- | :-- |
|
|
200
|
+
| `healthy` | The agent uses its first model |
|
|
201
|
+
| `failover` | The agent uses a later model, because an earlier model is in cooldown |
|
|
202
|
+
| `free-fallback` | The agent uses its free model, because the Go allowance is near its limit or in cooldown |
|
|
203
|
+
| `limited` | The agent has no usable model. The router keeps the first model |
|
|
204
|
+
|
|
178
205
|
## 5. Select your plan
|
|
179
206
|
|
|
180
207
|
The router finds your plan from the models in your catalog. If it selects the wrong plan,
|
|
@@ -220,6 +247,9 @@ use free models.
|
|
|
220
247
|
}
|
|
221
248
|
```
|
|
222
249
|
|
|
250
|
+
With this setting, these agents have no free model at the end of their list. When the
|
|
251
|
+
Go allowance runs out, they stop until it resets.
|
|
252
|
+
|
|
223
253
|
### Never use some models
|
|
224
254
|
|
|
225
255
|
`*` matches any text. `?` matches one character.
|
|
@@ -242,8 +272,9 @@ If `allowModels` is not empty, the router uses only the models in the list.
|
|
|
242
272
|
|
|
243
273
|
### Change models earlier or later
|
|
244
274
|
|
|
245
|
-
`softThreshold` sets when the
|
|
246
|
-
the 5
|
|
275
|
+
`softThreshold` sets when the agents change to their free model. `0.7` changes when the
|
|
276
|
+
use of the shared Go allowance reaches 70% in one of its windows (5 hours, 7 days, or
|
|
277
|
+
30 days). The default is `0.85`.
|
|
247
278
|
|
|
248
279
|
```json
|
|
249
280
|
{
|
|
@@ -280,7 +311,8 @@ Use this setting when you know that a model is better or worse than its score.
|
|
|
280
311
|
|
|
281
312
|
`chainDepth` sets the number of models in each failover list. The default is 4 for the
|
|
282
313
|
agents and 3 for the internal helpers. A value under `roles.<role>` wins over the
|
|
283
|
-
top-level value.
|
|
314
|
+
top-level value. On Go and Go Plus, the last model of each list is a free model, if your
|
|
315
|
+
privacy setting permits free models.
|
|
284
316
|
|
|
285
317
|
```json
|
|
286
318
|
{
|
|
@@ -381,7 +413,7 @@ These commands work in PowerShell, CMD, Git Bash, macOS, and Linux.
|
|
|
381
413
|
| `opencode-router refresh --scope project` | Write the routing table to `opencode.json` in the current folder |
|
|
382
414
|
| `opencode-router refresh --plan free` | Use the `free` profile for this run only |
|
|
383
415
|
| `opencode-router refresh --json` | Print the result as JSON |
|
|
384
|
-
| `opencode-router status` | Show the model in use for each role (after failover) and
|
|
416
|
+
| `opencode-router status` | Show the model in use for each role (after failover) and the estimated use of the Go allowance |
|
|
385
417
|
| `opencode-router plan go-plus --scope global` | Save the plan in the global settings, then calculate and write the routing table |
|
|
386
418
|
| `opencode-router cat` | Show the catalog path, its date, and the number of models |
|
|
387
419
|
| `opencode-router install --scope global` | Write the slash commands as files in `~/.config/opencode/command/`. If `router.config.json` does not exist, it also writes an empty one |
|
|
@@ -462,7 +494,7 @@ The router writes these files:
|
|
|
462
494
|
|
|
463
495
|
| File | Written by | Contents |
|
|
464
496
|
| :-- | :-- | :-- |
|
|
465
|
-
| `~/.config/opencode/router/ledger.json` | Plugin | Spend and tokens per model
|
|
497
|
+
| `~/.config/opencode/router/ledger.json` | Plugin | Spend and tokens per model, the use of the shared Go allowance in the last 30 days, and cooldowns |
|
|
466
498
|
| `~/.config/opencode/router/routing-table.json` | Plugin | The last routing table |
|
|
467
499
|
| `~/.config/opencode/router/last-apply.json` | Plugin | The result of the last live apply |
|
|
468
500
|
| `~/.config/opencode/router/eval-report.md` | `/routing-eval` | The test report |
|
|
@@ -493,11 +525,12 @@ Do not commit the `router/` folder. It contains your usage data.
|
|
|
493
525
|
| "live PATCH not attempted (no OPENCODE_SERVER_URL set)" | Normal when you run `refresh` in a terminal | No action. Or see [Apply to a running OpenCode server](#apply-to-a-running-opencode-server) |
|
|
494
526
|
| "hot-apply failed: ..." | OpenCode did not accept the change | Restart OpenCode. The file `opencode.json` has the new models |
|
|
495
527
|
| A model that you selected by hand changes back | The router selects the model at each message | See [Known limits](#11-known-limits) |
|
|
528
|
+
| An agent uses a free model (`free-fallback` in `/routing-status`) | The Go allowance is near its limit, or a usage-limit error occurred | No action. The agent goes back to its first model when the use falls or the cooldown ends. To change the limit, set [`softThreshold`](#change-models-earlier-or-later) |
|
|
496
529
|
| The scores look wrong | A model has no entry in `model-scores.json` | Run `npx @s-berdia/opencode-router refresh --dry-run --verbose`. If the source is `heuristic` or `default`, add the model to `scores` |
|
|
497
530
|
|
|
498
531
|
## 11. Known limits
|
|
499
532
|
|
|
500
|
-
These are the limits of version 1.
|
|
533
|
+
These are the limits of version 1.2.0.
|
|
501
534
|
|
|
502
535
|
1. The router sets the model of the `plan`, `build`, `general`, and `explore` agents at
|
|
503
536
|
each message. If you select a different model by hand, the router replaces it. To
|
|
@@ -507,12 +540,18 @@ These are the limits of version 1.1.1.
|
|
|
507
540
|
3. The slash commands always write the global files (`~/.config/opencode/`). To write the
|
|
508
541
|
files of one project, use the terminal tool with `--scope project`.
|
|
509
542
|
4. The slash commands need Node.js 20 or later on the `PATH`.
|
|
510
|
-
5. Use numbers are estimates. The router
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
543
|
+
5. Use numbers are estimates. The router counts only the messages that it records. Use
|
|
544
|
+
from another computer is not included. After an update from version 1.1 or earlier,
|
|
545
|
+
the estimate starts at 0%. If two OpenCode windows write at the same moment, one
|
|
546
|
+
record can be lost.
|
|
547
|
+
6. The router treats the 3 windows as rolling windows. If OpenCode resets a window at a
|
|
548
|
+
fixed time, the estimate can differ from the console.
|
|
549
|
+
7. After a usage-limit error, all `go` models go into a 1-hour cooldown. After a
|
|
550
|
+
different rate-limit or overload error, only that model goes into a cooldown.
|
|
551
|
+
8. With `"privacy": { "freeModelsOk": false }`, the `plan`, `build`, and `general` agents
|
|
552
|
+
have no free model. When the Go allowance runs out, they stop until it resets.
|
|
553
|
+
9. The router does not know about **Extra usage** in the OpenCode console. It moves the
|
|
554
|
+
agents to free models at the threshold, also when Extra usage is on.
|
|
516
555
|
|
|
517
556
|
## 12. Update the router
|
|
518
557
|
|
|
@@ -559,7 +598,7 @@ To select the version yourself, write it in the `plugin` list:
|
|
|
559
598
|
|
|
560
599
|
```json
|
|
561
600
|
{
|
|
562
|
-
"plugin": ["@s-berdia/opencode-router@1.
|
|
601
|
+
"plugin": ["@s-berdia/opencode-router@1.2.0"]
|
|
563
602
|
}
|
|
564
603
|
```
|
|
565
604
|
|
package/dist/bin.js
CHANGED
|
@@ -1657,6 +1657,7 @@ function catalogMeta(file = catalogCacheFile()) {
|
|
|
1657
1657
|
|
|
1658
1658
|
// src/scoring.ts
|
|
1659
1659
|
var FIVE_HOUR_SHARE = 0.2;
|
|
1660
|
+
var WEEK_SHARE = 0.5;
|
|
1660
1661
|
var FREE_ROLE_BONUS = 6;
|
|
1661
1662
|
var DEFAULT_UNVETTED_PENALTY = 8;
|
|
1662
1663
|
function modelClass(model) {
|
|
@@ -2169,6 +2170,13 @@ function rankRole(catalog, roleName, table, cfg, feeds) {
|
|
|
2169
2170
|
chosen.push(entry);
|
|
2170
2171
|
}
|
|
2171
2172
|
}
|
|
2173
|
+
if (cfg.plan !== "free" && !chosen.some((entry) => entry.model.free)) {
|
|
2174
|
+
const free = scored.find((entry) => entry.model.free);
|
|
2175
|
+
if (free) {
|
|
2176
|
+
if (chosen.length >= depth && chosen.length > 1) chosen.pop();
|
|
2177
|
+
chosen.push(free);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2172
2180
|
return chosen.map((entry, index) => ({
|
|
2173
2181
|
rank: index + 1,
|
|
2174
2182
|
providerID: entry.model.providerID,
|
|
@@ -2230,27 +2238,27 @@ async function buildRoutingTable(opts = {}) {
|
|
|
2230
2238
|
};
|
|
2231
2239
|
return { table, cfg, scoreTable, catalog };
|
|
2232
2240
|
}
|
|
2233
|
-
function
|
|
2234
|
-
return ROLE_NAMES.flatMap((role) => table.active[role] ?? []);
|
|
2235
|
-
}
|
|
2236
|
-
function pickForRole(table, role, isHealthy) {
|
|
2241
|
+
function pickForRole(table, role, isUsable) {
|
|
2237
2242
|
const chain = table.active[role];
|
|
2238
2243
|
if (!chain || chain.length === 0) return void 0;
|
|
2239
|
-
return chain.find(
|
|
2244
|
+
return chain.find(isUsable) ?? chain[0];
|
|
2240
2245
|
}
|
|
2241
2246
|
function healthCheck(result, ledger) {
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
function usagePercent(result, ledger, modelId) {
|
|
2245
|
-
return ledger.usage(modelId, monthlyCapFor(modelId, result.scoreTable, result.cfg.plan)).pct5h * 100;
|
|
2247
|
+
const allowanceOk = ledger.allowanceAvailable(result.cfg.softThreshold);
|
|
2248
|
+
return (entry) => !ledger.inCooldown(entry.id) && (allowanceOk || entry.class !== "subscription");
|
|
2246
2249
|
}
|
|
2247
2250
|
function statusRows(result, ledger) {
|
|
2248
|
-
const
|
|
2251
|
+
const isUsable = healthCheck(result, ledger);
|
|
2252
|
+
const allowanceOk = ledger.allowanceAvailable(result.cfg.softThreshold);
|
|
2249
2253
|
return ROLE_NAMES.map((role) => {
|
|
2250
|
-
const entry = isAgentRole(role) ? pickForRole(result.table, role,
|
|
2251
|
-
if (!entry) return { role,
|
|
2252
|
-
const
|
|
2253
|
-
|
|
2254
|
+
const entry = isAgentRole(role) ? pickForRole(result.table, role, isUsable) : result.table.active[role]?.[0];
|
|
2255
|
+
if (!entry) return { role, state: "none" };
|
|
2256
|
+
const first = result.table.active[role]?.[0];
|
|
2257
|
+
let state = "healthy";
|
|
2258
|
+
if (!isUsable(entry)) state = "limited";
|
|
2259
|
+
else if (entry.rank > 1 && entry.class === "free" && first?.class === "subscription" && !allowanceOk) state = "free-fallback";
|
|
2260
|
+
else if (entry.rank > 1) state = "failover";
|
|
2261
|
+
return { role, entry, state };
|
|
2254
2262
|
});
|
|
2255
2263
|
}
|
|
2256
2264
|
|
|
@@ -2276,6 +2284,11 @@ function formatScorecard(table, opts = {}) {
|
|
|
2276
2284
|
lines.push(`- Plan: **${table.plan}**`);
|
|
2277
2285
|
lines.push(`- Updated: ${when} (UTC)`);
|
|
2278
2286
|
lines.push(`- Legend: go = Go subscription, free* = $0 (data may be collected), paid = Zen pay-as-you-go`);
|
|
2287
|
+
if (table.plan !== "free") {
|
|
2288
|
+
lines.push(
|
|
2289
|
+
"- All `go` models share one usage allowance. When it is near its limit, each agent uses the free model at the end of its list."
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2279
2292
|
lines.push("");
|
|
2280
2293
|
lines.push("| Role | # | Model | Var | Score | Intel | Speed | Endur | Tokens/5h | Cap/mo | Class |");
|
|
2281
2294
|
lines.push("| :-- | --: | :-- | :-- | --: | --: | --: | --: | --: | --: | :-- |");
|
|
@@ -2305,23 +2318,37 @@ function formatScorecard(table, opts = {}) {
|
|
|
2305
2318
|
);
|
|
2306
2319
|
return lines.join("\n");
|
|
2307
2320
|
}
|
|
2308
|
-
function
|
|
2321
|
+
function percent(part) {
|
|
2322
|
+
return `${(part * 100).toFixed(0)}%`;
|
|
2323
|
+
}
|
|
2324
|
+
function formatStatus(table, rows, allowance) {
|
|
2309
2325
|
const lines = [];
|
|
2310
2326
|
lines.push(`## opencode-router status`);
|
|
2311
2327
|
lines.push("");
|
|
2312
2328
|
lines.push(`- Plan: **${table.plan}**`);
|
|
2329
|
+
if (allowance) {
|
|
2330
|
+
const { usage } = allowance;
|
|
2331
|
+
lines.push(
|
|
2332
|
+
`- Go allowance used (estimate): 5 hours ${percent(usage.pct5h)}, 7 days ${percent(usage.pctWeek)}, 30 days ${percent(usage.pctMonth)}`
|
|
2333
|
+
);
|
|
2334
|
+
if (usage.cooldownUntil !== null) {
|
|
2335
|
+
const until = new Date(usage.cooldownUntil).toISOString().slice(11, 16);
|
|
2336
|
+
lines.push(`- Go allowance cooldown until ${until} UTC (${usage.cooldownReason ?? "usage limit"})`);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
const freeFallback = allowance ? `free-fallback = a free model, because the Go allowance is at ${percent(allowance.softThreshold)} or more or in cooldown; ` : "";
|
|
2313
2340
|
lines.push(
|
|
2314
|
-
`- State: healthy = first model; failover = a later model, because
|
|
2341
|
+
`- State: healthy = first model; failover = a later model, because an earlier model is in cooldown; ${freeFallback}limited = no usable model`
|
|
2315
2342
|
);
|
|
2316
2343
|
lines.push("");
|
|
2317
|
-
lines.push("| Role | Model in use | Rank |
|
|
2318
|
-
lines.push("| :-- | :-- | --: |
|
|
2344
|
+
lines.push("| Role | Model in use | Rank | State |");
|
|
2345
|
+
lines.push("| :-- | :-- | --: | :-- |");
|
|
2319
2346
|
for (const row of rows) {
|
|
2320
2347
|
if (!row.entry) {
|
|
2321
|
-
lines.push(`| ${row.role} | _none_ | - | -
|
|
2348
|
+
lines.push(`| ${row.role} | _none_ | - | - |`);
|
|
2322
2349
|
continue;
|
|
2323
2350
|
}
|
|
2324
|
-
lines.push(`| ${row.role} | \`${row.entry.id}\` | ${row.entry.rank} | ${row.
|
|
2351
|
+
lines.push(`| ${row.role} | \`${row.entry.id}\` | ${row.entry.rank} | ${row.state} |`);
|
|
2325
2352
|
}
|
|
2326
2353
|
return lines.join("\n");
|
|
2327
2354
|
}
|
|
@@ -2399,7 +2426,6 @@ var DAY = 24 * HOUR;
|
|
|
2399
2426
|
var WINDOW_5H = 5 * HOUR;
|
|
2400
2427
|
var WINDOW_WEEK = 7 * DAY;
|
|
2401
2428
|
var WINDOW_MONTH = 30 * DAY;
|
|
2402
|
-
var MONTHLY_LIMIT = 0.95;
|
|
2403
2429
|
function emptyState(now) {
|
|
2404
2430
|
return {
|
|
2405
2431
|
cost5h: 0,
|
|
@@ -2482,7 +2508,8 @@ var LedgerStore = class {
|
|
|
2482
2508
|
}
|
|
2483
2509
|
return this.roll(entry, now);
|
|
2484
2510
|
}
|
|
2485
|
-
|
|
2511
|
+
/** `allowanceShare`: the cost divided by the model's monthly limit; 0 for a model outside the Go allowance. */
|
|
2512
|
+
record(modelId, costUSD, tokens, allowanceShare = 0) {
|
|
2486
2513
|
const state = this.state(modelId);
|
|
2487
2514
|
state.cost5h += costUSD;
|
|
2488
2515
|
state.costWeek += costUSD;
|
|
@@ -2491,8 +2518,23 @@ var LedgerStore = class {
|
|
|
2491
2518
|
state.tokensWeek += tokens;
|
|
2492
2519
|
state.tokensMonth += tokens;
|
|
2493
2520
|
state.lastUsedAt = this.now();
|
|
2521
|
+
if (allowanceShare > 0) this.addShare(allowanceShare);
|
|
2494
2522
|
this.save();
|
|
2495
2523
|
}
|
|
2524
|
+
/** Call after sync() or state(), never before: a reload would drop changes to the old data. */
|
|
2525
|
+
sharedAllowance() {
|
|
2526
|
+
if (!this.data.allowance || !Array.isArray(this.data.allowance.buckets)) this.data.allowance = { buckets: [] };
|
|
2527
|
+
return this.data.allowance;
|
|
2528
|
+
}
|
|
2529
|
+
addShare(share) {
|
|
2530
|
+
const allowance = this.sharedAllowance();
|
|
2531
|
+
const now = this.now();
|
|
2532
|
+
const hour = Math.floor(now / HOUR) * HOUR;
|
|
2533
|
+
allowance.buckets = allowance.buckets.filter(([start]) => start + HOUR > now - WINDOW_MONTH);
|
|
2534
|
+
const bucket = allowance.buckets.find(([start]) => start === hour);
|
|
2535
|
+
if (bucket) bucket[1] += share;
|
|
2536
|
+
else allowance.buckets.push([hour, share]);
|
|
2537
|
+
}
|
|
2496
2538
|
cooldown(modelId, durationMs, reason) {
|
|
2497
2539
|
const state = this.state(modelId);
|
|
2498
2540
|
state.cooldownUntil = this.now() + durationMs;
|
|
@@ -2504,15 +2546,46 @@ var LedgerStore = class {
|
|
|
2504
2546
|
state.cooldownUntil = void 0;
|
|
2505
2547
|
this.save();
|
|
2506
2548
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2549
|
+
inCooldown(modelId) {
|
|
2550
|
+
const until = this.state(modelId).cooldownUntil;
|
|
2551
|
+
return until !== void 0 && this.now() < until;
|
|
2509
2552
|
}
|
|
2510
|
-
/**
|
|
2511
|
-
|
|
2553
|
+
/** After a usage-limit error: every model that uses the shared allowance is limited. */
|
|
2554
|
+
cooldownAllowance(durationMs, reason) {
|
|
2555
|
+
this.sync();
|
|
2556
|
+
const allowance = this.sharedAllowance();
|
|
2557
|
+
allowance.cooldownUntil = this.now() + durationMs;
|
|
2558
|
+
if (reason) allowance.lastError = reason;
|
|
2559
|
+
this.save();
|
|
2560
|
+
}
|
|
2561
|
+
usage(modelId) {
|
|
2512
2562
|
const state = this.state(modelId);
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2563
|
+
return {
|
|
2564
|
+
cost5h: state.cost5h,
|
|
2565
|
+
costMonth: state.costMonth,
|
|
2566
|
+
tokens5h: state.tokens5h,
|
|
2567
|
+
tokensMonth: state.tokensMonth,
|
|
2568
|
+
cooldownUntil: state.cooldownUntil ?? null
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
/** Rolling use of the shared allowance in the last 5 hours, 7 days, and 30 days. */
|
|
2572
|
+
allowance() {
|
|
2573
|
+
this.sync();
|
|
2574
|
+
const allowance = this.sharedAllowance();
|
|
2575
|
+
const now = this.now();
|
|
2576
|
+
const cooldownUntil = allowance.cooldownUntil !== void 0 && now < allowance.cooldownUntil ? allowance.cooldownUntil : null;
|
|
2577
|
+
return {
|
|
2578
|
+
pct5h: windowShare(allowance.buckets, now, WINDOW_5H) / FIVE_HOUR_SHARE,
|
|
2579
|
+
pctWeek: windowShare(allowance.buckets, now, WINDOW_WEEK) / WEEK_SHARE,
|
|
2580
|
+
pctMonth: windowShare(allowance.buckets, now, WINDOW_MONTH),
|
|
2581
|
+
cooldownUntil,
|
|
2582
|
+
cooldownReason: cooldownUntil === null ? null : allowance.lastError ?? null
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
/** True when no window has reached `softThreshold` and no usage-limit cooldown is active. */
|
|
2586
|
+
allowanceAvailable(softThreshold) {
|
|
2587
|
+
const u = this.allowance();
|
|
2588
|
+
return u.cooldownUntil === null && u.pct5h < softThreshold && u.pctWeek < softThreshold && u.pctMonth < softThreshold;
|
|
2516
2589
|
}
|
|
2517
2590
|
snapshot() {
|
|
2518
2591
|
return structuredClone(this.data);
|
|
@@ -2523,17 +2596,16 @@ var LedgerStore = class {
|
|
|
2523
2596
|
this.loadedMtime = mtimeOf(this.file);
|
|
2524
2597
|
}
|
|
2525
2598
|
};
|
|
2526
|
-
function
|
|
2527
|
-
const
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
};
|
|
2599
|
+
function windowShare(buckets, now, windowMs) {
|
|
2600
|
+
const from = now - windowMs;
|
|
2601
|
+
let sum = 0;
|
|
2602
|
+
for (const [start, share] of buckets) {
|
|
2603
|
+
const end = Math.min(start + HOUR, now);
|
|
2604
|
+
if (end <= from || start > now) continue;
|
|
2605
|
+
const span = end - start;
|
|
2606
|
+
sum += span <= 0 ? share : share * (end - Math.max(start, from)) / span;
|
|
2607
|
+
}
|
|
2608
|
+
return sum;
|
|
2537
2609
|
}
|
|
2538
2610
|
|
|
2539
2611
|
// src/install.ts
|
|
@@ -2959,12 +3031,12 @@ async function cmdStatus(opts) {
|
|
|
2959
3031
|
const result = await build(opts);
|
|
2960
3032
|
const ledger = new LedgerStore(ledgerFile());
|
|
2961
3033
|
const rows = statusRows(result, ledger);
|
|
3034
|
+
const allowance = result.cfg.plan === "free" ? void 0 : ledger.allowance();
|
|
2962
3035
|
if (opts.json) {
|
|
2963
|
-
|
|
2964
|
-
for (const entry of activeEntries(result.table)) usage[entry.id] = { pct: usagePercent(result, ledger, entry.id) };
|
|
2965
|
-
process.stdout.write(JSON.stringify({ table: result.table, rows, usage }, null, 2) + "\n");
|
|
3036
|
+
process.stdout.write(JSON.stringify({ table: result.table, rows, allowance: allowance ?? null }, null, 2) + "\n");
|
|
2966
3037
|
} else {
|
|
2967
|
-
|
|
3038
|
+
const status = allowance && { usage: allowance, softThreshold: result.cfg.softThreshold };
|
|
3039
|
+
process.stdout.write(formatStatus(result.table, rows, status) + "\n");
|
|
2968
3040
|
await writeUpdateNotice(opts);
|
|
2969
3041
|
}
|
|
2970
3042
|
return 0;
|