@ereinha/opencode-enhanced-quotas 1.0.3 → 1.0.5
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/dist/defaults.js +9 -3
- package/dist/index.js +148 -117
- package/dist/providers/codex.js +2 -2
- package/package.json +1 -1
package/dist/defaults.js
CHANGED
|
@@ -43,9 +43,15 @@ export const DEFAULT_CONFIG = {
|
|
|
43
43
|
strategy: "most_critical",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
|
-
id: "codex-
|
|
47
|
-
name: "Codex
|
|
48
|
-
sources: ["codex-
|
|
46
|
+
id: "codex-session",
|
|
47
|
+
name: "Codex Session",
|
|
48
|
+
sources: ["codex-session"],
|
|
49
|
+
strategy: "most_critical",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "codex-window",
|
|
53
|
+
name: "Codex Window",
|
|
54
|
+
sources: ["codex-window"],
|
|
49
55
|
strategy: "most_critical",
|
|
50
56
|
},
|
|
51
57
|
],
|
package/dist/index.js
CHANGED
|
@@ -128,152 +128,183 @@ export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
|
|
|
128
128
|
lineCount: lines.length,
|
|
129
129
|
};
|
|
130
130
|
};
|
|
131
|
+
// ─── Helper: send result to chat without triggering AI ─────────────
|
|
132
|
+
const sendCommandResult = async (sessionID, text) => {
|
|
133
|
+
await client.session.prompt({
|
|
134
|
+
path: { id: sessionID },
|
|
135
|
+
body: {
|
|
136
|
+
noReply: true,
|
|
137
|
+
parts: [{
|
|
138
|
+
type: "text",
|
|
139
|
+
text,
|
|
140
|
+
ignored: true,
|
|
141
|
+
}],
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
};
|
|
131
145
|
// ─── Hooks ────────────────────────────────────────────────────────────
|
|
132
146
|
const hooks = {
|
|
133
147
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
* Register /usage and /mymodels commands in OpenCode's UI with descriptions.
|
|
149
|
+
* This makes them appear in the command palette (ctrl+p) and autocomplete.
|
|
150
|
+
*/
|
|
151
|
+
config: async (opencodeConfig) => {
|
|
152
|
+
opencodeConfig.command ??= {};
|
|
153
|
+
opencodeConfig.command["usage"] = {
|
|
154
|
+
template: "",
|
|
155
|
+
description: "Display quota usage for all configured providers",
|
|
156
|
+
};
|
|
157
|
+
opencodeConfig.command["mymodels"] = {
|
|
158
|
+
template: "",
|
|
159
|
+
description: "Show model availability filtered by quota status",
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
/**
|
|
163
|
+
* Intercept /usage and /mymodels commands to execute them deterministically.
|
|
164
|
+
* No AI involvement. Reads from the quota cache directly, sends the result
|
|
165
|
+
* via client.session.prompt with noReply:true, then throws to abort the
|
|
166
|
+
* normal command flow (same pattern as DCP plugin).
|
|
139
167
|
*/
|
|
140
|
-
"command.execute.before": async (input,
|
|
168
|
+
"command.execute.before": async (input, _output) => {
|
|
141
169
|
const command = input.command;
|
|
142
|
-
const args = input.arguments;
|
|
170
|
+
const args = input.arguments || "";
|
|
143
171
|
// Only handle our custom commands
|
|
144
172
|
if (command !== "usage" && command !== "mymodels") {
|
|
145
173
|
return;
|
|
146
174
|
}
|
|
147
175
|
await ensureInit().catch(() => { });
|
|
148
176
|
const snapshot = quotaCache?.getSnapshot();
|
|
149
|
-
const
|
|
177
|
+
const rawQuotas = snapshot?.data || [];
|
|
178
|
+
// Run through the aggregation/processing pipeline so we get
|
|
179
|
+
// the same groups shown in the footer (Session, Window, Flash, etc.)
|
|
180
|
+
const processed = quotaService.processQuotas(rawQuotas);
|
|
181
|
+
// Filter out unlimited / unmetered quotas (limit: null)
|
|
182
|
+
const quotas = processed.filter((q) => q.limit !== null && q.limit > 0);
|
|
150
183
|
// ── /usage ────────────────────────────────────────────────
|
|
151
184
|
if (command === "usage") {
|
|
185
|
+
let result;
|
|
152
186
|
if (quotas.length === 0) {
|
|
153
|
-
|
|
154
|
-
type: "text",
|
|
155
|
-
text: "No quota data available. Quota providers may not be configured or initialized yet.",
|
|
156
|
-
}];
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
// Parse arguments: format=table|json|compact provider=<name>
|
|
160
|
-
const argParts = args.split(" ").filter(Boolean);
|
|
161
|
-
const formatArg = argParts.find((a) => a.startsWith("format="));
|
|
162
|
-
const providerArg = argParts.find((a) => a.startsWith("provider="));
|
|
163
|
-
const format = formatArg?.split("=")[1] || "table";
|
|
164
|
-
const providerFilter = providerArg?.split("=")[1];
|
|
165
|
-
let filtered = quotas;
|
|
166
|
-
if (providerFilter) {
|
|
167
|
-
filtered = quotas.filter((q) => q.providerName.toLowerCase().includes(providerFilter.toLowerCase()));
|
|
168
|
-
}
|
|
169
|
-
let result = "";
|
|
170
|
-
if (format === "json") {
|
|
171
|
-
result = JSON.stringify(filtered.map((q) => ({
|
|
172
|
-
id: q.id,
|
|
173
|
-
provider: q.providerName,
|
|
174
|
-
used: q.used,
|
|
175
|
-
limit: q.limit,
|
|
176
|
-
percentUsed: q.limit ? Math.round((q.used / q.limit) * 100) : 0,
|
|
177
|
-
reset: q.reset || q.predictedReset || null,
|
|
178
|
-
unit: q.unit,
|
|
179
|
-
})), null, 2);
|
|
180
|
-
}
|
|
181
|
-
else if (format === "compact") {
|
|
182
|
-
result = filtered
|
|
183
|
-
.map((q) => {
|
|
184
|
-
const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
|
|
185
|
-
return `${q.providerName}: ${q.used}/${q.limit || "unlimited"} (${pct}%)`;
|
|
186
|
-
})
|
|
187
|
-
.join(" | ");
|
|
187
|
+
result = "No quota data available. Quota providers may not be configured or initialized yet.";
|
|
188
188
|
}
|
|
189
189
|
else {
|
|
190
|
-
//
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
190
|
+
// Parse arguments: format=table|json|compact provider=<name>
|
|
191
|
+
const argParts = args.split(" ").filter(Boolean);
|
|
192
|
+
const formatArg = argParts.find((a) => a.startsWith("format="));
|
|
193
|
+
const providerArg = argParts.find((a) => a.startsWith("provider="));
|
|
194
|
+
const format = formatArg?.split("=")[1] || "table";
|
|
195
|
+
const providerFilter = providerArg?.split("=")[1];
|
|
196
|
+
let filtered = quotas;
|
|
197
|
+
if (providerFilter) {
|
|
198
|
+
filtered = quotas.filter((q) => q.providerName.toLowerCase().includes(providerFilter.toLowerCase()));
|
|
199
|
+
}
|
|
200
|
+
if (format === "json") {
|
|
201
|
+
result = JSON.stringify(filtered.map((q) => ({
|
|
202
|
+
id: q.id,
|
|
203
|
+
provider: q.providerName,
|
|
204
|
+
used: q.used,
|
|
205
|
+
limit: q.limit,
|
|
206
|
+
percentUsed: q.limit ? Math.round((q.used / q.limit) * 100) : 0,
|
|
207
|
+
reset: q.reset || q.predictedReset || null,
|
|
208
|
+
unit: q.unit,
|
|
209
|
+
})), null, 2);
|
|
210
|
+
}
|
|
211
|
+
else if (format === "compact") {
|
|
212
|
+
result = filtered
|
|
213
|
+
.map((q) => {
|
|
214
|
+
const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
|
|
215
|
+
return `${q.providerName}: ${q.used}/${q.limit || "unlimited"} (${pct}%)`;
|
|
216
|
+
})
|
|
217
|
+
.join(" | ");
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// Default: table format using the existing renderQuotaTable
|
|
221
|
+
const config = quotaService.getConfig();
|
|
222
|
+
const lines = renderQuotaTable(filtered, {
|
|
223
|
+
progressBarConfig: config.progressBar,
|
|
224
|
+
tableConfig: config.table,
|
|
225
|
+
}).map((l) => l.line);
|
|
226
|
+
const showMode = config.progressBar?.show ?? "used";
|
|
227
|
+
const modeLabel = showMode === "available" ? "(Remaining)" : "(Used)";
|
|
228
|
+
const header = `_Opencode Quotas ${modeLabel}_`;
|
|
229
|
+
const updatedAt = snapshot?.fetchedAt
|
|
230
|
+
? `Last updated: ${snapshot.fetchedAt.toLocaleTimeString()}`
|
|
231
|
+
: "";
|
|
232
|
+
result = [header, ...lines, updatedAt].filter(Boolean).join("\n");
|
|
233
|
+
}
|
|
203
234
|
}
|
|
204
|
-
|
|
205
|
-
|
|
235
|
+
await sendCommandResult(input.sessionID, result);
|
|
236
|
+
throw new Error("__QUOTAHUB_USAGE_HANDLED__");
|
|
206
237
|
}
|
|
207
238
|
// ── /mymodels ─────────────────────────────────────────────
|
|
208
239
|
if (command === "mymodels") {
|
|
240
|
+
let result;
|
|
209
241
|
if (quotas.length === 0) {
|
|
210
|
-
|
|
211
|
-
type: "text",
|
|
212
|
-
text: "No quota data available to determine model availability.",
|
|
213
|
-
}];
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
// Parse arguments: status=all|available|limited|exhausted provider=<name>
|
|
217
|
-
const argParts = args.split(" ").filter(Boolean);
|
|
218
|
-
const statusArg = argParts.find((a) => a.startsWith("status="));
|
|
219
|
-
const providerArg = argParts.find((a) => a.startsWith("provider="));
|
|
220
|
-
const statusFilter = statusArg?.split("=")[1] || "all";
|
|
221
|
-
const providerFilter = providerArg?.split("=")[1];
|
|
222
|
-
let filtered = quotas;
|
|
223
|
-
if (providerFilter) {
|
|
224
|
-
filtered = quotas.filter((q) => q.providerName.toLowerCase().includes(providerFilter.toLowerCase()));
|
|
225
|
-
}
|
|
226
|
-
const available = [];
|
|
227
|
-
const limited = [];
|
|
228
|
-
const exhausted = [];
|
|
229
|
-
for (const q of filtered) {
|
|
230
|
-
const pct = q.limit ? (q.used / q.limit) * 100 : 0;
|
|
231
|
-
if (pct >= 100)
|
|
232
|
-
exhausted.push(q);
|
|
233
|
-
else if (pct >= 80)
|
|
234
|
-
limited.push(q);
|
|
235
|
-
else
|
|
236
|
-
available.push(q);
|
|
242
|
+
result = "No quota data available to determine model availability.";
|
|
237
243
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
else {
|
|
245
|
+
// Parse arguments: status=all|available|limited|exhausted provider=<name>
|
|
246
|
+
const argParts = args.split(" ").filter(Boolean);
|
|
247
|
+
const statusArg = argParts.find((a) => a.startsWith("status="));
|
|
248
|
+
const providerArg = argParts.find((a) => a.startsWith("provider="));
|
|
249
|
+
const statusFilter = statusArg?.split("=")[1] || "all";
|
|
250
|
+
const providerFilter = providerArg?.split("=")[1];
|
|
251
|
+
let filtered = quotas;
|
|
252
|
+
if (providerFilter) {
|
|
253
|
+
filtered = quotas.filter((q) => q.providerName.toLowerCase().includes(providerFilter.toLowerCase()));
|
|
254
|
+
}
|
|
255
|
+
const available = [];
|
|
256
|
+
const limited = [];
|
|
257
|
+
const exhausted = [];
|
|
258
|
+
for (const q of filtered) {
|
|
259
|
+
const pct = q.limit ? (q.used / q.limit) * 100 : 0;
|
|
260
|
+
if (pct >= 100)
|
|
261
|
+
exhausted.push(q);
|
|
262
|
+
else if (pct >= 80)
|
|
263
|
+
limited.push(q);
|
|
264
|
+
else
|
|
265
|
+
available.push(q);
|
|
266
|
+
}
|
|
267
|
+
const lines = ["Model Availability by Quota Status:"];
|
|
268
|
+
if (statusFilter === "all" || statusFilter === "available") {
|
|
269
|
+
if (available.length > 0) {
|
|
270
|
+
lines.push("\n[OK] Available:");
|
|
271
|
+
for (const q of available) {
|
|
272
|
+
const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
|
|
273
|
+
const reset = q.reset ? ` (resets: ${q.reset})` : "";
|
|
274
|
+
lines.push(` + ${q.providerName} - ${q.id}: ${pct}% used${reset}`);
|
|
275
|
+
}
|
|
246
276
|
}
|
|
247
277
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
278
|
+
if (statusFilter === "all" || statusFilter === "limited") {
|
|
279
|
+
if (limited.length > 0) {
|
|
280
|
+
lines.push("\n[~] Limited (approaching limit):");
|
|
281
|
+
for (const q of limited) {
|
|
282
|
+
const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
|
|
283
|
+
const reset = q.reset ? ` (resets: ${q.reset})` : "";
|
|
284
|
+
lines.push(` ~ ${q.providerName} - ${q.id}: ${pct}% used${reset}`);
|
|
285
|
+
}
|
|
256
286
|
}
|
|
257
287
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
288
|
+
if (statusFilter === "all" || statusFilter === "exhausted") {
|
|
289
|
+
if (exhausted.length > 0) {
|
|
290
|
+
lines.push("\n[!] Exhausted (unavailable):");
|
|
291
|
+
for (const q of exhausted) {
|
|
292
|
+
const reset = q.reset ? ` (resets: ${q.reset})` : "";
|
|
293
|
+
lines.push(` x ${q.providerName} - ${q.id}: 100%+ used${reset}`);
|
|
294
|
+
}
|
|
265
295
|
}
|
|
266
296
|
}
|
|
297
|
+
if (lines.length === 1) {
|
|
298
|
+
lines.push(`\nNo models found with status: ${statusFilter}`);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
lines.push(`\n${"-".repeat(40)}`);
|
|
302
|
+
lines.push(`Summary: ${available.length} available, ${limited.length} limited, ${exhausted.length} exhausted`);
|
|
303
|
+
}
|
|
304
|
+
result = lines.join("\n");
|
|
267
305
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
lines.push(`\n${"-".repeat(40)}`);
|
|
273
|
-
lines.push(`Summary: ${available.length} available, ${limited.length} limited, ${exhausted.length} exhausted`);
|
|
274
|
-
}
|
|
275
|
-
output.parts = [{ type: "text", text: lines.join("\n") }];
|
|
276
|
-
return;
|
|
306
|
+
await sendCommandResult(input.sessionID, result);
|
|
307
|
+
throw new Error("__QUOTAHUB_MYMODELS_HANDLED__");
|
|
277
308
|
}
|
|
278
309
|
},
|
|
279
310
|
/**
|
package/dist/providers/codex.js
CHANGED
|
@@ -182,12 +182,12 @@ export function extractCodexQuota(payload) {
|
|
|
182
182
|
? rateLimit.secondary_window
|
|
183
183
|
: null;
|
|
184
184
|
if (primary) {
|
|
185
|
-
const entry = parseRateLimitWindow("
|
|
185
|
+
const entry = parseRateLimitWindow("session", "Session", primary);
|
|
186
186
|
if (entry)
|
|
187
187
|
entries.push(entry);
|
|
188
188
|
}
|
|
189
189
|
if (secondary) {
|
|
190
|
-
const entry = parseRateLimitWindow("
|
|
190
|
+
const entry = parseRateLimitWindow("window", "Window", secondary);
|
|
191
191
|
if (entry)
|
|
192
192
|
entries.push(entry);
|
|
193
193
|
}
|