opencode-gemini-auth 1.4.0 → 1.4.1
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/package.json +1 -1
- package/src/plugin/quota.test.ts +63 -4
- package/src/plugin/quota.ts +220 -20
package/package.json
CHANGED
package/src/plugin/quota.test.ts
CHANGED
|
@@ -34,10 +34,10 @@ describe("formatGeminiQuotaOutput", () => {
|
|
|
34
34
|
Date.now = REAL_DATE_NOW;
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it("renders
|
|
37
|
+
it("renders grouped progress bars, groups by version, and hides token type when all are REQUESTS", () => {
|
|
38
38
|
const buckets: RetrieveUserQuotaBucket[] = [
|
|
39
39
|
{
|
|
40
|
-
modelId: "gemini-2.5-
|
|
40
|
+
modelId: "gemini-2.5-pro_vertex",
|
|
41
41
|
tokenType: "requests",
|
|
42
42
|
remainingFraction: 0.5,
|
|
43
43
|
remainingAmount: "100",
|
|
@@ -47,16 +47,75 @@ describe("formatGeminiQuotaOutput", () => {
|
|
|
47
47
|
modelId: "gemini-2.5-flash",
|
|
48
48
|
remainingAmount: "20",
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
modelId: "gemini-2.5-pro",
|
|
52
|
+
tokenType: "requests",
|
|
53
|
+
remainingFraction: 0.7,
|
|
54
|
+
remainingAmount: "140",
|
|
55
|
+
resetTime: new Date(FIXED_NOW + 2 * 60 * 60 * 1000).toISOString(),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
modelId: "gemini-3-pro-preview",
|
|
59
|
+
tokenType: "requests",
|
|
60
|
+
remainingFraction: 0.95,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
modelId: "gemini-2.0-flash",
|
|
64
|
+
tokenType: "requests",
|
|
65
|
+
remainingFraction: 0.8,
|
|
66
|
+
},
|
|
50
67
|
];
|
|
51
68
|
|
|
52
69
|
const output = formatGeminiQuotaOutput("test-project", buckets);
|
|
53
70
|
expect(output).toContain("Gemini quota usage for project `test-project`");
|
|
54
|
-
expect(output).toContain("
|
|
71
|
+
expect(output).toContain("Variant");
|
|
72
|
+
expect(output).toContain("Remaining");
|
|
73
|
+
expect(output).toContain("Reset");
|
|
74
|
+
expect(output).not.toContain("Type");
|
|
75
|
+
expect(output).toContain("gemini-2.5-flash\n ↳ default");
|
|
76
|
+
expect(output).toContain("gemini-2.5-pro\n ↳ default");
|
|
77
|
+
expect(output).toContain(" ↳ vertex");
|
|
78
|
+
expect(output).toContain("Gemini 3 (1 model, 1 bucket)");
|
|
79
|
+
expect(output).toContain("Gemini 2.5 (2 models, 3 buckets)");
|
|
80
|
+
expect(output).toContain("Gemini 2.0 (1 model, 1 bucket)");
|
|
81
|
+
expect(output).toContain("▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ 70.0% (140 left)");
|
|
55
82
|
expect(output).toContain(
|
|
56
|
-
"
|
|
83
|
+
"▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░ 50.0% (100 left)",
|
|
84
|
+
);
|
|
85
|
+
expect(output.indexOf("Gemini 3 (1 model, 1 bucket)")).toBeLessThan(
|
|
86
|
+
output.indexOf("Gemini 2.5 (2 models, 3 buckets)"),
|
|
87
|
+
);
|
|
88
|
+
expect(output.indexOf("Gemini 2.5 (2 models, 3 buckets)")).toBeLessThan(
|
|
89
|
+
output.indexOf("Gemini 2.0 (1 model, 1 bucket)"),
|
|
90
|
+
);
|
|
91
|
+
expect(output).toContain("\n\nGemini 2.5 (2 models, 3 buckets)");
|
|
92
|
+
expect(output.indexOf("gemini-2.0-flash")).toBeGreaterThan(
|
|
93
|
+
output.indexOf("gemini-2.5-pro"),
|
|
57
94
|
);
|
|
58
95
|
expect(output.indexOf("gemini-2.5-flash")).toBeLessThan(
|
|
59
96
|
output.indexOf("gemini-2.5-pro"),
|
|
60
97
|
);
|
|
61
98
|
});
|
|
99
|
+
|
|
100
|
+
it("shows token type column when multiple token types are present", () => {
|
|
101
|
+
const buckets: RetrieveUserQuotaBucket[] = [
|
|
102
|
+
{
|
|
103
|
+
modelId: "gemini-2.5-pro_vertex",
|
|
104
|
+
tokenType: "REQUESTS",
|
|
105
|
+
remainingFraction: 0.9,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
modelId: "gemini-2.5-pro",
|
|
109
|
+
tokenType: "TOKENS",
|
|
110
|
+
remainingFraction: 0.8,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const output = formatGeminiQuotaOutput("test-project", buckets);
|
|
115
|
+
expect(output).toContain("Type");
|
|
116
|
+
expect(output).toContain("Gemini 2.5 (1 model, 2 buckets)");
|
|
117
|
+
expect(output).toContain("REQUESTS");
|
|
118
|
+
expect(output).toContain("TOKENS");
|
|
119
|
+
expect(output).toContain(" ↳ vertex");
|
|
120
|
+
});
|
|
62
121
|
});
|
package/src/plugin/quota.ts
CHANGED
|
@@ -82,10 +82,45 @@ export function formatGeminiQuotaOutput(
|
|
|
82
82
|
buckets: RetrieveUserQuotaBucket[],
|
|
83
83
|
): string {
|
|
84
84
|
const sortedBuckets = [...buckets].sort(compareQuotaBuckets);
|
|
85
|
-
const
|
|
85
|
+
const groupedRows = groupQuotaRows(sortedBuckets);
|
|
86
|
+
const versionGroups = groupByVersion(groupedRows);
|
|
87
|
+
const variantWidth = Math.max(
|
|
88
|
+
"Variant".length,
|
|
89
|
+
...versionGroups.flatMap((group) =>
|
|
90
|
+
group.models.flatMap((model) => model.rows.map((row) => row.variant.length))
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
const tokenTypeValues = [...new Set(versionGroups.flatMap((group) =>
|
|
94
|
+
group.models.flatMap((model) => model.rows.map((row) => row.tokenType))
|
|
95
|
+
))];
|
|
96
|
+
const showTokenType = tokenTypeValues.length > 1 || tokenTypeValues[0] !== "REQUESTS";
|
|
97
|
+
const lines = [
|
|
98
|
+
`Gemini quota usage for project \`${projectId}\``,
|
|
99
|
+
"",
|
|
100
|
+
showTokenType
|
|
101
|
+
? ` ↳ ${pad("Variant", variantWidth)} Remaining Reset Type`
|
|
102
|
+
: ` ↳ ${pad("Variant", variantWidth)} Remaining Reset`,
|
|
103
|
+
];
|
|
86
104
|
|
|
87
|
-
for (
|
|
88
|
-
|
|
105
|
+
for (let index = 0; index < versionGroups.length; index += 1) {
|
|
106
|
+
const versionGroup = versionGroups[index];
|
|
107
|
+
if (!versionGroup) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (index > 0) {
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
lines.push(formatVersionGroupTitle(versionGroup));
|
|
114
|
+
for (const model of versionGroup.models) {
|
|
115
|
+
lines.push(model.baseModel);
|
|
116
|
+
for (const row of model.rows) {
|
|
117
|
+
lines.push(
|
|
118
|
+
showTokenType
|
|
119
|
+
? ` ↳ ${pad(row.variant, variantWidth)} ${pad(row.usageRemaining, 27)} ${pad(row.resetValue, 8)} ${row.tokenType}`
|
|
120
|
+
: ` ↳ ${pad(row.variant, variantWidth)} ${pad(row.usageRemaining, 27)} ${row.resetValue}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
89
124
|
}
|
|
90
125
|
|
|
91
126
|
return lines.join("\n");
|
|
@@ -110,18 +145,6 @@ function compareQuotaBuckets(
|
|
|
110
145
|
return (left.resetTime ?? "").localeCompare(right.resetTime ?? "");
|
|
111
146
|
}
|
|
112
147
|
|
|
113
|
-
function formatQuotaBucketLine(bucket: RetrieveUserQuotaBucket): string {
|
|
114
|
-
const modelId = bucket.modelId?.trim() || "unknown-model";
|
|
115
|
-
const tokenType = bucket.tokenType?.trim();
|
|
116
|
-
const usageRemaining = formatUsageRemaining(bucket);
|
|
117
|
-
const resetLabel = formatRelativeResetTime(bucket.resetTime);
|
|
118
|
-
const subject = tokenType ? `${modelId} (${tokenType})` : modelId;
|
|
119
|
-
|
|
120
|
-
return resetLabel
|
|
121
|
-
? `- ${subject}: ${usageRemaining}, ${resetLabel}`
|
|
122
|
-
: `- ${subject}: ${usageRemaining}`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
148
|
function formatUsageRemaining(bucket: RetrieveUserQuotaBucket): string {
|
|
126
149
|
const remainingAmount = formatRemainingAmount(bucket.remainingAmount);
|
|
127
150
|
const remainingFraction = bucket.remainingFraction;
|
|
@@ -129,17 +152,19 @@ function formatUsageRemaining(bucket: RetrieveUserQuotaBucket): string {
|
|
|
129
152
|
typeof remainingFraction === "number" && Number.isFinite(remainingFraction);
|
|
130
153
|
|
|
131
154
|
if (hasFraction) {
|
|
132
|
-
const
|
|
155
|
+
const clamped = clamp(remainingFraction, 0, 1);
|
|
156
|
+
const percent = (clamped * 100).toFixed(1);
|
|
157
|
+
const bar = buildProgressBar(clamped);
|
|
133
158
|
return remainingAmount
|
|
134
|
-
? `${percent}%
|
|
135
|
-
: `${percent}
|
|
159
|
+
? `${bar} ${percent}% (${remainingAmount} left)`
|
|
160
|
+
: `${bar} ${percent}%`;
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
if (remainingAmount) {
|
|
139
|
-
return
|
|
164
|
+
return remainingAmount;
|
|
140
165
|
}
|
|
141
166
|
|
|
142
|
-
return "
|
|
167
|
+
return "unknown";
|
|
143
168
|
}
|
|
144
169
|
|
|
145
170
|
function formatRemainingAmount(value: string | undefined): string | undefined {
|
|
@@ -180,3 +205,178 @@ export function formatRelativeResetTime(resetTime: string | undefined): string |
|
|
|
180
205
|
}
|
|
181
206
|
return `resets in ${minutes}m`;
|
|
182
207
|
}
|
|
208
|
+
|
|
209
|
+
function buildProgressBar(fraction: number, width = 20): string {
|
|
210
|
+
const clamped = clamp(fraction, 0, 1);
|
|
211
|
+
const filled = clamped >= 1
|
|
212
|
+
? width
|
|
213
|
+
: Math.max(0, Math.min(width, Math.max(clamped > 0 ? 1 : 0, Math.floor(clamped * width))));
|
|
214
|
+
const empty = width - filled;
|
|
215
|
+
return `${"▓".repeat(filled)}${"░".repeat(empty)}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function pad(value: string, width: number): string {
|
|
219
|
+
if (value.length >= width) {
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
return value.padEnd(width, " ");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function clamp(value: number, min: number, max: number): number {
|
|
226
|
+
if (value < min) {
|
|
227
|
+
return min;
|
|
228
|
+
}
|
|
229
|
+
if (value > max) {
|
|
230
|
+
return max;
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeTokenType(bucket: RetrieveUserQuotaBucket): string {
|
|
236
|
+
const value = bucket.tokenType?.trim();
|
|
237
|
+
return value ? value.toUpperCase() : "REQUESTS";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface GroupedQuotaRow {
|
|
241
|
+
variant: string;
|
|
242
|
+
usageRemaining: string;
|
|
243
|
+
resetValue: string;
|
|
244
|
+
tokenType: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
interface GroupedQuotaModel {
|
|
248
|
+
baseModel: string;
|
|
249
|
+
version: string | undefined;
|
|
250
|
+
rows: GroupedQuotaRow[];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function groupQuotaRows(sortedBuckets: RetrieveUserQuotaBucket[]): GroupedQuotaModel[] {
|
|
254
|
+
const groups = new Map<string, GroupedQuotaModel>();
|
|
255
|
+
|
|
256
|
+
for (const bucket of sortedBuckets) {
|
|
257
|
+
const modelId = bucket.modelId?.trim() || "unknown-model";
|
|
258
|
+
const { baseModel, variant } = splitModelVariant(modelId);
|
|
259
|
+
const usageRemaining = formatUsageRemaining(bucket);
|
|
260
|
+
const resetLabel = formatRelativeResetTime(bucket.resetTime);
|
|
261
|
+
const resetValue = resetLabel?.replace("resets in ", "") ?? "-";
|
|
262
|
+
const tokenType = normalizeTokenType(bucket);
|
|
263
|
+
|
|
264
|
+
const existing = groups.get(baseModel);
|
|
265
|
+
if (existing) {
|
|
266
|
+
existing.rows.push({
|
|
267
|
+
variant,
|
|
268
|
+
usageRemaining,
|
|
269
|
+
resetValue,
|
|
270
|
+
tokenType,
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
groups.set(baseModel, {
|
|
276
|
+
baseModel,
|
|
277
|
+
version: extractModelVersion(baseModel),
|
|
278
|
+
rows: [{
|
|
279
|
+
variant,
|
|
280
|
+
usageRemaining,
|
|
281
|
+
resetValue,
|
|
282
|
+
tokenType,
|
|
283
|
+
}],
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return [...groups.values()];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface VersionQuotaGroup {
|
|
291
|
+
title: string;
|
|
292
|
+
version: string | undefined;
|
|
293
|
+
models: GroupedQuotaModel[];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function groupByVersion(models: GroupedQuotaModel[]): VersionQuotaGroup[] {
|
|
297
|
+
const groups = new Map<string, VersionQuotaGroup>();
|
|
298
|
+
|
|
299
|
+
for (const model of models) {
|
|
300
|
+
const key = model.version ?? "__unknown__";
|
|
301
|
+
const existing = groups.get(key);
|
|
302
|
+
if (existing) {
|
|
303
|
+
existing.models.push(model);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
groups.set(key, {
|
|
308
|
+
title: model.version ? `Gemini ${model.version}` : "Other",
|
|
309
|
+
version: model.version,
|
|
310
|
+
models: [model],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const ordered = [...groups.values()].sort((left, right) =>
|
|
315
|
+
compareVersionDesc(left.version, right.version),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
for (const group of ordered) {
|
|
319
|
+
group.models.sort((left, right) => left.baseModel.localeCompare(right.baseModel));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return ordered;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function extractModelVersion(modelId: string): string | undefined {
|
|
326
|
+
const match = modelId.match(/^gemini-([0-9]+(?:\.[0-9]+)*)-/i);
|
|
327
|
+
return match?.[1];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function compareVersionDesc(left: string | undefined, right: string | undefined): number {
|
|
331
|
+
if (!left && !right) {
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
if (!left) {
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
if (!right) {
|
|
338
|
+
return -1;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const leftSegments = left.split(".").map((part) => Number.parseInt(part, 10));
|
|
342
|
+
const rightSegments = right.split(".").map((part) => Number.parseInt(part, 10));
|
|
343
|
+
const max = Math.max(leftSegments.length, rightSegments.length);
|
|
344
|
+
|
|
345
|
+
for (let index = 0; index < max; index += 1) {
|
|
346
|
+
const l = leftSegments[index] ?? 0;
|
|
347
|
+
const r = rightSegments[index] ?? 0;
|
|
348
|
+
if (Number.isNaN(l) || Number.isNaN(r)) {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
if (l > r) {
|
|
352
|
+
return -1;
|
|
353
|
+
}
|
|
354
|
+
if (l < r) {
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return right.localeCompare(left);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function formatVersionGroupTitle(group: VersionQuotaGroup): string {
|
|
363
|
+
const modelCount = group.models.length;
|
|
364
|
+
const bucketCount = group.models.reduce((count, model) => count + model.rows.length, 0);
|
|
365
|
+
const modelLabel = modelCount === 1 ? "model" : "models";
|
|
366
|
+
const bucketLabel = bucketCount === 1 ? "bucket" : "buckets";
|
|
367
|
+
return `${group.title} (${modelCount} ${modelLabel}, ${bucketCount} ${bucketLabel})`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function splitModelVariant(modelId: string): { baseModel: string; variant: string } {
|
|
371
|
+
const vertexSuffix = "_vertex";
|
|
372
|
+
if (modelId.endsWith(vertexSuffix)) {
|
|
373
|
+
return {
|
|
374
|
+
baseModel: modelId.slice(0, -vertexSuffix.length),
|
|
375
|
+
variant: "vertex",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
baseModel: modelId,
|
|
380
|
+
variant: "default",
|
|
381
|
+
};
|
|
382
|
+
}
|