@leo000001/opencode-quota-sidebar 3.0.0 → 3.0.2
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/CHANGELOG.md +14 -2
- package/CONTRIBUTING.md +4 -1
- package/README.md +210 -514
- package/README.zh-CN.md +337 -0
- package/SECURITY.md +2 -2
- package/assets/OpenCode-Quota-Sidebar.png +0 -0
- package/dist/cost.d.ts +3 -3
- package/dist/cost.js +258 -169
- package/dist/format.d.ts +4 -0
- package/dist/format.js +24 -10
- package/dist/providers/common.d.ts +6 -0
- package/dist/providers/common.js +32 -12
- package/dist/providers/core/anthropic.d.ts +1 -1
- package/dist/providers/core/anthropic.js +43 -39
- package/dist/providers/core/kimi_for_coding.d.ts +1 -1
- package/dist/providers/core/kimi_for_coding.js +44 -64
- package/dist/providers/core/minimax_cn_coding_plan.d.ts +2 -0
- package/dist/providers/core/minimax_cn_coding_plan.js +214 -0
- package/dist/providers/core/zhipu_coding_plan.d.ts +1 -1
- package/dist/providers/core/zhipu_coding_plan.js +41 -61
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +5 -5
- package/dist/providers/third_party/rightcode.d.ts +1 -1
- package/dist/providers/third_party/rightcode.js +41 -61
- package/dist/providers/third_party/xyai.d.ts +2 -0
- package/dist/providers/third_party/{xyai_vibe.js → xyai.js} +113 -79
- package/dist/quota.d.ts +2 -2
- package/dist/quota.js +24 -18
- package/dist/quota_render.d.ts +1 -1
- package/dist/quota_render.js +23 -17
- package/dist/storage_parse.js +1 -0
- package/dist/title.js +7 -7
- package/dist/title_apply.js +18 -1
- package/dist/tui.tsx +133 -36
- package/dist/tui_helpers.d.ts +16 -0
- package/dist/tui_helpers.js +146 -0
- package/dist/types.d.ts +2 -0
- package/package.json +9 -2
- package/quota-sidebar.config.example.json +45 -45
- package/dist/providers/third_party/buzz.d.ts +0 -2
- package/dist/providers/third_party/buzz.js +0 -156
- package/dist/providers/third_party/xyai_vibe.d.ts +0 -2
package/dist/format.js
CHANGED
|
@@ -219,12 +219,12 @@ function compactProviderLabel(quota) {
|
|
|
219
219
|
return 'Kimi';
|
|
220
220
|
if (canonical === 'zhipuai-coding-plan')
|
|
221
221
|
return 'Zhipu';
|
|
222
|
+
if (canonical === 'minimax-cn-coding-plan')
|
|
223
|
+
return 'MiniMax';
|
|
222
224
|
if (canonical === 'rightcode')
|
|
223
225
|
return 'RC';
|
|
224
|
-
if (canonical === 'xyai
|
|
226
|
+
if (canonical === 'xyai')
|
|
225
227
|
return 'XYAI';
|
|
226
|
-
if (canonical === 'buzz')
|
|
227
|
-
return 'Buzz';
|
|
228
228
|
return sanitizeLine(quotaDisplayLabel(quota));
|
|
229
229
|
}
|
|
230
230
|
function compactWindowToken(label) {
|
|
@@ -260,6 +260,13 @@ function compactQuotaPercentToken(label, percent) {
|
|
|
260
260
|
return rounded ? `R${rounded}` : '';
|
|
261
261
|
if (/^sonnet\s+7d$/i.test(safe))
|
|
262
262
|
return rounded ? `S7d${rounded}` : 'S7d';
|
|
263
|
+
if (/^opus\s+7d$/i.test(safe))
|
|
264
|
+
return rounded ? `O7d${rounded}` : 'O7d';
|
|
265
|
+
if (/^oauth\s+apps\s+7d$/i.test(safe)) {
|
|
266
|
+
return rounded ? `OA7d${rounded}` : 'OA7d';
|
|
267
|
+
}
|
|
268
|
+
if (/^cowork\s+7d$/i.test(safe))
|
|
269
|
+
return rounded ? `Co7d${rounded}` : 'Co7d';
|
|
263
270
|
const token = compactWindowToken(safe).replace(/\s+/g, '');
|
|
264
271
|
if (!rounded)
|
|
265
272
|
return token;
|
|
@@ -599,6 +606,9 @@ export function renderSidebarUsageLines(usage, config, options) {
|
|
|
599
606
|
}).map((line) => fitLine(line, width));
|
|
600
607
|
}
|
|
601
608
|
export function renderSidebarQuotaLines(quotas, config) {
|
|
609
|
+
return renderSidebarQuotaLineGroups(quotas, config).flatMap((group) => group.lines);
|
|
610
|
+
}
|
|
611
|
+
export function renderSidebarQuotaLineGroups(quotas, config) {
|
|
602
612
|
const width = Math.max(8, Math.floor(config.sidebar.width || 36));
|
|
603
613
|
const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
|
|
604
614
|
const labelWidth = visibleQuotas.reduce((max, item) => {
|
|
@@ -606,14 +616,18 @@ export function renderSidebarQuotaLines(quotas, config) {
|
|
|
606
616
|
return Math.max(max, stringCellWidth(label));
|
|
607
617
|
}, 0);
|
|
608
618
|
return visibleQuotas
|
|
609
|
-
.
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
619
|
+
.map((item) => ({
|
|
620
|
+
quota: item,
|
|
621
|
+
lines: compactQuotaWide(item, labelWidth, {
|
|
622
|
+
width,
|
|
623
|
+
wrapLines: config.sidebar.wrapQuotaLines,
|
|
624
|
+
forceWrapped: false,
|
|
625
|
+
compactDetails: true,
|
|
626
|
+
})
|
|
627
|
+
.filter((line) => Boolean(line))
|
|
628
|
+
.map((line) => fitLine(line, width)),
|
|
614
629
|
}))
|
|
615
|
-
.filter((
|
|
616
|
-
.map((line) => fitLine(line, width));
|
|
630
|
+
.filter((group) => group.lines.length > 0);
|
|
617
631
|
}
|
|
618
632
|
/**
|
|
619
633
|
* Multi-window quota format for sidebar.
|
|
@@ -21,5 +21,11 @@ export declare function configuredProviderEnabled(config: {
|
|
|
21
21
|
[key: string]: unknown;
|
|
22
22
|
}>;
|
|
23
23
|
}, adapterID: string, fallback?: boolean): boolean;
|
|
24
|
+
export declare function resolveApiKey(auth: {
|
|
25
|
+
type: "oauth" | "api" | "wellknown";
|
|
26
|
+
access?: string;
|
|
27
|
+
key?: string;
|
|
28
|
+
token?: string;
|
|
29
|
+
} | undefined, providerOptions: Record<string, unknown> | undefined): string | undefined;
|
|
24
30
|
export declare function sanitizeBaseURL(value: unknown): string | undefined;
|
|
25
31
|
export declare function basePathPrefixes(value: unknown): string[];
|
package/dist/providers/common.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { asNumber as asNumberShared, isRecord } from
|
|
1
|
+
import { asNumber as asNumberShared, isRecord } from "../helpers.js";
|
|
2
2
|
// Public OAuth client ID embedded in the ChatGPT web app (not a private secret).
|
|
3
3
|
// Source: https://github.com/vbgate/opencode-mystatus (reverse-engineered from browser client).
|
|
4
4
|
// If OpenAI rotates this value, update it here or expose it via quota-sidebar.config.json.
|
|
5
|
-
export const OPENAI_OAUTH_CLIENT_ID =
|
|
5
|
+
export const OPENAI_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
6
6
|
export async function fetchWithTimeout(url, init, timeoutMs) {
|
|
7
7
|
const controller = new AbortController();
|
|
8
8
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -30,7 +30,7 @@ export function normalizePercent(value) {
|
|
|
30
30
|
return expanded;
|
|
31
31
|
}
|
|
32
32
|
export function toIso(value) {
|
|
33
|
-
if (typeof value ===
|
|
33
|
+
if (typeof value === "string") {
|
|
34
34
|
const time = Date.parse(value);
|
|
35
35
|
if (!Number.isNaN(time))
|
|
36
36
|
return new Date(time).toISOString();
|
|
@@ -46,7 +46,7 @@ export function toIso(value) {
|
|
|
46
46
|
* Derive a human-readable window label from `limit_window_seconds`.
|
|
47
47
|
* Uses fallback label when the limit is missing.
|
|
48
48
|
*/
|
|
49
|
-
export function windowLabel(win, fallback =
|
|
49
|
+
export function windowLabel(win, fallback = "") {
|
|
50
50
|
const limitSec = asNumber(win.limit_window_seconds);
|
|
51
51
|
if (limitSec !== undefined && limitSec > 0) {
|
|
52
52
|
const hours = limitSec / 3600;
|
|
@@ -55,9 +55,9 @@ export function windowLabel(win, fallback = '') {
|
|
|
55
55
|
const days = hours / 24;
|
|
56
56
|
if (days <= 6)
|
|
57
57
|
return `${Math.round(days)}d`;
|
|
58
|
-
return
|
|
58
|
+
return "Weekly";
|
|
59
59
|
}
|
|
60
|
-
return fallback ||
|
|
60
|
+
return fallback || "Window";
|
|
61
61
|
}
|
|
62
62
|
export function parseRateLimitWindow(win, fallbackLabel) {
|
|
63
63
|
const usedPercent = normalizePercent(win.used_percent);
|
|
@@ -77,16 +77,36 @@ export function asRecord(value) {
|
|
|
77
77
|
}
|
|
78
78
|
export function configuredProviderEnabled(config, adapterID, fallback = true) {
|
|
79
79
|
const enabled = config.providers?.[adapterID]?.enabled;
|
|
80
|
-
if (typeof enabled ===
|
|
80
|
+
if (typeof enabled === "boolean")
|
|
81
81
|
return enabled;
|
|
82
82
|
return fallback;
|
|
83
83
|
}
|
|
84
|
+
export function resolveApiKey(auth, providerOptions) {
|
|
85
|
+
const optionKey = providerOptions?.apiKey;
|
|
86
|
+
if (typeof optionKey === "string" && optionKey)
|
|
87
|
+
return optionKey;
|
|
88
|
+
if (!auth)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (auth.type === "api" && typeof auth.key === "string" && auth.key) {
|
|
91
|
+
return auth.key;
|
|
92
|
+
}
|
|
93
|
+
if (auth.type === "wellknown") {
|
|
94
|
+
if (typeof auth.key === "string" && auth.key)
|
|
95
|
+
return auth.key;
|
|
96
|
+
if (typeof auth.token === "string" && auth.token)
|
|
97
|
+
return auth.token;
|
|
98
|
+
}
|
|
99
|
+
if (auth.type === "oauth" && typeof auth.access === "string" && auth.access) {
|
|
100
|
+
return auth.access;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
84
104
|
export function sanitizeBaseURL(value) {
|
|
85
|
-
if (typeof value !==
|
|
105
|
+
if (typeof value !== "string" || !value)
|
|
86
106
|
return undefined;
|
|
87
107
|
try {
|
|
88
108
|
const parsed = new URL(value);
|
|
89
|
-
const pathname = parsed.pathname.replace(/\/+$/,
|
|
109
|
+
const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
90
110
|
return `${parsed.origin}${pathname}`;
|
|
91
111
|
}
|
|
92
112
|
catch {
|
|
@@ -99,13 +119,13 @@ export function basePathPrefixes(value) {
|
|
|
99
119
|
return [];
|
|
100
120
|
try {
|
|
101
121
|
const parsed = new URL(sanitized);
|
|
102
|
-
const parts = parsed.pathname.split(
|
|
122
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
103
123
|
const prefixes = [];
|
|
104
124
|
for (let i = parts.length; i >= 1; i--) {
|
|
105
|
-
prefixes.push(`/${parts.slice(0, i).join(
|
|
125
|
+
prefixes.push(`/${parts.slice(0, i).join("/")}`);
|
|
106
126
|
}
|
|
107
127
|
if (prefixes.length === 0)
|
|
108
|
-
prefixes.push(
|
|
128
|
+
prefixes.push("/");
|
|
109
129
|
return prefixes;
|
|
110
130
|
}
|
|
111
131
|
catch {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { QuotaProviderAdapter } from
|
|
1
|
+
import type { QuotaProviderAdapter } from "../types.js";
|
|
2
2
|
export declare const anthropicAdapter: QuotaProviderAdapter;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { swallow } from
|
|
2
|
-
import { asRecord, configuredProviderEnabled, fetchWithTimeout, normalizePercent, toIso, } from
|
|
3
|
-
const ANTHROPIC_OAUTH_USAGE_BETA =
|
|
1
|
+
import { swallow } from "../../helpers.js";
|
|
2
|
+
import { asRecord, configuredProviderEnabled, fetchWithTimeout, normalizePercent, toIso, } from "../common.js";
|
|
3
|
+
const ANTHROPIC_OAUTH_USAGE_BETA = "oauth-2025-04-20";
|
|
4
|
+
const ANTHROPIC_WINDOW_FIELDS = [
|
|
5
|
+
["five_hour", "5h"],
|
|
6
|
+
["seven_day", "Weekly"],
|
|
7
|
+
["seven_day_sonnet", "Sonnet 7d"],
|
|
8
|
+
["seven_day_opus", "Opus 7d"],
|
|
9
|
+
["seven_day_oauth_apps", "OAuth Apps 7d"],
|
|
10
|
+
["seven_day_cowork", "Cowork 7d"],
|
|
11
|
+
];
|
|
4
12
|
function parseAnthropicWindow(value, label) {
|
|
5
13
|
const win = asRecord(value);
|
|
6
14
|
if (!win)
|
|
@@ -20,96 +28,92 @@ async function fetchAnthropicQuota({ providerID, auth, config, }) {
|
|
|
20
28
|
const checkedAt = Date.now();
|
|
21
29
|
const base = {
|
|
22
30
|
providerID,
|
|
23
|
-
adapterID:
|
|
24
|
-
label:
|
|
25
|
-
shortLabel:
|
|
31
|
+
adapterID: "anthropic",
|
|
32
|
+
label: "Anthropic",
|
|
33
|
+
shortLabel: "Anthropic",
|
|
26
34
|
sortOrder: 30,
|
|
27
35
|
};
|
|
28
36
|
if (!auth) {
|
|
29
37
|
return {
|
|
30
38
|
...base,
|
|
31
|
-
status:
|
|
39
|
+
status: "unavailable",
|
|
32
40
|
checkedAt,
|
|
33
|
-
note:
|
|
41
|
+
note: "auth not found",
|
|
34
42
|
};
|
|
35
43
|
}
|
|
36
|
-
if (auth.type !==
|
|
44
|
+
if (auth.type !== "oauth") {
|
|
37
45
|
return {
|
|
38
46
|
...base,
|
|
39
|
-
status:
|
|
47
|
+
status: "unsupported",
|
|
40
48
|
checkedAt,
|
|
41
|
-
note:
|
|
49
|
+
note: "api key auth has no quota endpoint",
|
|
42
50
|
};
|
|
43
51
|
}
|
|
44
|
-
if (typeof auth.access !==
|
|
52
|
+
if (typeof auth.access !== "string" || !auth.access) {
|
|
45
53
|
return {
|
|
46
54
|
...base,
|
|
47
|
-
status:
|
|
55
|
+
status: "unavailable",
|
|
48
56
|
checkedAt,
|
|
49
|
-
note:
|
|
57
|
+
note: "missing oauth access token",
|
|
50
58
|
};
|
|
51
59
|
}
|
|
52
|
-
const response = await fetchWithTimeout(
|
|
53
|
-
method:
|
|
60
|
+
const response = await fetchWithTimeout("https://api.anthropic.com/api/oauth/usage", {
|
|
61
|
+
method: "GET",
|
|
54
62
|
headers: {
|
|
55
|
-
Accept:
|
|
63
|
+
Accept: "application/json",
|
|
56
64
|
Authorization: `Bearer ${auth.access}`,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"User-Agent": "opencode-quota-sidebar",
|
|
67
|
+
"anthropic-beta": ANTHROPIC_OAUTH_USAGE_BETA,
|
|
60
68
|
},
|
|
61
|
-
}, config.quota.requestTimeoutMs).catch(swallow(
|
|
69
|
+
}, config.quota.requestTimeoutMs).catch(swallow("fetchAnthropicQuota:usage"));
|
|
62
70
|
if (!response) {
|
|
63
71
|
return {
|
|
64
72
|
...base,
|
|
65
|
-
status:
|
|
73
|
+
status: "error",
|
|
66
74
|
checkedAt,
|
|
67
|
-
note:
|
|
75
|
+
note: "network request failed",
|
|
68
76
|
};
|
|
69
77
|
}
|
|
70
78
|
if (!response.ok) {
|
|
71
79
|
return {
|
|
72
80
|
...base,
|
|
73
|
-
status:
|
|
81
|
+
status: "error",
|
|
74
82
|
checkedAt,
|
|
75
83
|
note: `http ${response.status}`,
|
|
76
84
|
};
|
|
77
85
|
}
|
|
78
86
|
const payload = await response
|
|
79
87
|
.json()
|
|
80
|
-
.catch(swallow(
|
|
88
|
+
.catch(swallow("fetchAnthropicQuota:json"));
|
|
81
89
|
const usage = asRecord(payload);
|
|
82
90
|
if (!usage) {
|
|
83
91
|
return {
|
|
84
92
|
...base,
|
|
85
|
-
status:
|
|
93
|
+
status: "error",
|
|
86
94
|
checkedAt,
|
|
87
|
-
note:
|
|
95
|
+
note: "invalid response",
|
|
88
96
|
};
|
|
89
97
|
}
|
|
90
|
-
const windows = [
|
|
91
|
-
parseAnthropicWindow(usage.five_hour, '5h'),
|
|
92
|
-
parseAnthropicWindow(usage.seven_day, 'Weekly'),
|
|
93
|
-
parseAnthropicWindow(usage.seven_day_sonnet, 'Sonnet 7d'),
|
|
94
|
-
].filter((window) => Boolean(window));
|
|
98
|
+
const windows = ANTHROPIC_WINDOW_FIELDS.map(([field, label]) => parseAnthropicWindow(usage[field], label)).filter((window) => Boolean(window));
|
|
95
99
|
const primary = windows[0];
|
|
96
100
|
return {
|
|
97
101
|
...base,
|
|
98
|
-
status: primary ?
|
|
102
|
+
status: primary ? "ok" : "error",
|
|
99
103
|
checkedAt,
|
|
100
104
|
usedPercent: primary?.usedPercent,
|
|
101
105
|
remainingPercent: primary?.remainingPercent,
|
|
102
106
|
resetAt: primary?.resetAt,
|
|
103
|
-
note: primary ? undefined :
|
|
107
|
+
note: primary ? undefined : "missing quota fields",
|
|
104
108
|
windows: windows.length > 0 ? windows : undefined,
|
|
105
109
|
};
|
|
106
110
|
}
|
|
107
111
|
export const anthropicAdapter = {
|
|
108
|
-
id:
|
|
109
|
-
label:
|
|
110
|
-
shortLabel:
|
|
112
|
+
id: "anthropic",
|
|
113
|
+
label: "Anthropic",
|
|
114
|
+
shortLabel: "Anthropic",
|
|
111
115
|
sortOrder: 30,
|
|
112
|
-
matchScore: ({ providerID }) => (providerID ===
|
|
113
|
-
isEnabled: (config) => configuredProviderEnabled(config.quota,
|
|
116
|
+
matchScore: ({ providerID }) => (providerID === "anthropic" ? 80 : 0),
|
|
117
|
+
isEnabled: (config) => configuredProviderEnabled(config.quota, "anthropic", config.quota.includeAnthropic),
|
|
114
118
|
fetch: fetchAnthropicQuota,
|
|
115
119
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { QuotaProviderAdapter } from
|
|
1
|
+
import type { QuotaProviderAdapter } from "../types.js";
|
|
2
2
|
export declare const kimiForCodingAdapter: QuotaProviderAdapter;
|
|
@@ -1,36 +1,16 @@
|
|
|
1
|
-
import { isRecord, swallow } from
|
|
2
|
-
import { asNumber, configuredProviderEnabled, fetchWithTimeout, sanitizeBaseURL, toIso, } from
|
|
3
|
-
const KIMI_FOR_CODING_BASE_URL =
|
|
4
|
-
function resolveApiKey(auth, providerOptions) {
|
|
5
|
-
const optionKey = providerOptions?.apiKey;
|
|
6
|
-
if (typeof optionKey === 'string' && optionKey)
|
|
7
|
-
return optionKey;
|
|
8
|
-
if (!auth)
|
|
9
|
-
return undefined;
|
|
10
|
-
if (auth.type === 'api' && typeof auth.key === 'string' && auth.key) {
|
|
11
|
-
return auth.key;
|
|
12
|
-
}
|
|
13
|
-
if (auth.type === 'wellknown') {
|
|
14
|
-
if (typeof auth.key === 'string' && auth.key)
|
|
15
|
-
return auth.key;
|
|
16
|
-
if (typeof auth.token === 'string' && auth.token)
|
|
17
|
-
return auth.token;
|
|
18
|
-
}
|
|
19
|
-
if (auth.type === 'oauth' && typeof auth.access === 'string' && auth.access) {
|
|
20
|
-
return auth.access;
|
|
21
|
-
}
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
1
|
+
import { isRecord, swallow } from "../../helpers.js";
|
|
2
|
+
import { asNumber, configuredProviderEnabled, fetchWithTimeout, resolveApiKey, sanitizeBaseURL, toIso, } from "../common.js";
|
|
3
|
+
const KIMI_FOR_CODING_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
24
4
|
function isKimiCodingBaseURL(value) {
|
|
25
5
|
const normalized = sanitizeBaseURL(value);
|
|
26
6
|
if (!normalized)
|
|
27
7
|
return false;
|
|
28
8
|
try {
|
|
29
9
|
const parsed = new URL(normalized);
|
|
30
|
-
if (parsed.protocol !==
|
|
10
|
+
if (parsed.protocol !== "https:")
|
|
31
11
|
return false;
|
|
32
|
-
const pathname = parsed.pathname.replace(/\/+$/,
|
|
33
|
-
return parsed.host ===
|
|
12
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
13
|
+
return parsed.host === "api.kimi.com" && pathname === "/coding/v1";
|
|
34
14
|
}
|
|
35
15
|
catch {
|
|
36
16
|
return false;
|
|
@@ -45,9 +25,9 @@ function usagesUrl(baseURL) {
|
|
|
45
25
|
}
|
|
46
26
|
function percentFromQuota(limit, remaining) {
|
|
47
27
|
const total = asNumber(limit) ??
|
|
48
|
-
(typeof limit ===
|
|
28
|
+
(typeof limit === "string" && limit.trim() ? Number(limit) : undefined);
|
|
49
29
|
const left = asNumber(remaining) ??
|
|
50
|
-
(typeof remaining ===
|
|
30
|
+
(typeof remaining === "string" && remaining.trim()
|
|
51
31
|
? Number(remaining)
|
|
52
32
|
: undefined);
|
|
53
33
|
if (total === undefined || left === undefined || total <= 0)
|
|
@@ -57,27 +37,27 @@ function percentFromQuota(limit, remaining) {
|
|
|
57
37
|
return Math.max(0, Math.min(100, (left / total) * 100));
|
|
58
38
|
}
|
|
59
39
|
function windowLabel(duration, timeUnit) {
|
|
60
|
-
if (timeUnit ===
|
|
61
|
-
return
|
|
62
|
-
if (timeUnit ===
|
|
63
|
-
return
|
|
64
|
-
if (timeUnit ===
|
|
40
|
+
if (timeUnit === "TIME_UNIT_MINUTE" && duration === 300)
|
|
41
|
+
return "5h";
|
|
42
|
+
if (timeUnit === "TIME_UNIT_DAY" && duration === 7)
|
|
43
|
+
return "Weekly";
|
|
44
|
+
if (timeUnit === "TIME_UNIT_MINUTE" && duration && duration > 0) {
|
|
65
45
|
const hours = duration / 60;
|
|
66
46
|
if (hours <= 24)
|
|
67
47
|
return `${Math.round(hours)}h`;
|
|
68
48
|
}
|
|
69
|
-
if (timeUnit ===
|
|
49
|
+
if (timeUnit === "TIME_UNIT_HOUR" && duration && duration > 0) {
|
|
70
50
|
if (duration <= 24)
|
|
71
51
|
return `${Math.round(duration)}h`;
|
|
72
52
|
const days = duration / 24;
|
|
73
53
|
if (days <= 6)
|
|
74
54
|
return `${Math.round(days)}d`;
|
|
75
55
|
}
|
|
76
|
-
if (timeUnit ===
|
|
56
|
+
if (timeUnit === "TIME_UNIT_DAY" && duration && duration > 0) {
|
|
77
57
|
if (duration <= 6)
|
|
78
58
|
return `${Math.round(duration)}d`;
|
|
79
59
|
if (duration === 7)
|
|
80
|
-
return
|
|
60
|
+
return "Weekly";
|
|
81
61
|
}
|
|
82
62
|
return undefined;
|
|
83
63
|
}
|
|
@@ -89,7 +69,7 @@ function parseWindow(value) {
|
|
|
89
69
|
if (!window || !detail)
|
|
90
70
|
return undefined;
|
|
91
71
|
const duration = asNumber(window.duration);
|
|
92
|
-
const timeUnit = typeof window.timeUnit ===
|
|
72
|
+
const timeUnit = typeof window.timeUnit === "string" ? window.timeUnit : undefined;
|
|
93
73
|
const label = windowLabel(duration, timeUnit);
|
|
94
74
|
const remainingPercent = percentFromQuota(detail.limit, detail.remaining);
|
|
95
75
|
if (!label || remainingPercent === undefined)
|
|
@@ -104,7 +84,7 @@ function dedupeWindows(windows) {
|
|
|
104
84
|
const seen = new Set();
|
|
105
85
|
const deduped = [];
|
|
106
86
|
for (const window of windows) {
|
|
107
|
-
const key = `${window.label}|${window.resetAt ||
|
|
87
|
+
const key = `${window.label}|${window.resetAt || ""}|${window.remainingPercent ?? ""}`;
|
|
108
88
|
if (seen.has(key))
|
|
109
89
|
continue;
|
|
110
90
|
seen.add(key);
|
|
@@ -116,53 +96,53 @@ async function fetchKimiForCodingQuota({ providerID, providerOptions, auth, conf
|
|
|
116
96
|
const checkedAt = Date.now();
|
|
117
97
|
const base = {
|
|
118
98
|
providerID,
|
|
119
|
-
adapterID:
|
|
120
|
-
label:
|
|
121
|
-
shortLabel:
|
|
99
|
+
adapterID: "kimi-for-coding",
|
|
100
|
+
label: "Kimi For Coding",
|
|
101
|
+
shortLabel: "Kimi",
|
|
122
102
|
sortOrder: 15,
|
|
123
103
|
};
|
|
124
104
|
const apiKey = resolveApiKey(auth, providerOptions);
|
|
125
105
|
if (!apiKey) {
|
|
126
106
|
return {
|
|
127
107
|
...base,
|
|
128
|
-
status:
|
|
108
|
+
status: "unavailable",
|
|
129
109
|
checkedAt,
|
|
130
|
-
note:
|
|
110
|
+
note: "missing api key",
|
|
131
111
|
};
|
|
132
112
|
}
|
|
133
113
|
const response = await fetchWithTimeout(usagesUrl(providerOptions?.baseURL), {
|
|
134
|
-
method:
|
|
114
|
+
method: "GET",
|
|
135
115
|
headers: {
|
|
136
|
-
Accept:
|
|
116
|
+
Accept: "application/json",
|
|
137
117
|
Authorization: `Bearer ${apiKey}`,
|
|
138
|
-
|
|
118
|
+
"User-Agent": "opencode-quota-sidebar",
|
|
139
119
|
},
|
|
140
|
-
}, config.quota.requestTimeoutMs).catch(swallow(
|
|
120
|
+
}, config.quota.requestTimeoutMs).catch(swallow("fetchKimiForCodingQuota:usage"));
|
|
141
121
|
if (!response) {
|
|
142
122
|
return {
|
|
143
123
|
...base,
|
|
144
|
-
status:
|
|
124
|
+
status: "error",
|
|
145
125
|
checkedAt,
|
|
146
|
-
note:
|
|
126
|
+
note: "network request failed",
|
|
147
127
|
};
|
|
148
128
|
}
|
|
149
129
|
if (!response.ok) {
|
|
150
130
|
return {
|
|
151
131
|
...base,
|
|
152
|
-
status:
|
|
132
|
+
status: "error",
|
|
153
133
|
checkedAt,
|
|
154
134
|
note: `http ${response.status}`,
|
|
155
135
|
};
|
|
156
136
|
}
|
|
157
137
|
const payload = await response
|
|
158
138
|
.json()
|
|
159
|
-
.catch(swallow(
|
|
139
|
+
.catch(swallow("fetchKimiForCodingQuota:json"));
|
|
160
140
|
if (!isRecord(payload)) {
|
|
161
141
|
return {
|
|
162
142
|
...base,
|
|
163
|
-
status:
|
|
143
|
+
status: "error",
|
|
164
144
|
checkedAt,
|
|
165
|
-
note:
|
|
145
|
+
note: "invalid response",
|
|
166
146
|
};
|
|
167
147
|
}
|
|
168
148
|
const windows = Array.isArray(payload.limits)
|
|
@@ -177,16 +157,16 @@ async function fetchKimiForCodingQuota({ providerID, providerOptions, auth, conf
|
|
|
177
157
|
...windows,
|
|
178
158
|
topLevelRemainingPercent !== undefined
|
|
179
159
|
? {
|
|
180
|
-
label:
|
|
160
|
+
label: "Weekly",
|
|
181
161
|
remainingPercent: topLevelRemainingPercent,
|
|
182
162
|
resetAt: topLevelResetAt,
|
|
183
163
|
}
|
|
184
164
|
: undefined,
|
|
185
165
|
].filter((value) => Boolean(value))).sort((left, right) => {
|
|
186
166
|
const order = (label) => {
|
|
187
|
-
if (label ===
|
|
167
|
+
if (label === "5h")
|
|
188
168
|
return 0;
|
|
189
|
-
if (label ===
|
|
169
|
+
if (label === "Weekly")
|
|
190
170
|
return 1;
|
|
191
171
|
return 2;
|
|
192
172
|
};
|
|
@@ -195,25 +175,25 @@ async function fetchKimiForCodingQuota({ providerID, providerOptions, auth, conf
|
|
|
195
175
|
const primary = allWindows[0];
|
|
196
176
|
return {
|
|
197
177
|
...base,
|
|
198
|
-
status: primary ?
|
|
178
|
+
status: primary ? "ok" : "error",
|
|
199
179
|
checkedAt,
|
|
200
180
|
remainingPercent: primary?.remainingPercent,
|
|
201
181
|
resetAt: primary?.resetAt,
|
|
202
|
-
note: primary ? undefined :
|
|
182
|
+
note: primary ? undefined : "missing quota fields",
|
|
203
183
|
windows: allWindows.length > 0 ? allWindows : undefined,
|
|
204
184
|
};
|
|
205
185
|
}
|
|
206
186
|
export const kimiForCodingAdapter = {
|
|
207
|
-
id:
|
|
208
|
-
label:
|
|
209
|
-
shortLabel:
|
|
187
|
+
id: "kimi-for-coding",
|
|
188
|
+
label: "Kimi For Coding",
|
|
189
|
+
shortLabel: "Kimi",
|
|
210
190
|
sortOrder: 15,
|
|
211
|
-
normalizeID: (providerID) => providerID ===
|
|
191
|
+
normalizeID: (providerID) => providerID === "kimi-for-coding" ? "kimi-for-coding" : undefined,
|
|
212
192
|
matchScore: ({ providerID, providerOptions }) => {
|
|
213
|
-
if (providerID ===
|
|
193
|
+
if (providerID === "kimi-for-coding")
|
|
214
194
|
return 100;
|
|
215
195
|
return isKimiCodingBaseURL(providerOptions?.baseURL) ? 95 : 0;
|
|
216
196
|
},
|
|
217
|
-
isEnabled: (config) => configuredProviderEnabled(config.quota,
|
|
197
|
+
isEnabled: (config) => configuredProviderEnabled(config.quota, "kimi-for-coding", true),
|
|
218
198
|
fetch: fetchKimiForCodingQuota,
|
|
219
199
|
};
|