opencode-usage-report 0.6.1 → 0.6.3
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 +12 -0
- package/package.json +7 -3
- package/src/context/dialog.tsx +2 -2
- package/src/context/system.ts +37 -1
- package/src/providers/kimi.ts +16 -7
- package/src/v2-tui.ts +12 -4
package/README.md
CHANGED
|
@@ -302,6 +302,18 @@ so a failing check blocks the stage.
|
|
|
302
302
|
|
|
303
303
|
## Changelog
|
|
304
304
|
|
|
305
|
+
- **0.6.3** — Kimi Code CN (`kimi-code-plan-cn`): the 5-hour window falls back
|
|
306
|
+
to the `usages.limit_5h` ratio when the `limits[]` row carries no counts,
|
|
307
|
+
instead of rendering an empty `—` bar; a monthly window is also read from a
|
|
308
|
+
counted 30-day `limits[]` entry when the endpoint reports it there. Includes
|
|
309
|
+
the outstanding Dependabot dev-dependency bumps.
|
|
310
|
+
- **0.6.2** — OpenCode 2 context panel fixes: the V2 adapter now reads the
|
|
311
|
+
model id from the store's `model.id`, so the model lookup succeeds and the
|
|
312
|
+
cell grid, per-category percentages and compaction-headroom line render
|
|
313
|
+
again instead of `—`; the translated messages also carry the session id, and
|
|
314
|
+
the system-prompt capture anchors its freshness check to the turn's user
|
|
315
|
+
message — OpenCode 2 fires the capture hook just before the assistant
|
|
316
|
+
message is stamped, which previously marked every capture `(derived)`.
|
|
305
317
|
- **0.6.1** — Kimi Code adapter tracks the 2026-09 quota-model API change:
|
|
306
318
|
usage windows now come from the `usages.limit_5h` / `limit_7d` /
|
|
307
319
|
`limit_month_total` ratios (with a `limit_month_code` breakdown), and Extra
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-report",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "opencode plugin: /usage command showing subscription quota windows for configured providers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kimi",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@opencode-ai/plugin": "^1.0.0",
|
|
51
|
-
"@opentui/core": "^0.
|
|
51
|
+
"@opentui/core": "^0.6.2",
|
|
52
52
|
"@opentui/solid": "^0.5.11",
|
|
53
53
|
"@types/node": "^26.6.2",
|
|
54
|
-
"oxfmt": "^0.
|
|
54
|
+
"oxfmt": "^0.72.0",
|
|
55
55
|
"oxlint": "^1.85.0",
|
|
56
56
|
"solid-js": "1.9.12",
|
|
57
57
|
"tsx": "^4.19.0",
|
|
@@ -63,5 +63,9 @@
|
|
|
63
63
|
"@opentui/core": ">=0.5.11",
|
|
64
64
|
"@opentui/solid": ">=0.5.11",
|
|
65
65
|
"solid-js": ">=1.9.0"
|
|
66
|
+
},
|
|
67
|
+
"overrides": {
|
|
68
|
+
"seroval": "1.6.9",
|
|
69
|
+
"seroval-plugins": "1.6.9"
|
|
66
70
|
}
|
|
67
71
|
}
|
package/src/context/dialog.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import type { AssistantMessage, Model } from "@opencode-ai/sdk/v2";
|
|
|
4
4
|
import { For, Show, createMemo } from "solid-js";
|
|
5
5
|
import { buildContextLines, type ContextTone } from "./format";
|
|
6
6
|
import { collectContext } from "./collect";
|
|
7
|
-
import { captureIsFresh } from "./system";
|
|
7
|
+
import { captureIsFresh, captureSince } from "./system";
|
|
8
8
|
import type { ContextBreakdown, SystemCapture } from "./types";
|
|
9
9
|
import { buildLines } from "../quota-lines";
|
|
10
10
|
import type { ProviderReport } from "../types";
|
|
@@ -98,7 +98,7 @@ export function UsageDialog(props: UsageDialogProps) {
|
|
|
98
98
|
sessionID: last.sessionID,
|
|
99
99
|
providerID: last.providerID,
|
|
100
100
|
modelID: last.modelID,
|
|
101
|
-
since: last
|
|
101
|
+
since: captureSince(messages(), last),
|
|
102
102
|
});
|
|
103
103
|
return ok ? capture : null;
|
|
104
104
|
});
|
package/src/context/system.ts
CHANGED
|
@@ -110,6 +110,42 @@ export function captureIsFresh(
|
|
|
110
110
|
if (capture.modelID !== ref.modelID) return false;
|
|
111
111
|
const capturedAt = nonNegative(capture.capturedAt);
|
|
112
112
|
const since = nonNegative(ref.since);
|
|
113
|
-
|
|
113
|
+
// A zero boundary means the caller had no usable timestamp — treat it as
|
|
114
|
+
// unusable rather than "anything is fresh" (epoch 0 is never a real turn).
|
|
115
|
+
if (capturedAt === null || since === null || since === 0) return false;
|
|
114
116
|
return capturedAt >= since;
|
|
115
117
|
}
|
|
118
|
+
|
|
119
|
+
function messageCreated(value: unknown): number | null {
|
|
120
|
+
if (!isRecord(value)) return null;
|
|
121
|
+
const time = isRecord(value.time) ? value.time : {};
|
|
122
|
+
const created = nonNegative(time.created);
|
|
123
|
+
return created !== null && created > 0 ? created : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The `since` boundary for `captureIsFresh`: the creation time of the user
|
|
128
|
+
* message that started the turn `reference` belongs to, falling back to the
|
|
129
|
+
* reference's own creation time. Returns `0` when neither carries a usable
|
|
130
|
+
* timestamp, which `captureIsFresh` rejects.
|
|
131
|
+
*
|
|
132
|
+
* OpenCode 2 assembles the request — firing the system-transform hook — just
|
|
133
|
+
* before the assistant message row gets its `time.created`, so a capture from
|
|
134
|
+
* the current turn is a few milliseconds *older* than the message and the old
|
|
135
|
+
* `capturedAt >= message.created` boundary rejected every fresh capture. The
|
|
136
|
+
* turn's user message predates request assembly under both OpenCode 1 and 2,
|
|
137
|
+
* so it is an ordering-stable boundary; a capture written for an earlier turn
|
|
138
|
+
* still sits before it and is still rejected.
|
|
139
|
+
*/
|
|
140
|
+
export function captureSince(messages: readonly unknown[], reference: unknown): number {
|
|
141
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
142
|
+
const index = list.indexOf(reference);
|
|
143
|
+
const start = index === -1 ? list.length : index;
|
|
144
|
+
for (let i = start - 1; i >= 0; i--) {
|
|
145
|
+
const message = list[i];
|
|
146
|
+
if (!isRecord(message) || message.role !== "user") continue;
|
|
147
|
+
const created = messageCreated(message);
|
|
148
|
+
if (created !== null) return created;
|
|
149
|
+
}
|
|
150
|
+
return messageCreated(reference) ?? 0;
|
|
151
|
+
}
|
package/src/providers/kimi.ts
CHANGED
|
@@ -161,13 +161,18 @@ function parse(body: Record<string, unknown>, cred: Credential): AdapterResult {
|
|
|
161
161
|
|
|
162
162
|
// Count-based windows are authoritative when present; the `usages.limit_*`
|
|
163
163
|
// ratios are the primary wire on the post-2026-09 quota model, where the
|
|
164
|
-
// absolute `usage`/`limits[]` rows were removed.
|
|
164
|
+
// absolute `usage`/`limits[]` rows were removed. Some regional responses
|
|
165
|
+
// send a limits[] 5h row with no counts, so fall back to its ratio instead
|
|
166
|
+
// of rendering an empty bar.
|
|
165
167
|
const detail5h = findLimitDetail(limits, 300);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
const limit5h = pick(usages, "limit_5h");
|
|
169
|
+
const fiveHour = detail5h === undefined ? undefined : detailWindow("5h", "5-hour", detail5h);
|
|
170
|
+
if (fiveHour !== undefined && fiveHour.usedPercent !== null) {
|
|
171
|
+
windows.push(fiveHour);
|
|
172
|
+
} else if (limit5h !== undefined) {
|
|
173
|
+
windows.push(ratioWindow("5h", "5-hour", limit5h));
|
|
174
|
+
} else if (fiveHour !== undefined) {
|
|
175
|
+
windows.push(fiveHour);
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
if (usage !== undefined) {
|
|
@@ -179,12 +184,16 @@ function parse(body: Record<string, unknown>, cred: Credential): AdapterResult {
|
|
|
179
184
|
|
|
180
185
|
// The monthly total joined the quota model; `totalQuota` is the legacy shape.
|
|
181
186
|
// `limit_month_code` is the code-typed share the official client shows as a
|
|
182
|
-
// breakdown under the monthly row.
|
|
187
|
+
// breakdown under the monthly row. Some regional responses carry the monthly
|
|
188
|
+
// window as a counted 30-day entry in `limits[]` instead.
|
|
183
189
|
const monthTotal = pick(usages, "limit_month_total");
|
|
190
|
+
const detailMonth = findLimitDetail(limits, 43200);
|
|
184
191
|
if (monthTotal !== undefined) {
|
|
185
192
|
windows.push(ratioWindow("monthly", "Monthly", monthTotal));
|
|
186
193
|
const codePercent = ratioPercent(pick(usages, "limit_month_code"));
|
|
187
194
|
if (codePercent !== null) extras["Monthly code"] = `${codePercent}%`;
|
|
195
|
+
} else if (detailMonth !== undefined) {
|
|
196
|
+
windows.push(detailWindow("monthly", "Monthly", detailMonth));
|
|
188
197
|
} else {
|
|
189
198
|
const totalQuota = pick(body, "totalQuota", "total_quota");
|
|
190
199
|
const quotaUsed = toNumber(pick(totalQuota, "used"));
|
package/src/v2-tui.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface V2Route {
|
|
|
62
62
|
|
|
63
63
|
export interface V2ModelRef {
|
|
64
64
|
readonly providerID?: string;
|
|
65
|
+
/** The OpenCode 2 store spells the model id `id`; older mirrors used `modelID`. */
|
|
66
|
+
readonly id?: string;
|
|
65
67
|
readonly modelID?: string;
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -249,7 +251,10 @@ function createdAt(message: { readonly time?: { readonly created?: number } }):
|
|
|
249
251
|
* messages carry a single `text` and an optional file list, so those are
|
|
250
252
|
* synthesized into the same part shape.
|
|
251
253
|
*/
|
|
252
|
-
function translateMessage(
|
|
254
|
+
function translateMessage(
|
|
255
|
+
raw: unknown,
|
|
256
|
+
sessionID: string,
|
|
257
|
+
): {
|
|
253
258
|
record: Record<string, unknown>;
|
|
254
259
|
parts: Array<Record<string, unknown>>;
|
|
255
260
|
} {
|
|
@@ -264,9 +269,12 @@ function translateMessage(raw: unknown): {
|
|
|
264
269
|
return {
|
|
265
270
|
record: {
|
|
266
271
|
id,
|
|
272
|
+
sessionID,
|
|
267
273
|
role: "assistant",
|
|
268
274
|
providerID: safeString(model.providerID),
|
|
269
|
-
|
|
275
|
+
// The store sends `model.id` (e.g. `{ id: "k3", providerID, variant }`);
|
|
276
|
+
// `model.modelID` is kept as a fallback for older hosts and fixtures.
|
|
277
|
+
modelID: safeString(model.modelID) || safeString(model.id),
|
|
270
278
|
time: createdAt(message),
|
|
271
279
|
tokens: {
|
|
272
280
|
input: finiteOrNull(tokens.input) ?? 0,
|
|
@@ -294,7 +302,7 @@ function translateMessage(raw: unknown): {
|
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
return {
|
|
297
|
-
record: { id, role: "user", time: createdAt(message), tokens: { ...ZERO_TOKENS } },
|
|
305
|
+
record: { id, sessionID, role: "user", time: createdAt(message), tokens: { ...ZERO_TOKENS } },
|
|
298
306
|
parts,
|
|
299
307
|
};
|
|
300
308
|
}
|
|
@@ -362,7 +370,7 @@ export function createV2Api(ctx: V2Context): TuiPluginApi {
|
|
|
362
370
|
const messages: Array<Record<string, unknown>> = [];
|
|
363
371
|
partsById.clear();
|
|
364
372
|
for (const raw of list) {
|
|
365
|
-
const { record, parts } = translateMessage(raw);
|
|
373
|
+
const { record, parts } = translateMessage(raw, sessionID);
|
|
366
374
|
if (typeof record.id !== "string" || record.id === "") continue;
|
|
367
375
|
messages.push(record);
|
|
368
376
|
partsById.set(record.id, parts);
|