@leo000001/opencode-quota-sidebar 4.0.9 → 4.0.11
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 +504 -446
- package/README.zh-CN.md +516 -458
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +95 -40
- package/dist/cli_render.d.ts +4 -4
- package/dist/cli_render.js +74 -74
- package/dist/cost.d.ts +21 -4
- package/dist/cost.js +493 -264
- package/dist/format.d.ts +5 -5
- package/dist/format.js +288 -287
- package/dist/history_usage.d.ts +15 -9
- package/dist/history_usage.js +28 -22
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -34
- package/dist/models_dev_pricing.d.ts +6 -0
- package/dist/models_dev_pricing.js +226 -0
- package/dist/opencode_pricing.d.ts +14 -0
- package/dist/opencode_pricing.js +273 -0
- package/dist/storage.d.ts +3 -3
- package/dist/storage.js +27 -28
- package/dist/storage_parse.d.ts +1 -1
- package/dist/storage_parse.js +51 -45
- package/dist/storage_paths.d.ts +1 -0
- package/dist/storage_paths.js +26 -11
- package/dist/title_apply.d.ts +5 -22
- package/dist/title_apply.js +19 -61
- package/dist/tui.d.ts +1 -1
- package/dist/tui.tsx +481 -471
- package/dist/tui_helpers.d.ts +5 -3
- package/dist/tui_helpers.js +62 -34
- package/dist/types.d.ts +8 -10
- package/dist/usage.d.ts +9 -6
- package/dist/usage.js +27 -21
- package/dist/usage_service.d.ts +8 -7
- package/dist/usage_service.js +261 -150
- package/package.json +1 -1
package/dist/history_usage.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { AssistantMessage, Message } from
|
|
2
|
-
import { type HistoryPeriod, type PeriodRange, type SinceSpec } from
|
|
3
|
-
import { type UsageSummary } from
|
|
4
|
-
import type { CacheCoverageMode, CachedSessionUsage, IncrementalCursor } from
|
|
1
|
+
import type { AssistantMessage, Message } from "@opencode-ai/sdk";
|
|
2
|
+
import { type HistoryPeriod, type PeriodRange, type SinceSpec } from "./period.js";
|
|
3
|
+
import { type UsageSummary } from "./usage.js";
|
|
4
|
+
import type { CacheCoverageMode, CachedSessionUsage, IncrementalCursor } from "./types.js";
|
|
5
5
|
export type HistoryDialogRow = {
|
|
6
6
|
label: string;
|
|
7
7
|
isCurrent: boolean;
|
|
@@ -37,19 +37,21 @@ export type HistoryPersistenceHint = {
|
|
|
37
37
|
ranges: UsageSummary[];
|
|
38
38
|
totalUsage: UsageSummary;
|
|
39
39
|
fullUsage: UsageSummary | undefined;
|
|
40
|
+
pricingFingerprint?: string;
|
|
41
|
+
pricingKeys?: string[];
|
|
40
42
|
persist: boolean;
|
|
41
43
|
cursor: IncrementalCursor | undefined;
|
|
42
44
|
missing: boolean;
|
|
43
45
|
loadFailed: boolean;
|
|
44
46
|
};
|
|
45
47
|
export type LoadMessagesPageResult = {
|
|
46
|
-
status:
|
|
48
|
+
status: "ok";
|
|
47
49
|
entries: MessageEntry[];
|
|
48
50
|
nextBefore?: string;
|
|
49
51
|
} | {
|
|
50
|
-
status:
|
|
52
|
+
status: "missing";
|
|
51
53
|
} | {
|
|
52
|
-
status:
|
|
54
|
+
status: "error";
|
|
53
55
|
};
|
|
54
56
|
type SessionEntry = {
|
|
55
57
|
sessionID: string;
|
|
@@ -74,10 +76,14 @@ export type ComputeHistoryUsageDeps = {
|
|
|
74
76
|
classifyCacheMode: (message: AssistantMessage, modelCostMap: Record<string, unknown>) => CacheCoverageMode;
|
|
75
77
|
/** Check whether a set of entries has at least one resolvable API-cost message. */
|
|
76
78
|
hasResolvableApiCostMessages: (entries: MessageEntry[], modelCostMap: Record<string, unknown>) => boolean;
|
|
79
|
+
/** Build a pricing fingerprint from provider/model keys and current rates. */
|
|
80
|
+
pricingFingerprintForKeys: (pricingKeys: string[], modelCostMap: Record<string, unknown>) => string;
|
|
81
|
+
/** Whether cached usage still matches current billing + pricing semantics. */
|
|
82
|
+
isUsageBillingCurrent: (cached: CachedSessionUsage | undefined, modelCostMap: Record<string, unknown>) => boolean;
|
|
77
83
|
/** Whether the cached usage for a session needs a full recompute. */
|
|
78
|
-
shouldTrackFullUsage: (cached: CachedSessionUsage | undefined,
|
|
84
|
+
shouldTrackFullUsage: (cached: CachedSessionUsage | undefined, modelCostMap: Record<string, unknown>) => boolean;
|
|
79
85
|
/** Whether cached usage needs recompute (for persistence decision). */
|
|
80
|
-
shouldRecomputeUsageCache: (cached: CachedSessionUsage,
|
|
86
|
+
shouldRecomputeUsageCache: (cached: CachedSessionUsage, pricingFingerprint: string | undefined) => boolean;
|
|
81
87
|
throwOnLoadFailure?: boolean;
|
|
82
88
|
};
|
|
83
89
|
/**
|
package/dist/history_usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { parseSince, periodRanges, } from
|
|
2
|
-
import { mapConcurrent } from
|
|
3
|
-
import { accumulateMessagesAcrossCompletedRanges, accumulateMessagesInCompletedRange, emptyUsageSummary, fromCachedSessionUsage, mergeCursorFromEntries, mergeUsage,
|
|
1
|
+
import { parseSince, periodRanges, } from "./period.js";
|
|
2
|
+
import { mapConcurrent } from "./helpers.js";
|
|
3
|
+
import { accumulateMessagesAcrossCompletedRanges, accumulateMessagesInCompletedRange, emptyUsageSummary, fromCachedSessionUsage, mergeCursorFromEntries, mergeUsage, } from "./usage.js";
|
|
4
4
|
// ── Constants ──────────────────────────────────────────────────────────────
|
|
5
5
|
const RANGE_USAGE_CONCURRENCY = 5;
|
|
6
6
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
@@ -11,7 +11,7 @@ function filterRangeSessions(sessions, startAt, endAt) {
|
|
|
11
11
|
if (session.state.dirty === true)
|
|
12
12
|
return true;
|
|
13
13
|
const lastMessageTime = session.state.cursor?.lastMessageTime;
|
|
14
|
-
if (typeof lastMessageTime ===
|
|
14
|
+
if (typeof lastMessageTime === "number" &&
|
|
15
15
|
Number.isFinite(lastMessageTime) &&
|
|
16
16
|
lastMessageTime < startAt) {
|
|
17
17
|
return false;
|
|
@@ -24,13 +24,13 @@ function pageLatestTimestamp(entries) {
|
|
|
24
24
|
for (const entry of entries) {
|
|
25
25
|
const info = entry.info;
|
|
26
26
|
const completed = info.time?.completed;
|
|
27
|
-
if (typeof completed ===
|
|
27
|
+
if (typeof completed === "number" && Number.isFinite(completed)) {
|
|
28
28
|
if (completed > latest)
|
|
29
29
|
latest = completed;
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
32
|
const created = info.time?.created;
|
|
33
|
-
if (typeof created ===
|
|
33
|
+
if (typeof created === "number" && Number.isFinite(created)) {
|
|
34
34
|
if (created > latest)
|
|
35
35
|
latest = created;
|
|
36
36
|
}
|
|
@@ -55,19 +55,19 @@ function rangeIndexForTimestamp(ranges, timestamp) {
|
|
|
55
55
|
}
|
|
56
56
|
return -1;
|
|
57
57
|
}
|
|
58
|
-
function canUseCurrentSessionCache(cached, session, ranges) {
|
|
58
|
+
function canUseCurrentSessionCache(cached, session, ranges, deps, modelCostMap) {
|
|
59
59
|
if (!cached)
|
|
60
60
|
return undefined;
|
|
61
|
-
if (cached
|
|
61
|
+
if (!deps.isUsageBillingCurrent(cached, modelCostMap))
|
|
62
62
|
return undefined;
|
|
63
63
|
if (session.state.dirty === true)
|
|
64
64
|
return undefined;
|
|
65
65
|
const lastMessageTime = session.state.cursor?.lastMessageTime;
|
|
66
|
-
if (typeof lastMessageTime !==
|
|
66
|
+
if (typeof lastMessageTime !== "number" ||
|
|
67
67
|
!Number.isFinite(lastMessageTime)) {
|
|
68
68
|
return undefined;
|
|
69
69
|
}
|
|
70
|
-
if (typeof session.state.createdAt !==
|
|
70
|
+
if (typeof session.state.createdAt !== "number" ||
|
|
71
71
|
!Number.isFinite(session.state.createdAt)) {
|
|
72
72
|
return undefined;
|
|
73
73
|
}
|
|
@@ -107,13 +107,12 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
107
107
|
const endAt = ranges[ranges.length - 1].endAt;
|
|
108
108
|
const sessions = filterRangeSessions(deps.sessions, startAt, endAt);
|
|
109
109
|
const modelCostMap = await deps.getModelCostMap();
|
|
110
|
-
const hasPricing = Object.keys(modelCostMap).length > 0;
|
|
111
110
|
if (sessions.length > 0) {
|
|
112
111
|
const fetched = await mapConcurrent(sessions, RANGE_USAGE_CONCURRENCY, async (session) => {
|
|
113
112
|
const cachedHit = canUseCurrentSessionCache(session.state.usage, session, rows.map((row) => ({
|
|
114
113
|
startAt: row.range.startAt,
|
|
115
114
|
endAt: row.range.endAt,
|
|
116
|
-
})));
|
|
115
|
+
})), deps, modelCostMap);
|
|
117
116
|
if (cachedHit) {
|
|
118
117
|
const rangeUsage = rows.map(() => emptyUsageSummary());
|
|
119
118
|
rangeUsage[cachedHit.index] = cachedHit.usage;
|
|
@@ -137,14 +136,14 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
137
136
|
};
|
|
138
137
|
const rangeUsage = rows.map(() => emptyUsageSummary());
|
|
139
138
|
const totalUsage = emptyUsageSummary();
|
|
140
|
-
const trackFullUsage = deps.shouldTrackFullUsage(session.state.usage,
|
|
139
|
+
const trackFullUsage = deps.shouldTrackFullUsage(session.state.usage, modelCostMap);
|
|
141
140
|
const fullUsage = trackFullUsage ? emptyUsageSummary() : undefined;
|
|
142
141
|
let cursor;
|
|
143
|
-
|
|
142
|
+
const pricingKeys = new Set();
|
|
144
143
|
let before;
|
|
145
144
|
while (true) {
|
|
146
145
|
const load = await deps.loadMessagesPage(session.sessionID, before);
|
|
147
|
-
if (load.status !==
|
|
146
|
+
if (load.status !== "ok") {
|
|
148
147
|
return {
|
|
149
148
|
sessionID: session.sessionID,
|
|
150
149
|
dateKey: session.dateKey,
|
|
@@ -153,8 +152,8 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
153
152
|
ranges: rows.map(() => emptyUsageSummary()),
|
|
154
153
|
totalUsage: emptyUsageSummary(),
|
|
155
154
|
fullUsage: undefined,
|
|
156
|
-
loadFailed: load.status ===
|
|
157
|
-
missing: load.status ===
|
|
155
|
+
loadFailed: load.status === "error",
|
|
156
|
+
missing: load.status === "missing",
|
|
158
157
|
persist: false,
|
|
159
158
|
cursor: undefined,
|
|
160
159
|
};
|
|
@@ -169,9 +168,11 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
169
168
|
if (fullUsage) {
|
|
170
169
|
accumulateMessagesInCompletedRange(fullUsage, entries, 0, Number.POSITIVE_INFINITY, usageOptions);
|
|
171
170
|
cursor = mergeCursorFromEntries(cursor, entries);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
for (const { info } of entries) {
|
|
172
|
+
if (info.role !== "assistant")
|
|
173
|
+
continue;
|
|
174
|
+
pricingKeys.add(`${info.providerID}:${info.modelID}`);
|
|
175
|
+
}
|
|
175
176
|
}
|
|
176
177
|
// `session.messages(limit, before)` pages from newest to oldest.
|
|
177
178
|
// When we are only computing range usage (no full-session persistence),
|
|
@@ -195,9 +196,12 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
195
196
|
if (totalUsage.assistantMessages > 0) {
|
|
196
197
|
totalUsage.sessionCount = 1;
|
|
197
198
|
}
|
|
199
|
+
const pricingFingerprint = fullUsage
|
|
200
|
+
? deps.pricingFingerprintForKeys([...pricingKeys], modelCostMap)
|
|
201
|
+
: undefined;
|
|
198
202
|
const shouldPersist = !!fullUsage &&
|
|
199
203
|
(!session.state.usage ||
|
|
200
|
-
deps.shouldRecomputeUsageCache(session.state.usage,
|
|
204
|
+
deps.shouldRecomputeUsageCache(session.state.usage, pricingFingerprint));
|
|
201
205
|
return {
|
|
202
206
|
sessionID: session.sessionID,
|
|
203
207
|
dateKey: session.dateKey,
|
|
@@ -206,6 +210,8 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
206
210
|
ranges: rangeUsage,
|
|
207
211
|
totalUsage,
|
|
208
212
|
fullUsage: shouldPersist ? fullUsage : undefined,
|
|
213
|
+
pricingFingerprint,
|
|
214
|
+
pricingKeys: fullUsage ? [...pricingKeys].sort() : undefined,
|
|
209
215
|
loadFailed: false,
|
|
210
216
|
missing: false,
|
|
211
217
|
persist: shouldPersist,
|
|
@@ -218,7 +224,7 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
218
224
|
if (item.dirty)
|
|
219
225
|
return true;
|
|
220
226
|
const lastMessageTime = item.lastMessageTime;
|
|
221
|
-
if (typeof lastMessageTime ===
|
|
227
|
+
if (typeof lastMessageTime === "number" && lastMessageTime < startAt) {
|
|
222
228
|
return false;
|
|
223
229
|
}
|
|
224
230
|
return true;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Hooks, type PluginInput } from
|
|
1
|
+
import { type Hooks, type PluginInput } from "@opencode-ai/plugin";
|
|
2
2
|
export declare function QuotaSidebarPlugin(input: PluginInput): Promise<Hooks>;
|
|
3
3
|
export default QuotaSidebarPlugin;
|
|
4
|
-
export type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, QuotaStatus, SessionState, CachedSessionUsage, CachedProviderUsage, IncrementalCursor, } from
|
|
5
|
-
export type { UsageSummary } from
|
|
4
|
+
export type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, QuotaStatus, SessionState, CachedSessionUsage, CachedProviderUsage, IncrementalCursor, } from "./types.js";
|
|
5
|
+
export type { UsageSummary } from "./usage.js";
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { renderHistoryMarkdownReport, renderMarkdownReport, resolveTitleView, renderSidebarTitle, renderToastMessage, } from
|
|
2
|
-
import { createQuotaRuntime } from
|
|
3
|
-
import { authFilePath, dateKeyFromTimestamp, deleteSessionFromDayChunk, evictOldSessions, loadConfig, loadState, normalizeTimestampMs, quotaConfigPaths, resolveOpencodeDataDir, saveState, stateFilePath, } from
|
|
4
|
-
import { debug, swallow } from
|
|
5
|
-
import { normalizeBaseTitle } from
|
|
6
|
-
import { createDescendantsResolver } from
|
|
7
|
-
import { createTitleRefreshScheduler } from
|
|
8
|
-
import { createQuotaSidebarTools } from
|
|
9
|
-
import { createEventDispatcher } from
|
|
10
|
-
import { createPersistenceScheduler } from
|
|
11
|
-
import { createQuotaService } from
|
|
12
|
-
import { createUsageService } from
|
|
13
|
-
import { createTitleApplicator } from
|
|
14
|
-
import { listCurrentProviderIDs } from
|
|
15
|
-
const SHUTDOWN_HOOK_KEY = Symbol.for(
|
|
16
|
-
const SHUTDOWN_CALLBACKS_KEY = Symbol.for(
|
|
1
|
+
import { renderHistoryMarkdownReport, renderMarkdownReport, resolveTitleView, renderSidebarTitle, renderToastMessage, } from "./format.js";
|
|
2
|
+
import { createQuotaRuntime } from "./quota.js";
|
|
3
|
+
import { authFilePath, dateKeyFromTimestamp, deleteSessionFromDayChunk, evictOldSessions, loadConfig, loadState, normalizeTimestampMs, quotaConfigPaths, resolveOpencodeDataDir, saveState, stateFilePath, } from "./storage.js";
|
|
4
|
+
import { debug, swallow } from "./helpers.js";
|
|
5
|
+
import { normalizeBaseTitle } from "./title.js";
|
|
6
|
+
import { createDescendantsResolver } from "./descendants.js";
|
|
7
|
+
import { createTitleRefreshScheduler } from "./title_refresh.js";
|
|
8
|
+
import { createQuotaSidebarTools } from "./tools.js";
|
|
9
|
+
import { createEventDispatcher } from "./events.js";
|
|
10
|
+
import { createPersistenceScheduler } from "./persistence.js";
|
|
11
|
+
import { createQuotaService } from "./quota_service.js";
|
|
12
|
+
import { createUsageService } from "./usage_service.js";
|
|
13
|
+
import { createTitleApplicator } from "./title_apply.js";
|
|
14
|
+
import { listCurrentProviderIDs } from "./provider_catalog.js";
|
|
15
|
+
const SHUTDOWN_HOOK_KEY = Symbol.for("opencode-quota-sidebar.shutdown-hook");
|
|
16
|
+
const SHUTDOWN_CALLBACKS_KEY = Symbol.for("opencode-quota-sidebar.shutdown-callbacks");
|
|
17
17
|
const SESSION_ACTIVE_GRACE_MS = 15_000;
|
|
18
18
|
export async function QuotaSidebarPlugin(input) {
|
|
19
19
|
const quotaRuntime = createQuotaRuntime();
|
|
@@ -95,7 +95,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
95
95
|
query: { directory: input.directory },
|
|
96
96
|
throwOnError: true,
|
|
97
97
|
})
|
|
98
|
-
.catch(swallow(
|
|
98
|
+
.catch(swallow("listSessionChildren"));
|
|
99
99
|
return response?.data ?? [];
|
|
100
100
|
},
|
|
101
101
|
getParentID: (sessionID) => state.sessions[sessionID]?.parentID,
|
|
@@ -110,6 +110,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
110
110
|
statePath,
|
|
111
111
|
client: input.client,
|
|
112
112
|
directory: input.directory,
|
|
113
|
+
worktree: input.worktree,
|
|
113
114
|
persistence: {
|
|
114
115
|
markDirty,
|
|
115
116
|
scheduleSave,
|
|
@@ -191,7 +192,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
191
192
|
return;
|
|
192
193
|
await titleApplicator.applyTitle(sessionID);
|
|
193
194
|
},
|
|
194
|
-
onError: swallow(
|
|
195
|
+
onError: swallow("titleRefresh"),
|
|
195
196
|
});
|
|
196
197
|
scheduleTitleRefresh = titleRefresh.schedule;
|
|
197
198
|
const startupTitleWork = Promise.resolve();
|
|
@@ -199,11 +200,11 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
199
200
|
await Promise.race([
|
|
200
201
|
startupTitleWork,
|
|
201
202
|
new Promise((resolve) => setTimeout(resolve, 5_000)),
|
|
202
|
-
]).catch(swallow(
|
|
203
|
+
]).catch(swallow("shutdown:startupTitleWork"));
|
|
203
204
|
await titleRefresh
|
|
204
205
|
.waitForQuiescence()
|
|
205
|
-
.catch(swallow(
|
|
206
|
-
await flushSave().catch(swallow(
|
|
206
|
+
.catch(swallow("shutdown:titleQuiescence"));
|
|
207
|
+
await flushSave().catch(swallow("shutdown:flushSave"));
|
|
207
208
|
};
|
|
208
209
|
const processWithHook = process;
|
|
209
210
|
const shutdownCallbacks = (processWithHook[SHUTDOWN_CALLBACKS_KEY] ||=
|
|
@@ -211,10 +212,10 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
211
212
|
shutdownCallbacks.add(shutdown);
|
|
212
213
|
if (!processWithHook[SHUTDOWN_HOOK_KEY]) {
|
|
213
214
|
processWithHook[SHUTDOWN_HOOK_KEY] = true;
|
|
214
|
-
process.once(
|
|
215
|
+
process.once("beforeExit", () => {
|
|
215
216
|
void Promise.allSettled(Array.from(shutdownCallbacks).map((callback) => callback()));
|
|
216
217
|
});
|
|
217
|
-
for (const signal of [
|
|
218
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
218
219
|
process.once(signal, () => {
|
|
219
220
|
void Promise.allSettled(Array.from(shutdownCallbacks).map((callback) => callback())).finally(() => {
|
|
220
221
|
process.kill(process.pid, signal);
|
|
@@ -231,12 +232,12 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
231
232
|
body: {
|
|
232
233
|
title: `Quota ${period}`,
|
|
233
234
|
message,
|
|
234
|
-
variant:
|
|
235
|
+
variant: "info",
|
|
235
236
|
duration: config.toast.durationMs,
|
|
236
237
|
},
|
|
237
238
|
throwOnError: true,
|
|
238
239
|
})
|
|
239
|
-
.catch(swallow(
|
|
240
|
+
.catch(swallow("showToast"));
|
|
240
241
|
};
|
|
241
242
|
const expiryAlertText = (iso, nowMs = Date.now()) => {
|
|
242
243
|
if (!iso)
|
|
@@ -250,7 +251,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
250
251
|
return undefined;
|
|
251
252
|
const value = new Date(timestamp);
|
|
252
253
|
const now = new Date(nowMs);
|
|
253
|
-
const two = (num) => `${num}`.padStart(2,
|
|
254
|
+
const two = (num) => `${num}`.padStart(2, "0");
|
|
254
255
|
const hhmm = `${two(value.getHours())}:${two(value.getMinutes())}`;
|
|
255
256
|
const sameDay = value.getFullYear() === now.getFullYear() &&
|
|
256
257
|
value.getMonth() === now.getMonth() &&
|
|
@@ -272,7 +273,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
272
273
|
const quotas = await getQuotaSnapshots([], { allowDefault: true });
|
|
273
274
|
const nowMs = Date.now();
|
|
274
275
|
const expiryLines = quotas
|
|
275
|
-
.filter((item) => item.status ===
|
|
276
|
+
.filter((item) => item.status === "ok")
|
|
276
277
|
.map((item) => ({
|
|
277
278
|
label: item.shortLabel || item.label,
|
|
278
279
|
value: expiryAlertText(item.expiresAt, nowMs),
|
|
@@ -287,10 +288,10 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
287
288
|
markDirty(dateKey);
|
|
288
289
|
scheduleSave();
|
|
289
290
|
const body = [
|
|
290
|
-
|
|
291
|
+
"Expiry Soon",
|
|
291
292
|
...expiryLines.map((item) => `${item.label} ${item.value}`),
|
|
292
|
-
].join(
|
|
293
|
-
await showToast(
|
|
293
|
+
].join("\n");
|
|
294
|
+
await showToast("session", body);
|
|
294
295
|
}
|
|
295
296
|
catch (error) {
|
|
296
297
|
debug(`expiry toast check failed: ${String(error)}`);
|
|
@@ -328,7 +329,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
328
329
|
});
|
|
329
330
|
},
|
|
330
331
|
onSessionDeleted: async (session) => {
|
|
331
|
-
await flushSave().catch(swallow(
|
|
332
|
+
await flushSave().catch(swallow("onSessionDeleted:flushSave"));
|
|
332
333
|
descendantsResolver.invalidateForAncestors(session.parentID);
|
|
333
334
|
descendantsResolver.invalidateForAncestors(session.id);
|
|
334
335
|
usageService.forgetSession(session.id);
|
|
@@ -342,7 +343,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
342
343
|
delete state.sessionDateMap[session.id];
|
|
343
344
|
markDirty(dateKey);
|
|
344
345
|
scheduleSave();
|
|
345
|
-
const deletedFromChunk = await deleteSessionFromDayChunk(statePath, session.id, dateKey).catch(swallow(
|
|
346
|
+
const deletedFromChunk = await deleteSessionFromDayChunk(statePath, session.id, dateKey).catch(swallow("deleteSessionFromDayChunk"));
|
|
346
347
|
if (deletedFromChunk) {
|
|
347
348
|
delete state.deletedSessionDateMap[session.id];
|
|
348
349
|
scheduleSave();
|
|
@@ -371,9 +372,9 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
371
372
|
onAssistantMessageUpdated: async (message) => {
|
|
372
373
|
const now = Date.now();
|
|
373
374
|
const completed = message.time.completed;
|
|
374
|
-
if (typeof completed !==
|
|
375
|
+
if (typeof completed !== "number" || !Number.isFinite(completed)) {
|
|
375
376
|
const created = message.time.created;
|
|
376
|
-
if (typeof created ===
|
|
377
|
+
if (typeof created === "number" &&
|
|
377
378
|
Number.isFinite(created) &&
|
|
378
379
|
created < now - SESSION_ACTIVE_GRACE_MS) {
|
|
379
380
|
return;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ModelCostRates } from "./cost.js";
|
|
2
|
+
import type { OpenCodePricingModel } from "./opencode_pricing.js";
|
|
3
|
+
export declare function modelsDevHasProvider(providerID: string): boolean;
|
|
4
|
+
export declare function loadModelsDevPricingModels(requests: OpenCodePricingModel[]): Promise<OpenCodePricingModel[]>;
|
|
5
|
+
export declare function clearModelsDevPricingCache(): void;
|
|
6
|
+
export declare function modelsDevCostToRates(cost: OpenCodePricingModel["cost"]): ModelCostRates | undefined;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { debug, mapConcurrent } from "./helpers.js";
|
|
2
|
+
import { canonicalPricingProviderID, modelCostLookupKeys, } from "./cost.js";
|
|
3
|
+
const MODELS_DEV_RAW_BASE_URL = "https://raw.githubusercontent.com/anomalyco/models.dev/dev/providers";
|
|
4
|
+
const MODELS_DEV_TIMEOUT_MS = 10_000;
|
|
5
|
+
const MODELS_DEV_POSITIVE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
6
|
+
const MODELS_DEV_NEGATIVE_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
const MODELS_DEV_PARSE_MISS_TTL_MS = 10 * 60 * 1000;
|
|
8
|
+
const MODELS_DEV_REQUEST_CONCURRENCY = 4;
|
|
9
|
+
const fileCache = new Map();
|
|
10
|
+
function modelsDevProviderDirs(providerID) {
|
|
11
|
+
const canonical = canonicalPricingProviderID(providerID);
|
|
12
|
+
if (canonical === "openai")
|
|
13
|
+
return ["openai"];
|
|
14
|
+
if (canonical === "anthropic")
|
|
15
|
+
return ["anthropic"];
|
|
16
|
+
if (canonical === "moonshotai")
|
|
17
|
+
return ["moonshotai", "kimi-for-coding"];
|
|
18
|
+
if (canonical === "minimax") {
|
|
19
|
+
return ["minimax-cn-coding-plan", "minimax-coding-plan", "minimax"];
|
|
20
|
+
}
|
|
21
|
+
if (canonical === "zhipu") {
|
|
22
|
+
return ["zai-coding-plan", "zai", "zhipuai-coding-plan", "zhipuai"];
|
|
23
|
+
}
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
function stripInlineComment(line) {
|
|
27
|
+
let inString = false;
|
|
28
|
+
let escaping = false;
|
|
29
|
+
let output = "";
|
|
30
|
+
for (const char of line) {
|
|
31
|
+
if (char === '"' && !escaping)
|
|
32
|
+
inString = !inString;
|
|
33
|
+
if (char === "#" && !inString)
|
|
34
|
+
break;
|
|
35
|
+
output += char;
|
|
36
|
+
if (escaping) {
|
|
37
|
+
escaping = false;
|
|
38
|
+
}
|
|
39
|
+
else if (char === "\\") {
|
|
40
|
+
escaping = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return output.trim();
|
|
44
|
+
}
|
|
45
|
+
function parseTomlNumber(raw) {
|
|
46
|
+
const parsed = Number(raw.replace(/_/g, "").trim());
|
|
47
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
48
|
+
}
|
|
49
|
+
function parseModelsDevCost(text) {
|
|
50
|
+
const rates = {};
|
|
51
|
+
const contextRates = {};
|
|
52
|
+
let section = "";
|
|
53
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
54
|
+
const line = stripInlineComment(rawLine);
|
|
55
|
+
if (!line)
|
|
56
|
+
continue;
|
|
57
|
+
const sectionMatch = /^\[([^\]]+)\]$/.exec(line);
|
|
58
|
+
if (sectionMatch) {
|
|
59
|
+
section = sectionMatch[1] || "";
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const kvMatch = /^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/.exec(line);
|
|
63
|
+
if (!kvMatch)
|
|
64
|
+
continue;
|
|
65
|
+
const key = kvMatch[1];
|
|
66
|
+
const value = parseTomlNumber(kvMatch[2] || "");
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
if (section === "cost") {
|
|
70
|
+
rates[key] = value;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (section === "cost.context_over_200k") {
|
|
74
|
+
contextRates[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const hasBase = (rates.input || 0) > 0 ||
|
|
78
|
+
(rates.output || 0) > 0 ||
|
|
79
|
+
(rates.cache_read || 0) > 0 ||
|
|
80
|
+
(rates.cache_write || 0) > 0;
|
|
81
|
+
if (!hasBase)
|
|
82
|
+
return undefined;
|
|
83
|
+
const hasContext = (contextRates.input || 0) > 0 ||
|
|
84
|
+
(contextRates.output || 0) > 0 ||
|
|
85
|
+
(contextRates.cache_read || 0) > 0 ||
|
|
86
|
+
(contextRates.cache_write || 0) > 0;
|
|
87
|
+
return {
|
|
88
|
+
input: rates.input || 0,
|
|
89
|
+
output: rates.output || 0,
|
|
90
|
+
cache_read: rates.cache_read || 0,
|
|
91
|
+
cache_write: rates.cache_write || 0,
|
|
92
|
+
...(hasContext
|
|
93
|
+
? {
|
|
94
|
+
context_over_200k: {
|
|
95
|
+
input: contextRates.input || 0,
|
|
96
|
+
output: contextRates.output || 0,
|
|
97
|
+
cache_read: contextRates.cache_read || 0,
|
|
98
|
+
cache_write: contextRates.cache_write || 0,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function modelsDevModelCandidates(model) {
|
|
105
|
+
const candidates = new Set();
|
|
106
|
+
for (const stem of [model.modelID, model.modelKey].filter((value) => Boolean(value))) {
|
|
107
|
+
for (const key of modelCostLookupKeys(model.providerID, stem)) {
|
|
108
|
+
const separator = key.indexOf(":");
|
|
109
|
+
const candidate = separator >= 0 ? key.slice(separator + 1) : key;
|
|
110
|
+
if (!candidate || candidate.includes("/"))
|
|
111
|
+
continue;
|
|
112
|
+
candidates.add(candidate);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...candidates];
|
|
116
|
+
}
|
|
117
|
+
async function fetchModelsDevCost(url) {
|
|
118
|
+
const cached = fileCache.get(url);
|
|
119
|
+
if (cached && cached.expiresAt > Date.now())
|
|
120
|
+
return cached.cost;
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timeout = setTimeout(() => controller.abort(), MODELS_DEV_TIMEOUT_MS);
|
|
123
|
+
timeout.unref?.();
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
126
|
+
if (response.status === 404) {
|
|
127
|
+
fileCache.set(url, {
|
|
128
|
+
expiresAt: Date.now() + MODELS_DEV_NEGATIVE_TTL_MS,
|
|
129
|
+
cost: null,
|
|
130
|
+
});
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
debug(`models.dev fetch failed ${response.status} for ${url}`);
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const parsed = parseModelsDevCost(await response.text());
|
|
138
|
+
fileCache.set(url, {
|
|
139
|
+
expiresAt: Date.now() +
|
|
140
|
+
(parsed ? MODELS_DEV_POSITIVE_TTL_MS : MODELS_DEV_PARSE_MISS_TTL_MS),
|
|
141
|
+
cost: parsed || null,
|
|
142
|
+
});
|
|
143
|
+
return parsed || null;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
debug(`models.dev fetch error for ${url}: ${String(error)}`);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function modelsDevHasProvider(providerID) {
|
|
154
|
+
return modelsDevProviderDirs(providerID).length > 0;
|
|
155
|
+
}
|
|
156
|
+
export async function loadModelsDevPricingModels(requests) {
|
|
157
|
+
const resolved = new Map();
|
|
158
|
+
const entries = await mapConcurrent(requests, MODELS_DEV_REQUEST_CONCURRENCY, async (request) => {
|
|
159
|
+
const dirs = modelsDevProviderDirs(request.providerID);
|
|
160
|
+
if (dirs.length === 0)
|
|
161
|
+
return undefined;
|
|
162
|
+
const candidates = modelsDevModelCandidates(request);
|
|
163
|
+
if (candidates.length === 0)
|
|
164
|
+
return undefined;
|
|
165
|
+
let found = undefined;
|
|
166
|
+
for (const dir of dirs) {
|
|
167
|
+
for (const candidate of candidates) {
|
|
168
|
+
const url = `${MODELS_DEV_RAW_BASE_URL}/${dir}/models/${candidate}.toml`;
|
|
169
|
+
const cost = await fetchModelsDevCost(url);
|
|
170
|
+
if (cost === undefined || cost === null)
|
|
171
|
+
continue;
|
|
172
|
+
found = cost;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
if (found)
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
if (!found)
|
|
179
|
+
return undefined;
|
|
180
|
+
return {
|
|
181
|
+
key: `${request.providerID}:${request.modelID}`,
|
|
182
|
+
model: {
|
|
183
|
+
providerKey: request.providerKey,
|
|
184
|
+
providerID: request.providerID,
|
|
185
|
+
modelID: request.modelID,
|
|
186
|
+
modelKey: request.modelKey,
|
|
187
|
+
cost: found,
|
|
188
|
+
options: request.options,
|
|
189
|
+
headers: request.headers,
|
|
190
|
+
api: request.api,
|
|
191
|
+
limit: request.limit,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (!entry)
|
|
197
|
+
continue;
|
|
198
|
+
resolved.set(entry.key, entry.model);
|
|
199
|
+
}
|
|
200
|
+
return [...resolved.values()];
|
|
201
|
+
}
|
|
202
|
+
export function clearModelsDevPricingCache() {
|
|
203
|
+
fileCache.clear();
|
|
204
|
+
}
|
|
205
|
+
export function modelsDevCostToRates(cost) {
|
|
206
|
+
if (!cost || typeof cost !== "object")
|
|
207
|
+
return undefined;
|
|
208
|
+
const record = cost;
|
|
209
|
+
const context = record.context_over_200k && typeof record.context_over_200k === "object"
|
|
210
|
+
? record.context_over_200k
|
|
211
|
+
: undefined;
|
|
212
|
+
return {
|
|
213
|
+
input: Number(record.input || 0),
|
|
214
|
+
output: Number(record.output || 0),
|
|
215
|
+
cacheRead: Number(record.cache_read || 0),
|
|
216
|
+
cacheWrite: Number(record.cache_write || 0),
|
|
217
|
+
contextOver200k: context
|
|
218
|
+
? {
|
|
219
|
+
input: Number(context.input || 0),
|
|
220
|
+
output: Number(context.output || 0),
|
|
221
|
+
cacheRead: Number(context.cache_read || 0),
|
|
222
|
+
cacheWrite: Number(context.cache_write || 0),
|
|
223
|
+
}
|
|
224
|
+
: undefined,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type OpenCodePricingModel = {
|
|
2
|
+
providerKey?: string;
|
|
3
|
+
providerID: string;
|
|
4
|
+
modelID: string;
|
|
5
|
+
modelKey?: string;
|
|
6
|
+
cost?: unknown;
|
|
7
|
+
options?: Record<string, unknown>;
|
|
8
|
+
headers?: Record<string, unknown>;
|
|
9
|
+
api?: Record<string, unknown>;
|
|
10
|
+
limit?: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseJsonc(text: string): unknown;
|
|
13
|
+
export declare function extractOpenCodePricingModels(config: unknown): OpenCodePricingModel[];
|
|
14
|
+
export declare function loadOpenCodePricingModels(paths: string[]): Promise<OpenCodePricingModel[]>;
|