@toninho09/opencode-usage 1.0.0
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/AGENTS.md +226 -0
- package/README.md +133 -0
- package/index.ts +31 -0
- package/lib/providers/base.ts +46 -0
- package/lib/providers/claude/client.ts +72 -0
- package/lib/providers/claude/formatter.ts +56 -0
- package/lib/providers/claude/index.ts +36 -0
- package/lib/providers/claude/types.ts +22 -0
- package/lib/providers/copilot/client.ts +185 -0
- package/lib/providers/copilot/formatter.ts +65 -0
- package/lib/providers/copilot/index.ts +36 -0
- package/lib/providers/copilot/types.ts +38 -0
- package/lib/providers/index.ts +8 -0
- package/lib/providers/registry.ts +56 -0
- package/lib/providers/zai/client.ts +97 -0
- package/lib/providers/zai/formatter.ts +77 -0
- package/lib/providers/zai/index.ts +41 -0
- package/lib/providers/zai/types.ts +23 -0
- package/lib/shared/auth.ts +49 -0
- package/lib/shared/formatting.ts +71 -0
- package/lib/shared/notification.ts +28 -0
- package/lib/shared/utils.ts +43 -0
- package/lib/usage-handler.ts +55 -0
- package/opencode.json +4 -0
- package/package.json +14 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { fetchWithTimeout } from "../../shared/utils";
|
|
2
|
+
import { readAuthConfig, type AuthProvider } from "../../shared/auth";
|
|
3
|
+
import type { CopilotUsageResponse, CopilotTokenResponse } from "./types";
|
|
4
|
+
|
|
5
|
+
const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
6
|
+
|
|
7
|
+
const COPILOT_VERSION = "0.35.0";
|
|
8
|
+
const EDITOR_VERSION = "vscode/1.107.0";
|
|
9
|
+
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
10
|
+
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
11
|
+
|
|
12
|
+
const COPILOT_HEADERS = {
|
|
13
|
+
"User-Agent": USER_AGENT,
|
|
14
|
+
"Editor-Version": EDITOR_VERSION,
|
|
15
|
+
"Editor-Plugin-Version": EDITOR_PLUGIN_VERSION,
|
|
16
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class CopilotClient {
|
|
20
|
+
private readonly apiBaseUrl = GITHUB_API_BASE_URL;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds headers for authentication with Bearer token
|
|
24
|
+
*/
|
|
25
|
+
private buildGitHubHeaders(token: string): Record<string, string> {
|
|
26
|
+
return {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
Accept: "application/json",
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
...COPILOT_HEADERS,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds headers for legacy authentication (token prefix)
|
|
36
|
+
*/
|
|
37
|
+
private buildLegacyHeaders(token: string): Record<string, string> {
|
|
38
|
+
return {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
Accept: "application/json",
|
|
41
|
+
Authorization: `token ${token}`,
|
|
42
|
+
...COPILOT_HEADERS,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Exchanges OAuth token for specific Copilot token
|
|
48
|
+
*/
|
|
49
|
+
private async exchangeForCopilotToken(oauthToken: string): Promise<string | null> {
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetchWithTimeout(
|
|
52
|
+
`${this.apiBaseUrl}/copilot_internal/v2/token`,
|
|
53
|
+
{
|
|
54
|
+
headers: {
|
|
55
|
+
Accept: "application/json",
|
|
56
|
+
Authorization: `Bearer ${oauthToken}`,
|
|
57
|
+
...COPILOT_HEADERS,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tokenData: CopilotTokenResponse = await response.json();
|
|
67
|
+
return tokenData.token;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Strategy 1: Try with cached token if still valid
|
|
75
|
+
*/
|
|
76
|
+
private async fetchWithCachedToken(
|
|
77
|
+
cachedAccessToken: string,
|
|
78
|
+
tokenExpiry: number,
|
|
79
|
+
oauthToken: string,
|
|
80
|
+
): Promise<CopilotUsageResponse | null> {
|
|
81
|
+
if (!cachedAccessToken || cachedAccessToken === oauthToken || tokenExpiry <= Date.now()) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const response = await fetchWithTimeout(
|
|
86
|
+
`${this.apiBaseUrl}/copilot_internal/user`,
|
|
87
|
+
{ headers: this.buildGitHubHeaders(cachedAccessToken) },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (response.ok) {
|
|
91
|
+
return response.json() as Promise<CopilotUsageResponse>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Strategy 2: Try with direct OAuth token (legacy format)
|
|
99
|
+
*/
|
|
100
|
+
private async fetchWithLegacyToken(oauthToken: string): Promise<CopilotUsageResponse | null> {
|
|
101
|
+
const response = await fetchWithTimeout(
|
|
102
|
+
`${this.apiBaseUrl}/copilot_internal/user`,
|
|
103
|
+
{ headers: this.buildLegacyHeaders(oauthToken) },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (response.ok) {
|
|
107
|
+
return response.json() as Promise<CopilotUsageResponse>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Strategy 3: Exchange OAuth token for Copilot token and fetch
|
|
115
|
+
*/
|
|
116
|
+
private async fetchWithExchangedToken(oauthToken: string): Promise<CopilotUsageResponse> {
|
|
117
|
+
const copilotToken = await this.exchangeForCopilotToken(oauthToken);
|
|
118
|
+
|
|
119
|
+
if (!copilotToken) {
|
|
120
|
+
throw new Error("Failed to exchange OAuth token for Copilot token");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const response = await fetchWithTimeout(
|
|
124
|
+
`${this.apiBaseUrl}/copilot_internal/user`,
|
|
125
|
+
{ headers: this.buildGitHubHeaders(copilotToken) },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const errorText = await response.text();
|
|
130
|
+
throw new Error(`GitHub API Error ${response.status}: ${errorText}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return response.json() as Promise<CopilotUsageResponse>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Tries to fetch usage data using multiple authentication strategies
|
|
138
|
+
*/
|
|
139
|
+
private async fetchWithAuthStrategies(authData: AuthProvider): Promise<CopilotUsageResponse> {
|
|
140
|
+
const oauthToken = authData.refresh || authData.access;
|
|
141
|
+
if (!oauthToken) {
|
|
142
|
+
throw new Error("No OAuth token found in auth data");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const cachedAccessToken = authData.access || "";
|
|
146
|
+
const tokenExpiry = authData.expires || 0;
|
|
147
|
+
|
|
148
|
+
// Strategy 1: Try with cached token
|
|
149
|
+
const cachedResult = await this.fetchWithCachedToken(cachedAccessToken, tokenExpiry, oauthToken);
|
|
150
|
+
if (cachedResult) {
|
|
151
|
+
return cachedResult;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Strategy 2: Try with direct OAuth token
|
|
155
|
+
const legacyResult = await this.fetchWithLegacyToken(oauthToken);
|
|
156
|
+
if (legacyResult) {
|
|
157
|
+
return legacyResult;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Strategy 3: Exchange token and try again
|
|
161
|
+
return this.fetchWithExchangedToken(oauthToken);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Fetches Copilot usage data
|
|
166
|
+
*/
|
|
167
|
+
async fetchUsage(): Promise<CopilotUsageResponse | null> {
|
|
168
|
+
const auth = readAuthConfig();
|
|
169
|
+
const copilotAuth = auth?.["github-copilot"];
|
|
170
|
+
|
|
171
|
+
if (!copilotAuth?.refresh) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return this.fetchWithAuthStrategies(copilotAuth);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Checks if Copilot is configured
|
|
180
|
+
*/
|
|
181
|
+
isConfigured(): boolean {
|
|
182
|
+
const auth = readAuthConfig();
|
|
183
|
+
return !!auth?.["github-copilot"]?.refresh;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { formatResetLine } from "../../shared/formatting";
|
|
2
|
+
import { createUsedProgressBar } from "../../shared/utils";
|
|
3
|
+
import type { CopilotUsageResponse, QuotaDetail } from "./types";
|
|
4
|
+
|
|
5
|
+
export class CopilotFormatter {
|
|
6
|
+
/**
|
|
7
|
+
* Formats a Copilot quota line with progress bar
|
|
8
|
+
*/
|
|
9
|
+
private formatQuotaLine(name: string, quota: QuotaDetail | undefined): string {
|
|
10
|
+
if (!quota) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (quota.unlimited) {
|
|
15
|
+
return `${name.padEnd(16)} Unlimited`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const total = quota.entitlement;
|
|
19
|
+
const used = total - quota.remaining;
|
|
20
|
+
const percentUsed = Math.round((used / total) * 100);
|
|
21
|
+
const progressBar = createUsedProgressBar(percentUsed, 20);
|
|
22
|
+
|
|
23
|
+
return `${name.padEnd(16)} ${progressBar} ${percentUsed}% (${used}/${total})`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Formats Copilot usage data for display
|
|
28
|
+
*/
|
|
29
|
+
format(data: CopilotUsageResponse): string {
|
|
30
|
+
const lines: string[] = [];
|
|
31
|
+
|
|
32
|
+
lines.push("╔════════════════════════════════════════╗");
|
|
33
|
+
lines.push("║ GITHUB COPILOT ║");
|
|
34
|
+
lines.push("╚════════════════════════════════════════╝");
|
|
35
|
+
lines.push(`Plan: ${data.copilot_plan}`);
|
|
36
|
+
|
|
37
|
+
const premium = data.quota_snapshots.premium_interactions;
|
|
38
|
+
if (premium) {
|
|
39
|
+
const premiumLine = this.formatQuotaLine("Premium:", premium);
|
|
40
|
+
if (premiumLine) {
|
|
41
|
+
lines.push(premiumLine);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const chat = data.quota_snapshots.chat;
|
|
46
|
+
if (chat) {
|
|
47
|
+
const chatLine = this.formatQuotaLine("Chat:", chat);
|
|
48
|
+
if (chatLine) {
|
|
49
|
+
lines.push(chatLine);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const completions = data.quota_snapshots.completions;
|
|
54
|
+
if (completions) {
|
|
55
|
+
const completionsLine = this.formatQuotaLine("Completions:", completions);
|
|
56
|
+
if (completionsLine) {
|
|
57
|
+
lines.push(completionsLine);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
lines.push(formatResetLine("Quota Resets:", data.quota_reset_date, 16));
|
|
62
|
+
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { UsageProvider, ProviderMessage } from "../base";
|
|
2
|
+
import { CopilotClient } from "./client";
|
|
3
|
+
import { CopilotFormatter } from "./formatter";
|
|
4
|
+
|
|
5
|
+
const client = new CopilotClient();
|
|
6
|
+
const formatter = new CopilotFormatter();
|
|
7
|
+
|
|
8
|
+
export const copilotProvider: UsageProvider = {
|
|
9
|
+
name: "GitHub Copilot",
|
|
10
|
+
id: "copilot",
|
|
11
|
+
description: "Monitoramento de uso do GitHub Copilot",
|
|
12
|
+
|
|
13
|
+
async getUsageData(): Promise<ProviderMessage | null> {
|
|
14
|
+
try {
|
|
15
|
+
const data = await client.fetchUsage();
|
|
16
|
+
if (!data) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
content: formatter.format(data),
|
|
22
|
+
};
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
content: "",
|
|
26
|
+
error: error instanceof Error ? error.message : String(error),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
isConfigured(): boolean {
|
|
32
|
+
return client.isConfigured();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default copilotProvider;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface QuotaDetail {
|
|
2
|
+
entitlement: number;
|
|
3
|
+
overage_count: number;
|
|
4
|
+
overage_permitted: boolean;
|
|
5
|
+
percent_remaining: number;
|
|
6
|
+
quota_id: string;
|
|
7
|
+
quota_remaining: number;
|
|
8
|
+
remaining: number;
|
|
9
|
+
unlimited: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface QuotaSnapshots {
|
|
13
|
+
chat?: QuotaDetail;
|
|
14
|
+
completions?: QuotaDetail;
|
|
15
|
+
premium_interactions: QuotaDetail;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CopilotUsageResponse {
|
|
19
|
+
access_type_sku: string;
|
|
20
|
+
analytics_tracking_id: string;
|
|
21
|
+
assigned_date: string;
|
|
22
|
+
can_signup_for_limited: boolean;
|
|
23
|
+
chat_enabled: boolean;
|
|
24
|
+
copilot_plan: string;
|
|
25
|
+
organization_login_list: unknown[];
|
|
26
|
+
organization_list: unknown[];
|
|
27
|
+
quota_reset_date: string;
|
|
28
|
+
quota_snapshots: QuotaSnapshots;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CopilotTokenResponse {
|
|
32
|
+
token: string;
|
|
33
|
+
expires_at: number;
|
|
34
|
+
refresh_in: number;
|
|
35
|
+
endpoints: {
|
|
36
|
+
api: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exporta todos os providers e componentes principais
|
|
3
|
+
*/
|
|
4
|
+
export { copilotProvider } from "./copilot";
|
|
5
|
+
export { claudeProvider } from "./claude";
|
|
6
|
+
export { zaiProvider } from "./zai";
|
|
7
|
+
export { registry, ProviderRegistry } from "./registry";
|
|
8
|
+
export type { UsageProvider, ProviderMessage } from "./base";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { UsageProvider } from "./base";
|
|
2
|
+
import { copilotProvider } from "./copilot";
|
|
3
|
+
import { claudeProvider } from "./claude";
|
|
4
|
+
import { zaiProvider } from "./zai";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Centralized registry of all usage providers
|
|
8
|
+
*/
|
|
9
|
+
export class ProviderRegistry {
|
|
10
|
+
private providers: Map<string, UsageProvider> = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Registers a new provider
|
|
14
|
+
*/
|
|
15
|
+
register(provider: UsageProvider): void {
|
|
16
|
+
this.providers.set(provider.id, provider);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns all registered providers
|
|
21
|
+
*/
|
|
22
|
+
getAll(): UsageProvider[] {
|
|
23
|
+
return Array.from(this.providers.values());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns a specific provider by ID
|
|
28
|
+
*/
|
|
29
|
+
getById(id: string): UsageProvider | undefined {
|
|
30
|
+
return this.providers.get(id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns all configured providers
|
|
35
|
+
*/
|
|
36
|
+
getConfigured(): UsageProvider[] {
|
|
37
|
+
return this.getAll().filter((provider) => provider.isConfigured());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if any provider is configured
|
|
42
|
+
*/
|
|
43
|
+
hasConfigured(): boolean {
|
|
44
|
+
return this.getConfigured().length > 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Creates singleton instance of registry
|
|
49
|
+
export const registry = new ProviderRegistry();
|
|
50
|
+
|
|
51
|
+
// Auto-registers all providers
|
|
52
|
+
registry.register(copilotProvider);
|
|
53
|
+
registry.register(claudeProvider);
|
|
54
|
+
registry.register(zaiProvider);
|
|
55
|
+
|
|
56
|
+
export default registry;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { fetchWithTimeout } from "../../shared/utils";
|
|
2
|
+
import { readAuthConfig, type AuthProvider } from "../../shared/auth";
|
|
3
|
+
import type { ZaiUsageResponse } from "./types";
|
|
4
|
+
|
|
5
|
+
const ZAI_API_BASE_URL = "https://api.z.ai";
|
|
6
|
+
|
|
7
|
+
export class ZaiClient {
|
|
8
|
+
private readonly apiBaseUrl = ZAI_API_BASE_URL;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds headers for authentication with Z.ai API
|
|
12
|
+
*/
|
|
13
|
+
private buildZaiHeaders(apiKey: string): Record<string, string> {
|
|
14
|
+
return {
|
|
15
|
+
"Authorization": apiKey,
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
"User-Agent": "OpenCode-Status-Plugin/1.0",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetches usage data from Z.ai API
|
|
23
|
+
*/
|
|
24
|
+
private async fetchZaiUsage(authData: AuthProvider): Promise<ZaiUsageResponse> {
|
|
25
|
+
const apiKey = authData.key;
|
|
26
|
+
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
throw new Error("No API key found in Z.ai auth data");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const url = `${this.apiBaseUrl}/api/monitor/usage/quota/limit`;
|
|
32
|
+
const response = await fetchWithTimeout(url, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
headers: this.buildZaiHeaders(apiKey),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const errorText = await response.text();
|
|
39
|
+
throw new Error(`Z.ai API Error ${response.status}: ${errorText}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data: ZaiUsageResponse = await response.json();
|
|
43
|
+
|
|
44
|
+
if (!data.success || data.code !== 200) {
|
|
45
|
+
throw new Error(`Z.ai API Error ${data.code}: ${data.msg || "Unknown error"}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fetches Z.ai usage data
|
|
53
|
+
*/
|
|
54
|
+
async fetchUsage(): Promise<ZaiUsageResponse | null> {
|
|
55
|
+
const authConfig = readAuthConfig();
|
|
56
|
+
|
|
57
|
+
if (!authConfig) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const authData = authConfig["zai-coding-plan"];
|
|
62
|
+
|
|
63
|
+
if (!authData || authData.type !== "api" || !authData.key) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return this.fetchZaiUsage(authData);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the Z.ai API key (for use in formatter)
|
|
72
|
+
*/
|
|
73
|
+
getApiKey(): string | null {
|
|
74
|
+
const authConfig = readAuthConfig();
|
|
75
|
+
|
|
76
|
+
if (!authConfig) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const authData = authConfig["zai-coding-plan"];
|
|
81
|
+
|
|
82
|
+
if (!authData || authData.type !== "api" || !authData.key) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return authData.key;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Checks if Z.ai is configured
|
|
91
|
+
*/
|
|
92
|
+
isConfigured(): boolean {
|
|
93
|
+
const authConfig = readAuthConfig();
|
|
94
|
+
const authData = authConfig?.["zai-coding-plan"];
|
|
95
|
+
return !!(authData && authData.type === "api" && authData.key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { formatResetLine, formatTokens, maskApiKey } from "../../shared/formatting";
|
|
2
|
+
import { createUsedProgressBar } from "../../shared/utils";
|
|
3
|
+
import type { ZaiUsageResponse, UsageLimitItem } from "./types";
|
|
4
|
+
|
|
5
|
+
export class ZaiFormatter {
|
|
6
|
+
/**
|
|
7
|
+
* Calculates usage display based on available fields from API
|
|
8
|
+
*/
|
|
9
|
+
private calculateUsageDisplay(limit: UsageLimitItem): string {
|
|
10
|
+
const { usage, currentValue, remaining } = limit;
|
|
11
|
+
|
|
12
|
+
// Case 1: API returns usage and currentValue (when already used)
|
|
13
|
+
if (currentValue !== undefined && usage !== undefined) {
|
|
14
|
+
return ` (${formatTokens(currentValue)}/${formatTokens(usage)})`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Case 2: API returns remaining and usage (calculate currentValue)
|
|
18
|
+
if (remaining !== undefined && usage !== undefined) {
|
|
19
|
+
const used = usage - remaining;
|
|
20
|
+
return ` (${formatTokens(used)}/${formatTokens(usage)})`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Case 3: Only remaining available
|
|
24
|
+
if (remaining !== undefined) {
|
|
25
|
+
return ` (0/${formatTokens(remaining)})`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Formats Z.ai usage data for display
|
|
33
|
+
*/
|
|
34
|
+
format(data: ZaiUsageResponse, apiKey: string): string {
|
|
35
|
+
const lines: string[] = [];
|
|
36
|
+
const limits = data.data.limits;
|
|
37
|
+
|
|
38
|
+
const maskedKey = maskApiKey(apiKey);
|
|
39
|
+
lines.push("╔════════════════════════════════════════╗");
|
|
40
|
+
lines.push("║ Z.AI CODING PLAN ║");
|
|
41
|
+
lines.push("╚════════════════════════════════════════╝");
|
|
42
|
+
lines.push(`Account: ${maskedKey} (Z.AI Coding Plan)`);
|
|
43
|
+
|
|
44
|
+
if (!limits || limits.length === 0) {
|
|
45
|
+
lines.push("No quota data available");
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tokensLimit = limits.find((l) => l.type === "TOKENS_LIMIT");
|
|
50
|
+
if (tokensLimit) {
|
|
51
|
+
const percentUsed = tokensLimit.percentage;
|
|
52
|
+
const progressBar = createUsedProgressBar(percentUsed, 20);
|
|
53
|
+
const usageDisplay = this.calculateUsageDisplay(tokensLimit);
|
|
54
|
+
|
|
55
|
+
lines.push(`Tokens: ${progressBar} ${percentUsed}%${usageDisplay}`);
|
|
56
|
+
|
|
57
|
+
if (tokensLimit.nextResetTime) {
|
|
58
|
+
lines.push(formatResetLine("5h Resets:", tokensLimit.nextResetTime, 16));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const timeLimit = limits.find((l) => l.type === "TIME_LIMIT");
|
|
63
|
+
if (timeLimit) {
|
|
64
|
+
const percentUsed = timeLimit.percentage;
|
|
65
|
+
const progressBar = createUsedProgressBar(percentUsed, 20);
|
|
66
|
+
|
|
67
|
+
const currentValue = timeLimit.currentValue ?? 0;
|
|
68
|
+
const total = timeLimit.usage ?? (timeLimit.remaining ?? 0);
|
|
69
|
+
|
|
70
|
+
lines.push(
|
|
71
|
+
`MCP Searches: ${progressBar} ${percentUsed}% (${currentValue}/${total})`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UsageProvider, ProviderMessage } from "../base";
|
|
2
|
+
import { ZaiClient } from "./client";
|
|
3
|
+
import { ZaiFormatter } from "./formatter";
|
|
4
|
+
|
|
5
|
+
const client = new ZaiClient();
|
|
6
|
+
const formatter = new ZaiFormatter();
|
|
7
|
+
|
|
8
|
+
export const zaiProvider: UsageProvider = {
|
|
9
|
+
name: "Z.ai Coding Plan",
|
|
10
|
+
id: "zai",
|
|
11
|
+
description: "Monitoramento de uso do Z.ai Coding Plan",
|
|
12
|
+
|
|
13
|
+
async getUsageData(): Promise<ProviderMessage | null> {
|
|
14
|
+
try {
|
|
15
|
+
const data = await client.fetchUsage();
|
|
16
|
+
if (!data) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const apiKey = client.getApiKey();
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
content: formatter.format(data, apiKey),
|
|
27
|
+
};
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return {
|
|
30
|
+
content: "",
|
|
31
|
+
error: error instanceof Error ? error.message : String(error),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
isConfigured(): boolean {
|
|
37
|
+
return client.isConfigured();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default zaiProvider;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface UsageLimitItem {
|
|
2
|
+
type: "TIME_LIMIT" | "TOKENS_LIMIT";
|
|
3
|
+
usage?: number;
|
|
4
|
+
currentValue?: number;
|
|
5
|
+
remaining?: number;
|
|
6
|
+
percentage: number;
|
|
7
|
+
nextResetTime?: number;
|
|
8
|
+
unit?: number;
|
|
9
|
+
number?: number;
|
|
10
|
+
usageDetails?: Array<{
|
|
11
|
+
modelCode: string;
|
|
12
|
+
usage: number;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ZaiUsageResponse {
|
|
17
|
+
code: number;
|
|
18
|
+
msg: string;
|
|
19
|
+
data: {
|
|
20
|
+
limits: UsageLimitItem[];
|
|
21
|
+
};
|
|
22
|
+
success: boolean;
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
|
|
5
|
+
const AUTH_CONFIG_PATH = path.join(
|
|
6
|
+
os.homedir(),
|
|
7
|
+
".local",
|
|
8
|
+
"share",
|
|
9
|
+
"opencode",
|
|
10
|
+
"auth.json",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export interface AuthProvider {
|
|
14
|
+
type?: string;
|
|
15
|
+
access?: string;
|
|
16
|
+
refresh?: string;
|
|
17
|
+
expires?: number;
|
|
18
|
+
username?: string;
|
|
19
|
+
key?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AuthConfig {
|
|
23
|
+
"github-copilot"?: AuthProvider;
|
|
24
|
+
"anthropic"?: AuthProvider;
|
|
25
|
+
"zai-coding-plan"?: AuthProvider;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reads the OpenCode authentication configuration file
|
|
30
|
+
* Returns null if file doesn't exist or error reading
|
|
31
|
+
*/
|
|
32
|
+
export function readAuthConfig(): AuthConfig | null {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(AUTH_CONFIG_PATH)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const content = fs.readFileSync(AUTH_CONFIG_PATH, "utf-8");
|
|
38
|
+
return JSON.parse(content) as AuthConfig;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the authentication configuration file path
|
|
46
|
+
*/
|
|
47
|
+
export function getAuthConfigPath(): string {
|
|
48
|
+
return AUTH_CONFIG_PATH;
|
|
49
|
+
}
|