@leo000001/opencode-quota-sidebar 3.0.1 → 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.js +10 -3
- 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 +2 -1
- package/dist/tui_helpers.d.ts +2 -1
- package/dist/tui_helpers.js +6 -1
- 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/quota.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import { isRecord, swallow } from
|
|
3
|
-
import { createDefaultProviderRegistry } from
|
|
4
|
-
import { sanitizeBaseURL } from
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { isRecord, swallow } from "./helpers.js";
|
|
3
|
+
import { createDefaultProviderRegistry } from "./providers/index.js";
|
|
4
|
+
import { sanitizeBaseURL } from "./providers/common.js";
|
|
5
5
|
function resolveContext(providerID, providerOptions) {
|
|
6
6
|
return { providerID, providerOptions };
|
|
7
7
|
}
|
|
8
8
|
function authCandidates(providerID, normalizedProviderID, adapterID) {
|
|
9
9
|
const candidates = new Set([providerID]);
|
|
10
|
-
if (adapterID ===
|
|
11
|
-
candidates.add(
|
|
10
|
+
if (adapterID === "github-copilot") {
|
|
11
|
+
candidates.add("github-copilot-enterprise");
|
|
12
12
|
}
|
|
13
13
|
candidates.add(normalizedProviderID);
|
|
14
14
|
candidates.add(adapterID);
|
|
@@ -34,11 +34,12 @@ export function quotaSort(left, right) {
|
|
|
34
34
|
export function listDefaultQuotaProviderIDs() {
|
|
35
35
|
// Keep default report behavior stable for built-in subscription providers.
|
|
36
36
|
return [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
"openai",
|
|
38
|
+
"kimi-for-coding",
|
|
39
|
+
"zhipuai-coding-plan",
|
|
40
|
+
"minimax-cn-coding-plan",
|
|
41
|
+
"github-copilot",
|
|
42
|
+
"anthropic",
|
|
42
43
|
];
|
|
43
44
|
}
|
|
44
45
|
export function createQuotaRuntime() {
|
|
@@ -58,10 +59,15 @@ export function createQuotaRuntime() {
|
|
|
58
59
|
if (adapter?.id && normalizedProviderID !== providerID) {
|
|
59
60
|
keyBase = `${adapter.id}:${providerID}`;
|
|
60
61
|
}
|
|
61
|
-
//
|
|
62
|
-
// RC-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Some third-party adapters intentionally preserve provider-specific labels
|
|
63
|
+
// (for example RC-openai or an XYAI alias) even when they share one adapter.
|
|
64
|
+
// Keep the original provider identity in cache keys so same-host aliases with
|
|
65
|
+
// different auth/config entries do not overwrite each other.
|
|
66
|
+
if ((adapter?.id === "rightcode" &&
|
|
67
|
+
normalizedProviderID.startsWith("rightcode-")) ||
|
|
68
|
+
(adapter?.id === "xyai" &&
|
|
69
|
+
providerID !== "xyai" &&
|
|
70
|
+
providerID !== "xyai-vibe")) {
|
|
65
71
|
keyBase = normalizedProviderID;
|
|
66
72
|
}
|
|
67
73
|
return baseURL ? `${keyBase}@${baseURL}` : keyBase;
|
|
@@ -110,16 +116,16 @@ export function quotaCacheKey(providerID, providerOptions) {
|
|
|
110
116
|
}
|
|
111
117
|
export async function loadAuthMap(authPath) {
|
|
112
118
|
const parsed = await fs
|
|
113
|
-
.readFile(authPath,
|
|
119
|
+
.readFile(authPath, "utf8")
|
|
114
120
|
.then((value) => JSON.parse(value))
|
|
115
|
-
.catch(swallow(
|
|
121
|
+
.catch(swallow("loadAuthMap"));
|
|
116
122
|
if (!isRecord(parsed))
|
|
117
123
|
return {};
|
|
118
124
|
return Object.entries(parsed).reduce((acc, [key, value]) => {
|
|
119
125
|
if (!isRecord(value))
|
|
120
126
|
return acc;
|
|
121
127
|
const type = value.type;
|
|
122
|
-
if (type !==
|
|
128
|
+
if (type !== "oauth" && type !== "api" && type !== "wellknown")
|
|
123
129
|
return acc;
|
|
124
130
|
acc[key] = value;
|
|
125
131
|
return acc;
|
package/dist/quota_render.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QuotaSnapshot } from
|
|
1
|
+
import type { QuotaSnapshot } from "./types.js";
|
|
2
2
|
export declare function canonicalProviderID(providerID: string): string;
|
|
3
3
|
export declare function displayShortLabel(providerID: string): string;
|
|
4
4
|
export declare function quotaDisplayLabel(quota: QuotaSnapshot): string;
|
package/dist/quota_render.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
const PROVIDER_SHORT_LABELS = {
|
|
2
|
-
openai:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
openai: "OpenAI",
|
|
3
|
+
"kimi-for-coding": "Kimi",
|
|
4
|
+
"zhipuai-coding-plan": "Zhipu",
|
|
5
|
+
"minimax-cn-coding-plan": "MiniMax",
|
|
6
|
+
"github-copilot": "Copilot",
|
|
7
|
+
anthropic: "Anthropic",
|
|
8
|
+
rightcode: "RC",
|
|
9
|
+
xyai: "XYAI",
|
|
8
10
|
};
|
|
9
11
|
export function canonicalProviderID(providerID) {
|
|
10
|
-
if (providerID.startsWith(
|
|
11
|
-
return
|
|
12
|
-
if (providerID ===
|
|
13
|
-
return
|
|
12
|
+
if (providerID.startsWith("github-copilot"))
|
|
13
|
+
return "github-copilot";
|
|
14
|
+
if (providerID === "zhipuai-coding-plan")
|
|
15
|
+
return "zhipuai-coding-plan";
|
|
16
|
+
if (providerID === "minimax-cn-coding-plan")
|
|
17
|
+
return "minimax-cn-coding-plan";
|
|
18
|
+
if (providerID === "xyai-vibe")
|
|
19
|
+
return "xyai";
|
|
14
20
|
return providerID;
|
|
15
21
|
}
|
|
16
22
|
export function displayShortLabel(providerID) {
|
|
@@ -18,8 +24,8 @@ export function displayShortLabel(providerID) {
|
|
|
18
24
|
const direct = PROVIDER_SHORT_LABELS[canonical];
|
|
19
25
|
if (direct)
|
|
20
26
|
return direct;
|
|
21
|
-
if (canonical.startsWith(
|
|
22
|
-
return `RC-${canonical.slice(
|
|
27
|
+
if (canonical.startsWith("rightcode-")) {
|
|
28
|
+
return `RC-${canonical.slice("rightcode-".length)}`;
|
|
23
29
|
}
|
|
24
30
|
return providerID;
|
|
25
31
|
}
|
|
@@ -34,13 +40,13 @@ export function quotaDisplayLabel(quota) {
|
|
|
34
40
|
return displayShortLabel(quota.providerID);
|
|
35
41
|
}
|
|
36
42
|
function quotaKey(quota) {
|
|
37
|
-
if (quota.adapterID ===
|
|
43
|
+
if (quota.adapterID === "rightcode")
|
|
38
44
|
return `rightcode:${quota.providerID}`;
|
|
39
45
|
return `${quota.adapterID || quota.providerID}:${quota.providerID}`;
|
|
40
46
|
}
|
|
41
47
|
function quotaScore(quota) {
|
|
42
48
|
let score = 0;
|
|
43
|
-
if (quota.status ===
|
|
49
|
+
if (quota.status === "ok")
|
|
44
50
|
score += 10;
|
|
45
51
|
if (quota.windows && quota.windows.length > 0) {
|
|
46
52
|
score += 5 + quota.windows.length;
|
|
@@ -53,13 +59,13 @@ function quotaScore(quota) {
|
|
|
53
59
|
}
|
|
54
60
|
export function collapseQuotaSnapshots(quotas) {
|
|
55
61
|
const grouped = new Map();
|
|
56
|
-
const hasRightCodeBase = quotas.some((quota) => quota.adapterID ===
|
|
62
|
+
const hasRightCodeBase = quotas.some((quota) => quota.adapterID === "rightcode" && quotaDisplayLabel(quota) === "RC");
|
|
57
63
|
for (const quota of quotas) {
|
|
58
64
|
// If both RC (balance) and RC-variant (subscription) exist,
|
|
59
65
|
// treat balance as owned by RC.
|
|
60
66
|
const normalizedQuota = hasRightCodeBase &&
|
|
61
|
-
quota.adapterID ===
|
|
62
|
-
quotaDisplayLabel(quota).startsWith(
|
|
67
|
+
quota.adapterID === "rightcode" &&
|
|
68
|
+
quotaDisplayLabel(quota).startsWith("RC-")
|
|
63
69
|
? { ...quota, balance: undefined }
|
|
64
70
|
: quota;
|
|
65
71
|
const key = quotaKey(normalizedQuota);
|
package/dist/storage_parse.js
CHANGED
package/dist/title.js
CHANGED
|
@@ -57,28 +57,28 @@ function isCoreDecoratedDetail(line) {
|
|
|
57
57
|
function isQuotaDecoratedDetail(line) {
|
|
58
58
|
if (!line)
|
|
59
59
|
return false;
|
|
60
|
-
if (/^(OAI|Cop|Ant|Kimi|XYAI|
|
|
60
|
+
if (/^(OAI|Cop|Ant|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:\?|unsupported|unavailable|error|(?:\d+h|D|W|M)\d{1,3}|D[\d.,]+\/[\d.,]+|B(?:[¥$-])?[\d.,]+))+$/i.test(line)) {
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|
|
|
63
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)\s*$/.test(line)) {
|
|
64
64
|
return true;
|
|
65
65
|
}
|
|
66
66
|
if (/^(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|Balance\s+\$[\d.,]+|Remaining\s+\?|(?:error|unsupported|unavailable))$/.test(line)) {
|
|
67
67
|
return true;
|
|
68
68
|
}
|
|
69
|
-
if (/^(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?$/.test(line)) {
|
|
69
|
+
if (/^(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?$/.test(line)) {
|
|
70
70
|
return true;
|
|
71
71
|
}
|
|
72
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|
|
|
72
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|(?:error|unsupported|unavailable)))$/.test(line)) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|
|
|
75
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?)$/.test(line)) {
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
|
-
if (/^(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3}|(?:R|E\+?)\d[\d:.-]*))(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
|
|
78
|
+
if (/^(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*))(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
|
|
79
79
|
return true;
|
|
80
80
|
}
|
|
81
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|
|
|
81
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
|
|
82
82
|
return true;
|
|
83
83
|
}
|
|
84
84
|
return false;
|
package/dist/title_apply.js
CHANGED
|
@@ -15,6 +15,16 @@ export function createTitleApplicator(deps) {
|
|
|
15
15
|
balance: quota.balance ? { ...quota.balance } : undefined,
|
|
16
16
|
windows: quota.windows?.map((win) => ({ ...win })),
|
|
17
17
|
}));
|
|
18
|
+
const sameProviderIDs = (left, right) => {
|
|
19
|
+
if (left.length !== right.length)
|
|
20
|
+
return false;
|
|
21
|
+
const rightSet = new Set(right);
|
|
22
|
+
for (const value of left) {
|
|
23
|
+
if (!rightSet.has(value))
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
18
28
|
const applyTitle = async (sessionID) => {
|
|
19
29
|
if (!deps.config.sidebar.enabled)
|
|
20
30
|
return false;
|
|
@@ -82,16 +92,23 @@ export function createTitleApplicator(deps) {
|
|
|
82
92
|
const usage = await deps.summarizeSessionUsageForDisplay(sessionID, deps.config.sidebar.includeChildren);
|
|
83
93
|
const view = deps.getTitleView?.(sessionID) ??
|
|
84
94
|
resolveTitleView({ config: deps.config });
|
|
95
|
+
const panelQuotaProviders = Array.from(new Set(Object.keys(usage.providers)));
|
|
85
96
|
const quotaProviders = Array.from(new Set(view === 'compact'
|
|
86
97
|
? selectDesktopCompactProviderIDs(usage, deps.config)
|
|
87
|
-
:
|
|
98
|
+
: panelQuotaProviders));
|
|
88
99
|
const quotas = deps.config.sidebar.showQuota && quotaProviders.length > 0
|
|
89
100
|
? await deps.getQuotaSnapshots(quotaProviders)
|
|
90
101
|
: [];
|
|
102
|
+
const panelQuotas = deps.config.sidebar.showQuota && panelQuotaProviders.length > 0
|
|
103
|
+
? sameProviderIDs(quotaProviders, panelQuotaProviders)
|
|
104
|
+
? quotas
|
|
105
|
+
: await deps.getQuotaSnapshots(panelQuotaProviders)
|
|
106
|
+
: [];
|
|
91
107
|
sessionState.sidebarPanel = {
|
|
92
108
|
version: 1,
|
|
93
109
|
updatedAt: Date.now(),
|
|
94
110
|
usage: toCachedSessionUsage(usage),
|
|
111
|
+
panelQuotas: cloneQuotas(collapseQuotaSnapshots(panelQuotas)),
|
|
95
112
|
quotas: cloneQuotas(collapseQuotaSnapshots(quotas)),
|
|
96
113
|
};
|
|
97
114
|
stateMutated = true;
|
package/dist/tui.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
quotaGroupsSummary,
|
|
14
14
|
quotaGroupsUseBullets,
|
|
15
15
|
renderSidebarQuotaGroups,
|
|
16
|
+
sidebarPanelQuotaSnapshots,
|
|
16
17
|
type SidebarQuotaGroup,
|
|
17
18
|
} from './tui_helpers.js'
|
|
18
19
|
import {
|
|
@@ -109,7 +110,7 @@ async function loadSidebarPanel(
|
|
|
109
110
|
? renderSidebarUsageLines(usage, panelConfig(config))
|
|
110
111
|
: []
|
|
111
112
|
const quotaGroups = renderSidebarQuotaGroups(
|
|
112
|
-
session?.sidebarPanel
|
|
113
|
+
sidebarPanelQuotaSnapshots(session?.sidebarPanel),
|
|
113
114
|
panelConfig(config),
|
|
114
115
|
)
|
|
115
116
|
|
package/dist/tui_helpers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QuotaSidebarConfig, QuotaSnapshot } from './types.js';
|
|
1
|
+
import type { QuotaSidebarConfig, QuotaSnapshot, SidebarPanelState } from './types.js';
|
|
2
2
|
export type SidebarQuotaTone = 'success' | 'warning' | 'error' | 'muted';
|
|
3
3
|
export type SidebarQuotaGroup = {
|
|
4
4
|
providerID: string;
|
|
@@ -9,6 +9,7 @@ export type SidebarQuotaGroup = {
|
|
|
9
9
|
continuationLines: string[];
|
|
10
10
|
};
|
|
11
11
|
export declare function renderSidebarQuotaGroups(quotas: QuotaSnapshot[], config: QuotaSidebarConfig): SidebarQuotaGroup[];
|
|
12
|
+
export declare function sidebarPanelQuotaSnapshots(panel?: SidebarPanelState): QuotaSnapshot[];
|
|
12
13
|
export declare function fallbackQuotaGroupsFromTitle(title: string, width: number): SidebarQuotaGroup[];
|
|
13
14
|
export declare function quotaGroupsUseBullets(groups: SidebarQuotaGroup[]): boolean;
|
|
14
15
|
export declare function quotaGroupsAreCollapsible(groups: SidebarQuotaGroup[]): boolean;
|
package/dist/tui_helpers.js
CHANGED
|
@@ -69,7 +69,9 @@ function fallbackQuotaTone(detail) {
|
|
|
69
69
|
return 'error';
|
|
70
70
|
if (/\bB-/.test(safe))
|
|
71
71
|
return 'error';
|
|
72
|
-
const percents = [
|
|
72
|
+
const percents = [
|
|
73
|
+
...safe.matchAll(/\b(?:\d+[hdw]|[DWM]|S7d|O7d|OA7d|Co7d)(\d{1,3})\b/gi),
|
|
74
|
+
]
|
|
73
75
|
.map((match) => Number(match[1]))
|
|
74
76
|
.filter((value) => Number.isFinite(value));
|
|
75
77
|
if (percents.length === 0)
|
|
@@ -104,6 +106,9 @@ export function renderSidebarQuotaGroups(quotas, config) {
|
|
|
104
106
|
};
|
|
105
107
|
});
|
|
106
108
|
}
|
|
109
|
+
export function sidebarPanelQuotaSnapshots(panel) {
|
|
110
|
+
return panel?.panelQuotas || panel?.quotas || [];
|
|
111
|
+
}
|
|
107
112
|
export function fallbackQuotaGroupsFromTitle(title, width) {
|
|
108
113
|
const parts = (title || '')
|
|
109
114
|
.split(' | ')
|
package/dist/types.d.ts
CHANGED
|
@@ -113,6 +113,8 @@ export type SidebarPanelState = {
|
|
|
113
113
|
version: 1;
|
|
114
114
|
updatedAt: number;
|
|
115
115
|
usage?: CachedSessionUsage;
|
|
116
|
+
/** Full TUI sidebar provider list for this session. */
|
|
117
|
+
panelQuotas?: QuotaSnapshot[];
|
|
116
118
|
quotas?: QuotaSnapshot[];
|
|
117
119
|
};
|
|
118
120
|
/** Tracks incremental aggregation cursor for a session (P1). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leo000001/opencode-quota-sidebar",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "OpenCode plugin that shows quota and token usage in TUI sidebar panels and compact session titles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"CONTRIBUTING.md",
|
|
31
31
|
"SECURITY.md",
|
|
32
32
|
"README.md",
|
|
33
|
+
"README.zh-CN.md",
|
|
34
|
+
"assets/OpenCode-Quota-Sidebar.png",
|
|
33
35
|
"quota-sidebar.config.example.json"
|
|
34
36
|
],
|
|
35
37
|
"scripts": {
|
|
@@ -43,10 +45,15 @@
|
|
|
43
45
|
},
|
|
44
46
|
"keywords": [
|
|
45
47
|
"opencode",
|
|
48
|
+
"opencode-plugin",
|
|
46
49
|
"plugin",
|
|
47
50
|
"quota",
|
|
48
51
|
"sidebar",
|
|
49
|
-
"token-usage"
|
|
52
|
+
"token-usage",
|
|
53
|
+
"tui",
|
|
54
|
+
"openai",
|
|
55
|
+
"copilot",
|
|
56
|
+
"anthropic"
|
|
50
57
|
],
|
|
51
58
|
"license": "MIT",
|
|
52
59
|
"repository": {
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"sidebar": {
|
|
3
|
-
"enabled": true,
|
|
4
|
-
"width": 36,
|
|
5
|
-
"titleMode": "auto",
|
|
6
|
-
"multilineTitle": true,
|
|
7
|
-
"showCost": true,
|
|
8
|
-
"showQuota": true,
|
|
9
|
-
"wrapQuotaLines": true,
|
|
10
|
-
"includeChildren": true,
|
|
11
|
-
"childrenMaxDepth": 6,
|
|
12
|
-
"childrenMaxSessions": 128,
|
|
13
|
-
"childrenConcurrency": 5,
|
|
14
|
-
"desktopCompact": {
|
|
15
|
-
"recentRequests": 50,
|
|
16
|
-
"recentMinutes": 60
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"quota": {
|
|
20
|
-
"refreshMs": 300000,
|
|
21
|
-
"includeOpenAI": true,
|
|
22
|
-
"includeCopilot": true,
|
|
23
|
-
"includeAnthropic": true,
|
|
24
|
-
"providers": {
|
|
25
|
-
"rightcode": {
|
|
26
|
-
"enabled": true
|
|
27
|
-
},
|
|
28
|
-
"xyai
|
|
29
|
-
"enabled": false,
|
|
30
|
-
"baseURL": "https://new.xychatai.com",
|
|
31
|
-
"serviceType": "codex",
|
|
32
|
-
"login": {
|
|
33
|
-
"username": "your-account@example.com",
|
|
34
|
-
"password": "your-password"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"refreshAccessToken": false,
|
|
39
|
-
"requestTimeoutMs": 8000
|
|
40
|
-
},
|
|
41
|
-
"toast": {
|
|
42
|
-
"durationMs": 12000
|
|
43
|
-
},
|
|
44
|
-
"retentionDays": 730
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"sidebar": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"width": 36,
|
|
5
|
+
"titleMode": "auto",
|
|
6
|
+
"multilineTitle": true,
|
|
7
|
+
"showCost": true,
|
|
8
|
+
"showQuota": true,
|
|
9
|
+
"wrapQuotaLines": true,
|
|
10
|
+
"includeChildren": true,
|
|
11
|
+
"childrenMaxDepth": 6,
|
|
12
|
+
"childrenMaxSessions": 128,
|
|
13
|
+
"childrenConcurrency": 5,
|
|
14
|
+
"desktopCompact": {
|
|
15
|
+
"recentRequests": 50,
|
|
16
|
+
"recentMinutes": 60
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"quota": {
|
|
20
|
+
"refreshMs": 300000,
|
|
21
|
+
"includeOpenAI": true,
|
|
22
|
+
"includeCopilot": true,
|
|
23
|
+
"includeAnthropic": true,
|
|
24
|
+
"providers": {
|
|
25
|
+
"rightcode": {
|
|
26
|
+
"enabled": true
|
|
27
|
+
},
|
|
28
|
+
"xyai": {
|
|
29
|
+
"enabled": false,
|
|
30
|
+
"baseURL": "https://new.xychatai.com",
|
|
31
|
+
"serviceType": "codex",
|
|
32
|
+
"login": {
|
|
33
|
+
"username": "your-account@example.com",
|
|
34
|
+
"password": "your-password"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"refreshAccessToken": false,
|
|
39
|
+
"requestTimeoutMs": 8000
|
|
40
|
+
},
|
|
41
|
+
"toast": {
|
|
42
|
+
"durationMs": 12000
|
|
43
|
+
},
|
|
44
|
+
"retentionDays": 730
|
|
45
|
+
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { isRecord, swallow } from '../../helpers.js';
|
|
2
|
-
import { asNumber, configuredProviderEnabled, fetchWithTimeout, sanitizeBaseURL, toIso, } from '../common.js';
|
|
3
|
-
function isBuzzBaseURL(value) {
|
|
4
|
-
const normalized = sanitizeBaseURL(value);
|
|
5
|
-
if (!normalized)
|
|
6
|
-
return false;
|
|
7
|
-
try {
|
|
8
|
-
const parsed = new URL(normalized);
|
|
9
|
-
if (parsed.protocol !== 'https:')
|
|
10
|
-
return false;
|
|
11
|
-
return parsed.host === 'buzzai.cc' || parsed.host === 'www.buzzai.cc';
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function resolveApiKey(auth, providerOptions) {
|
|
18
|
-
const optionKey = providerOptions?.apiKey;
|
|
19
|
-
if (typeof optionKey === 'string' && optionKey)
|
|
20
|
-
return optionKey;
|
|
21
|
-
if (!auth)
|
|
22
|
-
return undefined;
|
|
23
|
-
if (auth.type === 'api' && typeof auth.key === 'string' && auth.key) {
|
|
24
|
-
return auth.key;
|
|
25
|
-
}
|
|
26
|
-
if (auth.type === 'wellknown') {
|
|
27
|
-
if (typeof auth.key === 'string' && auth.key)
|
|
28
|
-
return auth.key;
|
|
29
|
-
if (typeof auth.token === 'string' && auth.token)
|
|
30
|
-
return auth.token;
|
|
31
|
-
}
|
|
32
|
-
if (auth.type === 'oauth' && typeof auth.access === 'string' && auth.access) {
|
|
33
|
-
return auth.access;
|
|
34
|
-
}
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
function dashboardUrl(baseURL, pathname) {
|
|
38
|
-
const normalized = sanitizeBaseURL(baseURL);
|
|
39
|
-
if (normalized) {
|
|
40
|
-
try {
|
|
41
|
-
return new URL(pathname, normalized).toString();
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Fall through to the stable default host below.
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return `https://buzzai.cc${pathname}`;
|
|
48
|
-
}
|
|
49
|
-
async function fetchBuzzQuota({ sourceProviderID, providerID, providerOptions, auth, config, }) {
|
|
50
|
-
const checkedAt = Date.now();
|
|
51
|
-
const runtimeProviderID = typeof sourceProviderID === 'string' && sourceProviderID
|
|
52
|
-
? sourceProviderID
|
|
53
|
-
: providerID;
|
|
54
|
-
const base = {
|
|
55
|
-
providerID: runtimeProviderID,
|
|
56
|
-
adapterID: 'buzz',
|
|
57
|
-
label: 'Buzz',
|
|
58
|
-
shortLabel: 'Buzz',
|
|
59
|
-
sortOrder: 6,
|
|
60
|
-
};
|
|
61
|
-
const apiKey = resolveApiKey(auth, providerOptions);
|
|
62
|
-
if (!apiKey) {
|
|
63
|
-
return {
|
|
64
|
-
...base,
|
|
65
|
-
status: 'unavailable',
|
|
66
|
-
checkedAt,
|
|
67
|
-
note: 'missing api key',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const subscriptionEndpoint = dashboardUrl(providerOptions?.baseURL, '/v1/dashboard/billing/subscription');
|
|
71
|
-
const usageEndpoint = dashboardUrl(providerOptions?.baseURL, '/v1/dashboard/billing/usage');
|
|
72
|
-
const requestInit = {
|
|
73
|
-
headers: {
|
|
74
|
-
Accept: 'application/json',
|
|
75
|
-
Authorization: `Bearer ${apiKey}`,
|
|
76
|
-
'User-Agent': 'opencode-quota-sidebar',
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
const [subscriptionResponse, usageResponse] = await Promise.all([
|
|
80
|
-
fetchWithTimeout(subscriptionEndpoint, requestInit, config.quota.requestTimeoutMs).catch(swallow('fetchBuzzQuota:subscription')),
|
|
81
|
-
fetchWithTimeout(usageEndpoint, requestInit, config.quota.requestTimeoutMs).catch(swallow('fetchBuzzQuota:usage')),
|
|
82
|
-
]);
|
|
83
|
-
if (!subscriptionResponse || !usageResponse) {
|
|
84
|
-
return {
|
|
85
|
-
...base,
|
|
86
|
-
status: 'error',
|
|
87
|
-
checkedAt,
|
|
88
|
-
note: 'network request failed',
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
if (!subscriptionResponse.ok || !usageResponse.ok) {
|
|
92
|
-
const note = [
|
|
93
|
-
!subscriptionResponse.ok
|
|
94
|
-
? `subscription http ${subscriptionResponse.status}`
|
|
95
|
-
: undefined,
|
|
96
|
-
!usageResponse.ok ? `usage http ${usageResponse.status}` : undefined,
|
|
97
|
-
]
|
|
98
|
-
.filter((value) => Boolean(value))
|
|
99
|
-
.join(', ');
|
|
100
|
-
return {
|
|
101
|
-
...base,
|
|
102
|
-
status: 'error',
|
|
103
|
-
checkedAt,
|
|
104
|
-
note,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
const [subscriptionPayload, usagePayload] = await Promise.all([
|
|
108
|
-
subscriptionResponse
|
|
109
|
-
.json()
|
|
110
|
-
.catch(swallow('fetchBuzzQuota:subscriptionJson')),
|
|
111
|
-
usageResponse.json().catch(swallow('fetchBuzzQuota:usageJson')),
|
|
112
|
-
]);
|
|
113
|
-
if (!isRecord(subscriptionPayload) || !isRecord(usagePayload)) {
|
|
114
|
-
return {
|
|
115
|
-
...base,
|
|
116
|
-
status: 'error',
|
|
117
|
-
checkedAt,
|
|
118
|
-
note: 'invalid response',
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
const totalQuota = asNumber(subscriptionPayload.soft_limit_usd);
|
|
122
|
-
const totalUsage = asNumber(usagePayload.total_usage);
|
|
123
|
-
if (totalQuota === undefined || totalUsage === undefined) {
|
|
124
|
-
return {
|
|
125
|
-
...base,
|
|
126
|
-
status: 'error',
|
|
127
|
-
checkedAt,
|
|
128
|
-
note: 'missing billing fields',
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
const accessUntil = asNumber(subscriptionPayload.access_until);
|
|
132
|
-
const resetAt = accessUntil !== undefined && accessUntil > 0
|
|
133
|
-
? toIso(accessUntil)
|
|
134
|
-
: undefined;
|
|
135
|
-
const balance = totalQuota - totalUsage / 100;
|
|
136
|
-
return {
|
|
137
|
-
...base,
|
|
138
|
-
status: 'ok',
|
|
139
|
-
checkedAt,
|
|
140
|
-
resetAt,
|
|
141
|
-
balance: {
|
|
142
|
-
amount: balance,
|
|
143
|
-
currency: '¥',
|
|
144
|
-
},
|
|
145
|
-
note: 'remaining balance = soft_limit_usd - total_usage / 100',
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
export const buzzAdapter = {
|
|
149
|
-
id: 'buzz',
|
|
150
|
-
label: 'Buzz',
|
|
151
|
-
shortLabel: 'Buzz',
|
|
152
|
-
sortOrder: 6,
|
|
153
|
-
matchScore: ({ providerOptions }) => isBuzzBaseURL(providerOptions?.baseURL) ? 100 : 0,
|
|
154
|
-
isEnabled: (config) => configuredProviderEnabled(config.quota, 'buzz', true),
|
|
155
|
-
fetch: fetchBuzzQuota,
|
|
156
|
-
};
|