@ereinha/opencode-enhanced-quotas 1.0.1 → 1.0.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/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -1
- package/dist/defaults.d.ts +0 -1
- package/dist/defaults.js +0 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +141 -147
- package/dist/interfaces.d.ts +0 -1
- package/dist/interfaces.js +0 -1
- package/dist/logger.d.ts +0 -1
- package/dist/logger.js +0 -1
- package/dist/plugin-state.d.ts +0 -1
- package/dist/plugin-state.js +0 -1
- package/dist/providers/antigravity/auth.d.ts +0 -1
- package/dist/providers/antigravity/auth.js +0 -1
- package/dist/providers/antigravity/index.d.ts +0 -1
- package/dist/providers/antigravity/index.js +0 -1
- package/dist/providers/antigravity/provider.d.ts +0 -1
- package/dist/providers/antigravity/provider.js +0 -1
- package/dist/providers/codex.d.ts +0 -1
- package/dist/providers/codex.js +0 -1
- package/dist/providers/github.d.ts +0 -1
- package/dist/providers/github.js +0 -1
- package/dist/quota-cache.d.ts +0 -1
- package/dist/quota-cache.js +0 -1
- package/dist/registry.d.ts +0 -1
- package/dist/registry.js +0 -1
- package/dist/services/aggregation-service.d.ts +0 -1
- package/dist/services/aggregation-service.js +0 -1
- package/dist/services/config-loader.d.ts +0 -1
- package/dist/services/config-loader.js +0 -1
- package/dist/services/history-service.d.ts +0 -1
- package/dist/services/history-service.js +0 -1
- package/dist/services/prediction-engine.d.ts +0 -1
- package/dist/services/prediction-engine.js +0 -1
- package/dist/services/quota-service.d.ts +0 -1
- package/dist/services/quota-service.js +0 -1
- package/dist/ui/progress-bar.d.ts +0 -1
- package/dist/ui/progress-bar.js +0 -1
- package/dist/ui/quota-table.d.ts +0 -1
- package/dist/ui/quota-table.js +0 -1
- package/dist/utils/paths.d.ts +0 -1
- package/dist/utils/paths.js +0 -1
- package/dist/utils/time.d.ts +0 -1
- package/dist/utils/time.js +0 -1
- package/dist/utils/validation.d.ts +0 -1
- package/dist/utils/validation.js +0 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
package/dist/defaults.d.ts
CHANGED
package/dist/defaults.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
/**
|
|
3
3
|
* QuotaHub Plugin for OpenCode.ai
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - /usage command: deterministic local quota display (no AI)
|
|
7
|
+
* - /mymodels command: deterministic local model availability display (no AI)
|
|
8
|
+
* - Footer injection on assistant messages showing quota status
|
|
4
9
|
*/
|
|
5
10
|
export declare const QuotaHubPlugin: Plugin;
|
|
6
11
|
export default QuotaHubPlugin;
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
1
|
import { QuotaService } from "./services/quota-service";
|
|
3
2
|
import { HistoryService } from "./services/history-service";
|
|
4
3
|
import { renderQuotaTable } from "./ui/quota-table";
|
|
@@ -6,9 +5,13 @@ import { QuotaCache } from "./quota-cache";
|
|
|
6
5
|
import { PLUGIN_FOOTER_SIGNATURE, SKIP_REASONS, } from "./constants";
|
|
7
6
|
import { logger } from "./logger";
|
|
8
7
|
import { getPluginState } from "./plugin-state";
|
|
9
|
-
const z = tool.schema;
|
|
10
8
|
/**
|
|
11
9
|
* QuotaHub Plugin for OpenCode.ai
|
|
10
|
+
*
|
|
11
|
+
* Provides:
|
|
12
|
+
* - /usage command: deterministic local quota display (no AI)
|
|
13
|
+
* - /mymodels command: deterministic local model availability display (no AI)
|
|
14
|
+
* - Footer injection on assistant messages showing quota status
|
|
12
15
|
*/
|
|
13
16
|
export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
|
|
14
17
|
const state = getPluginState();
|
|
@@ -60,7 +63,6 @@ export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
|
|
|
60
63
|
attempt,
|
|
61
64
|
error: errorMsg,
|
|
62
65
|
});
|
|
63
|
-
// Log a user-visible warning
|
|
64
66
|
console.warn(`[QuotaHub] Failed to initialize after ${MAX_INIT_RETRIES} attempts. Quota information will be unavailable. Error: ${errorMsg}`);
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -126,159 +128,153 @@ export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
|
|
|
126
128
|
lineCount: lines.length,
|
|
127
129
|
};
|
|
128
130
|
};
|
|
131
|
+
// ─── Hooks ────────────────────────────────────────────────────────────
|
|
129
132
|
const hooks = {
|
|
130
133
|
/**
|
|
131
|
-
*
|
|
134
|
+
* Intercept /usage and /mymodels commands to execute them deterministically
|
|
135
|
+
* without any AI involvement. Reads from the quota cache directly.
|
|
136
|
+
*
|
|
137
|
+
* By setting output.parts, we replace whatever would normally be sent to the
|
|
138
|
+
* AI with our own pre-computed text. The AI never sees these commands.
|
|
132
139
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
140
|
+
"command.execute.before": async (input, output) => {
|
|
141
|
+
const command = input.command;
|
|
142
|
+
const args = input.arguments;
|
|
143
|
+
// Only handle our custom commands
|
|
144
|
+
if (command !== "usage" && command !== "mymodels") {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await ensureInit().catch(() => { });
|
|
148
|
+
const snapshot = quotaCache?.getSnapshot();
|
|
149
|
+
const quotas = snapshot?.data || [];
|
|
150
|
+
// ── /usage ────────────────────────────────────────────────
|
|
151
|
+
if (command === "usage") {
|
|
152
|
+
if (quotas.length === 0) {
|
|
153
|
+
output.parts = [{
|
|
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(" | ");
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Default: table format using the existing renderQuotaTable
|
|
168
191
|
const config = quotaService.getConfig();
|
|
169
|
-
const
|
|
170
|
-
if (format === "json") {
|
|
171
|
-
return JSON.stringify(filteredResults.map(q => ({
|
|
172
|
-
id: q.id,
|
|
173
|
-
provider: q.providerName,
|
|
174
|
-
used: q.used,
|
|
175
|
-
limit: q.limit,
|
|
176
|
-
remaining: q.limit ? q.limit - q.used : null,
|
|
177
|
-
percentUsed: calcPercent(q),
|
|
178
|
-
reset: q.reset,
|
|
179
|
-
unit: q.unit,
|
|
180
|
-
})), null, 2);
|
|
181
|
-
}
|
|
182
|
-
if (format === "compact") {
|
|
183
|
-
return filteredResults.map(q => {
|
|
184
|
-
const pct = calcPercent(q);
|
|
185
|
-
const status = pct >= 90 ? "[!]" : pct >= 70 ? "[~]" : "[OK]";
|
|
186
|
-
return `${status} ${q.id}: ${pct.toFixed(0)}% used`;
|
|
187
|
-
}).join("\n");
|
|
188
|
-
}
|
|
189
|
-
// Default: table format
|
|
190
|
-
const lines = renderQuotaTable(filteredResults, {
|
|
192
|
+
const lines = renderQuotaTable(filtered, {
|
|
191
193
|
progressBarConfig: config.progressBar,
|
|
192
194
|
tableConfig: config.table,
|
|
193
195
|
}).map((l) => l.line);
|
|
194
196
|
const showMode = config.progressBar?.show ?? "used";
|
|
195
197
|
const modeLabel = showMode === "available" ? "(Remaining)" : "(Used)";
|
|
196
|
-
const header = `
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
const statusFilter = args.status || "all";
|
|
246
|
-
let output = [];
|
|
247
|
-
const formatQuota = (q, marker) => {
|
|
248
|
-
const pct = calcPercent(q);
|
|
249
|
-
const reset = q.reset ? ` (resets: ${q.reset})` : "";
|
|
250
|
-
return `${marker} ${q.id}: ${pct.toFixed(0)}% used${reset}`;
|
|
251
|
-
};
|
|
252
|
-
if (statusFilter === "all" || statusFilter === "available") {
|
|
253
|
-
if (available.length > 0) {
|
|
254
|
-
output.push("[OK] Available Models:");
|
|
255
|
-
output.push(...available.map(q => formatQuota(q, " +")));
|
|
256
|
-
output.push("");
|
|
198
|
+
const header = `_Opencode Quotas ${modeLabel}_`;
|
|
199
|
+
const updatedAt = snapshot?.fetchedAt
|
|
200
|
+
? `Last updated: ${snapshot.fetchedAt.toLocaleTimeString()}`
|
|
201
|
+
: "";
|
|
202
|
+
result = [header, ...lines, updatedAt].filter(Boolean).join("\n");
|
|
203
|
+
}
|
|
204
|
+
output.parts = [{ type: "text", text: result }];
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// ── /mymodels ─────────────────────────────────────────────
|
|
208
|
+
if (command === "mymodels") {
|
|
209
|
+
if (quotas.length === 0) {
|
|
210
|
+
output.parts = [{
|
|
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);
|
|
237
|
+
}
|
|
238
|
+
const lines = ["Model Availability by Quota Status:"];
|
|
239
|
+
if (statusFilter === "all" || statusFilter === "available") {
|
|
240
|
+
if (available.length > 0) {
|
|
241
|
+
lines.push("\n[OK] Available:");
|
|
242
|
+
for (const q of available) {
|
|
243
|
+
const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
|
|
244
|
+
const reset = q.reset ? ` (resets: ${q.reset})` : "";
|
|
245
|
+
lines.push(` + ${q.providerName} - ${q.id}: ${pct}% used${reset}`);
|
|
257
246
|
}
|
|
258
247
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
248
|
+
}
|
|
249
|
+
if (statusFilter === "all" || statusFilter === "limited") {
|
|
250
|
+
if (limited.length > 0) {
|
|
251
|
+
lines.push("\n[~] Limited (approaching limit):");
|
|
252
|
+
for (const q of limited) {
|
|
253
|
+
const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
|
|
254
|
+
const reset = q.reset ? ` (resets: ${q.reset})` : "";
|
|
255
|
+
lines.push(` ~ ${q.providerName} - ${q.id}: ${pct}% used${reset}`);
|
|
264
256
|
}
|
|
265
257
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
258
|
+
}
|
|
259
|
+
if (statusFilter === "all" || statusFilter === "exhausted") {
|
|
260
|
+
if (exhausted.length > 0) {
|
|
261
|
+
lines.push("\n[!] Exhausted (unavailable):");
|
|
262
|
+
for (const q of exhausted) {
|
|
263
|
+
const reset = q.reset ? ` (resets: ${q.reset})` : "";
|
|
264
|
+
lines.push(` x ${q.providerName} - ${q.id}: 100%+ used${reset}`);
|
|
271
265
|
}
|
|
272
266
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
267
|
+
}
|
|
268
|
+
if (lines.length === 1) {
|
|
269
|
+
lines.push(`\nNo models found with status: ${statusFilter}`);
|
|
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;
|
|
277
|
+
}
|
|
282
278
|
},
|
|
283
279
|
/**
|
|
284
280
|
* The platform calls this hook after a text generation is complete.
|
|
@@ -387,7 +383,7 @@ export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
|
|
|
387
383
|
partID: input.partID,
|
|
388
384
|
partType: currentPart?.type,
|
|
389
385
|
isLastPart,
|
|
390
|
-
totalParts: parts.length
|
|
386
|
+
totalParts: parts.length,
|
|
391
387
|
});
|
|
392
388
|
const isSubagentMessage = assistantMsg.mode === "subagent";
|
|
393
389
|
const isReasoningPart = currentPart?.type === "reasoning";
|
|
@@ -398,18 +394,17 @@ export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
|
|
|
398
394
|
debugLog(SKIP_REASONS.THINKING, {
|
|
399
395
|
messageID: input.messageID,
|
|
400
396
|
partID: input.partID,
|
|
401
|
-
type: currentPart?.type
|
|
397
|
+
type: currentPart?.type,
|
|
402
398
|
});
|
|
403
399
|
return;
|
|
404
400
|
}
|
|
405
401
|
// Only inject on the last part of the message to avoid double injection
|
|
406
|
-
// and to ensure we are at the very end.
|
|
407
402
|
if (!isLastPart) {
|
|
408
403
|
debugLog("skip:not_last_part", {
|
|
409
404
|
messageID: input.messageID,
|
|
410
405
|
partID: input.partID,
|
|
411
406
|
index: currentPartIndex,
|
|
412
|
-
total: parts.length
|
|
407
|
+
total: parts.length,
|
|
413
408
|
});
|
|
414
409
|
return;
|
|
415
410
|
}
|
|
@@ -418,7 +413,7 @@ export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
|
|
|
418
413
|
debugLog("skip:not_text_part", {
|
|
419
414
|
messageID: input.messageID,
|
|
420
415
|
partID: input.partID,
|
|
421
|
-
type: currentPart?.type
|
|
416
|
+
type: currentPart?.type,
|
|
422
417
|
});
|
|
423
418
|
state.markProcessed(input.messageID);
|
|
424
419
|
return;
|
|
@@ -446,4 +441,3 @@ export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
|
|
|
446
441
|
return hooks;
|
|
447
442
|
};
|
|
448
443
|
export default QuotaHubPlugin;
|
|
449
|
-
//# sourceMappingURL=index.js.map
|
package/dist/interfaces.d.ts
CHANGED
package/dist/interfaces.js
CHANGED
package/dist/logger.d.ts
CHANGED
package/dist/logger.js
CHANGED
package/dist/plugin-state.d.ts
CHANGED
package/dist/plugin-state.js
CHANGED
|
@@ -31,4 +31,3 @@ export declare function fetchCloudQuota(accessToken: string, projectId?: string,
|
|
|
31
31
|
* Grouping and aggregation is handled by the service layer via AggregatedGroups.
|
|
32
32
|
*/
|
|
33
33
|
export declare function createAntigravityProvider(config?: AntigravityConfig): IQuotaProvider;
|
|
34
|
-
//# sourceMappingURL=provider.d.ts.map
|
package/dist/providers/codex.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import { type IQuotaProvider, type QuotaData } from "../interfaces";
|
|
2
2
|
export declare function parseGithubUsage(data: unknown, sku: string | null, apiWarning?: string | null): QuotaData[];
|
|
3
3
|
export declare function createGithubProvider(): IQuotaProvider;
|
|
4
|
-
//# sourceMappingURL=github.d.ts.map
|
package/dist/providers/github.js
CHANGED
package/dist/quota-cache.d.ts
CHANGED
package/dist/quota-cache.js
CHANGED
package/dist/registry.d.ts
CHANGED
package/dist/registry.js
CHANGED
package/dist/ui/progress-bar.js
CHANGED
package/dist/ui/quota-table.d.ts
CHANGED
package/dist/ui/quota-table.js
CHANGED
package/dist/utils/paths.d.ts
CHANGED
package/dist/utils/paths.js
CHANGED
|
@@ -34,4 +34,3 @@ export const AUTH_FILE = () => join(getDataDirectory(), "auth.json");
|
|
|
34
34
|
export const HISTORY_FILE = () => join(getDataDirectory(), "quota-history.json");
|
|
35
35
|
export const DEBUG_LOG_FILE = () => join(getDataDirectory(), "quotas-debug.log");
|
|
36
36
|
export const ANTIGRAVITY_ACCOUNTS_FILE = () => join(getConfigDirectory(), "antigravity-accounts.json");
|
|
37
|
-
//# sourceMappingURL=paths.js.map
|
package/dist/utils/time.d.ts
CHANGED
package/dist/utils/time.js
CHANGED
|
@@ -3,4 +3,3 @@ export declare function isValidNumber(v: unknown): v is number;
|
|
|
3
3
|
export declare function clamp(value: number, min: number, max?: number): number;
|
|
4
4
|
export declare function validatePollingInterval(v: unknown): number | null;
|
|
5
5
|
export declare function validateQuotaData(input: unknown): QuotaData | null;
|
|
6
|
-
//# sourceMappingURL=validation.d.ts.map
|
package/dist/utils/validation.js
CHANGED