@neuralnomads/codenomad-dev 0.18.0-dev-20260715-07e8ce68 → 0.18.0-dev-20260716-40cee071
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/README.md +12 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/dist/events/bus.js +13 -0
- package/dist/events/bus.test.js +37 -0
- package/dist/index.js +33 -56
- package/dist/index.test.js +32 -0
- package/dist/server/__tests__/remote-proxy.test.js +102 -6
- package/dist/server/http-server.js +2 -0
- package/dist/server/remote-proxy.js +96 -9
- package/dist/server/routes/usage.js +18 -0
- package/dist/server/routes/usage.test.js +26 -0
- package/dist/server/routes/workspaces.js +32 -1
- package/dist/server/routes/workspaces.test.js +75 -0
- package/dist/settings/binaries.js +4 -1
- package/dist/settings/binaries.test.js +24 -0
- package/dist/shutdown.js +75 -0
- package/dist/shutdown.test.js +111 -0
- package/dist/usage/providers/api-key.js +171 -0
- package/dist/usage/providers/google.js +171 -0
- package/dist/usage/providers/minimax.js +109 -0
- package/dist/usage/providers/oauth.js +111 -0
- package/dist/usage/providers/special.js +148 -0
- package/dist/usage/service.js +83 -0
- package/dist/usage/service.test.js +59 -0
- package/dist/usage/shared.js +130 -0
- package/dist/usage/shared.test.js +33 -0
- package/dist/usage/types.js +1 -0
- package/dist/workspaces/__tests__/spawn.test.js +64 -7
- package/dist/workspaces/__tests__/workspace-identity.test.js +177 -25
- package/dist/workspaces/launch-cleanup.js +132 -0
- package/dist/workspaces/launch-cleanup.test.js +33 -0
- package/dist/workspaces/manager.js +393 -135
- package/dist/workspaces/manager.test.js +318 -0
- package/dist/workspaces/process-identity.js +408 -0
- package/dist/workspaces/process-identity.test.js +165 -0
- package/dist/workspaces/runtime.js +567 -122
- package/dist/workspaces/runtime.test.js +603 -0
- package/dist/workspaces/spawn.js +86 -15
- package/package.json +1 -1
- package/public/assets/{FilesTab-EPvldvn1.js → FilesTab-BjztpSdj.js} +2 -2
- package/public/assets/{GitChangesTab-ofjovmv2.js → GitChangesTab-B9XraSoR.js} +2 -2
- package/public/assets/{SplitFilePanel-CyoPkcLd.js → SplitFilePanel-Cu_5-Z-E.js} +1 -1
- package/public/assets/StatusTab-D_IE79r-.js +1 -0
- package/public/assets/{align-justify-Bcf1-ZOl.js → align-justify-BKS9gyRk.js} +1 -1
- package/public/assets/{bundle-full-D5dzvToA.js → bundle-full-CXGXXlzR.js} +1 -1
- package/public/assets/{diff-viewer-DViXCI-4.js → diff-viewer-CSNOYoEB.js} +1 -1
- package/public/assets/{git-diff-vendor-CSgooKT_.js → git-diff-vendor-CYS3ApzA.js} +1 -1
- package/public/assets/index-B3YjNCh4.js +1 -0
- package/public/assets/index-B3ZD2_SU.js +1 -0
- package/public/assets/index-Bh2ofaQo.js +1 -0
- package/public/assets/index-CM0c9xRZ.js +1 -0
- package/public/assets/index-CdDxouJl.js +2 -0
- package/public/assets/index-D06Y2cFq.js +1 -0
- package/public/assets/{index-lf4nUOU1.js → index-DVc4ls8c.js} +1 -1
- package/public/assets/index-PmE51vPh.js +1 -0
- package/public/assets/{index-BMvFMsuy.css → index-Qj63IiSj.css} +1 -1
- package/public/assets/index-VY_zwmsG.js +1 -0
- package/public/assets/index-qiiH7egV.js +1 -0
- package/public/assets/{index-UbL8tz_V.js → index-sLe9hEfQ.js} +1 -1
- package/public/assets/{loading-CZ_tt3h0.js → loading-CDmt9OJU.js} +1 -1
- package/public/assets/main-CTN6Wwz1.js +68 -0
- package/public/assets/{markdown-G11aN6hz.js → markdown-1ZLSxCFT.js} +55 -44
- package/public/assets/monaco-viewer-D6JgH76W.js +26 -0
- package/public/assets/{tool-call-BsSXWErm.js → tool-call-CyXb63LH.js} +32 -32
- package/public/assets/{unified-picker-BRNI3UvE.js → unified-picker-DCCdSVwI.js} +1 -1
- package/public/assets/{wrap-text-B7up-uv7.js → wrap-text-pNMdTIq3.js} +1 -1
- package/public/index.html +5 -5
- package/public/loading.html +5 -5
- package/public/sw.js +1 -1
- package/public/assets/StatusTab-Bgu1Fh24.js +0 -1
- package/public/assets/index-3KsjbWGP.js +0 -1
- package/public/assets/index-B0WK8-cF.js +0 -1
- package/public/assets/index-BDv_iVtj.js +0 -1
- package/public/assets/index-BjxZvyb9.js +0 -2
- package/public/assets/index-BleKf9Nj.js +0 -1
- package/public/assets/index-CiIQ43NW.js +0 -1
- package/public/assets/index-DBquqL6Q.js +0 -1
- package/public/assets/index-yf9zolv4.js +0 -1
- package/public/assets/index-ypaL7i_s.js +0 -1
- package/public/assets/main-CAkg_fp_.js +0 -68
- package/public/assets/monaco-viewer-CZefBfct.js +0 -26
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { asObject, fetchJson, getAuthEntry, getString, notConfigured, result, toNumber, toTimestamp, toUsageWindow } from "../shared";
|
|
5
|
+
const GOOGLE_ENDPOINTS = [
|
|
6
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
7
|
+
"https://autopush-cloudcode-pa.sandbox.googleapis.com",
|
|
8
|
+
"https://cloudcode-pa.googleapis.com",
|
|
9
|
+
];
|
|
10
|
+
const DEFAULT_PROJECT_ID = "rising-fact-p41fc";
|
|
11
|
+
function parseRefresh(value) {
|
|
12
|
+
const raw = getString(value);
|
|
13
|
+
if (!raw)
|
|
14
|
+
return {};
|
|
15
|
+
const [token, projectId, managedProjectId] = raw.split("|");
|
|
16
|
+
return { token: getString(token) ?? undefined, projectId: getString(projectId) ?? getString(managedProjectId) ?? undefined };
|
|
17
|
+
}
|
|
18
|
+
function readJson(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function googleSources() {
|
|
27
|
+
const sources = [];
|
|
28
|
+
const entry = asObject(getAuthEntry(["google", "google.oauth"]));
|
|
29
|
+
const oauth = asObject(entry?.oauth) ?? entry;
|
|
30
|
+
if (oauth) {
|
|
31
|
+
const refresh = parseRefresh(oauth.refresh);
|
|
32
|
+
const accessToken = getString(oauth.access) ?? getString(oauth.token) ?? undefined;
|
|
33
|
+
if (accessToken || refresh.token) {
|
|
34
|
+
sources.push({
|
|
35
|
+
id: "gemini",
|
|
36
|
+
accessToken,
|
|
37
|
+
refreshToken: refresh.token,
|
|
38
|
+
projectId: refresh.projectId,
|
|
39
|
+
expires: toTimestamp(oauth.expires),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const home = os.homedir();
|
|
44
|
+
for (const candidate of [
|
|
45
|
+
path.join(home, ".config", "opencode", "antigravity-accounts.json"),
|
|
46
|
+
path.join(home, ".local", "share", "opencode", "antigravity-accounts.json"),
|
|
47
|
+
]) {
|
|
48
|
+
const data = readJson(candidate);
|
|
49
|
+
const accounts = Array.isArray(data?.accounts) ? data.accounts : [];
|
|
50
|
+
const account = accounts[data?.activeIndex ?? 0] ?? accounts[0];
|
|
51
|
+
const refresh = parseRefresh(account?.refreshToken);
|
|
52
|
+
const accessToken = getString(account?.accessToken) ?? getString(account?.access_token) ?? undefined;
|
|
53
|
+
if (accessToken || refresh.token) {
|
|
54
|
+
sources.push({
|
|
55
|
+
id: "antigravity",
|
|
56
|
+
accessToken,
|
|
57
|
+
refreshToken: refresh.token,
|
|
58
|
+
projectId: getString(account?.projectId) ?? refresh.projectId,
|
|
59
|
+
expires: toTimestamp(account?.expiresAt ?? account?.expires),
|
|
60
|
+
});
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return sources;
|
|
65
|
+
}
|
|
66
|
+
async function refreshAccessToken(source) {
|
|
67
|
+
if (!source.refreshToken)
|
|
68
|
+
return null;
|
|
69
|
+
const prefix = source.id === "gemini" ? "GOOGLE" : "ANTIGRAVITY";
|
|
70
|
+
const clientId = getString(process.env[`${prefix}_OAUTH_CLIENT_ID`]);
|
|
71
|
+
const clientSecret = getString(process.env[`${prefix}_OAUTH_CLIENT_SECRET`]);
|
|
72
|
+
if (!clientId || !clientSecret)
|
|
73
|
+
return null;
|
|
74
|
+
const payload = await fetchJson("https://oauth2.googleapis.com/token", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
77
|
+
body: new URLSearchParams({
|
|
78
|
+
client_id: clientId,
|
|
79
|
+
client_secret: clientSecret,
|
|
80
|
+
refresh_token: source.refreshToken,
|
|
81
|
+
grant_type: "refresh_token",
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
return getString(payload?.access_token);
|
|
85
|
+
}
|
|
86
|
+
function transformModel(sourceId, modelId, data) {
|
|
87
|
+
const fraction = toNumber(data?.quotaInfo?.remainingFraction ?? data?.remainingFraction);
|
|
88
|
+
const resetAt = toTimestamp(data?.quotaInfo?.resetTime ?? data?.resetTime);
|
|
89
|
+
const label = sourceId === "gemini" || (resetAt !== null && resetAt - Date.now() > 10 * 3600 * 1000) ? "daily" : "5h";
|
|
90
|
+
return {
|
|
91
|
+
[`${sourceId}/${modelId}`]: {
|
|
92
|
+
windows: {
|
|
93
|
+
[label]: toUsageWindow({
|
|
94
|
+
usedPercent: fraction === null ? null : 100 - fraction * 100,
|
|
95
|
+
windowSeconds: label === "daily" ? 86400 : 5 * 3600,
|
|
96
|
+
resetAt,
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const google = {
|
|
103
|
+
id: "google",
|
|
104
|
+
name: "Google",
|
|
105
|
+
aliases: ["google", "google.oauth", "gemini", "antigravity"],
|
|
106
|
+
async fetchQuota() {
|
|
107
|
+
const sources = googleSources();
|
|
108
|
+
if (sources.length === 0)
|
|
109
|
+
return notConfigured(this.id, this.name);
|
|
110
|
+
const models = {};
|
|
111
|
+
try {
|
|
112
|
+
for (const source of sources) {
|
|
113
|
+
const accessToken = source.accessToken && (!source.expires || source.expires > Date.now())
|
|
114
|
+
? source.accessToken
|
|
115
|
+
: await refreshAccessToken(source);
|
|
116
|
+
if (!accessToken)
|
|
117
|
+
continue;
|
|
118
|
+
const projectId = source.projectId ?? DEFAULT_PROJECT_ID;
|
|
119
|
+
if (source.id === "gemini") {
|
|
120
|
+
try {
|
|
121
|
+
const quota = await fetchJson(`${GOOGLE_ENDPOINTS[2]}/v1internal:retrieveUserQuota`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ project: projectId }),
|
|
125
|
+
});
|
|
126
|
+
for (const bucket of Array.isArray(quota?.buckets) ? quota.buckets : []) {
|
|
127
|
+
const modelId = getString(bucket?.modelId);
|
|
128
|
+
if (modelId)
|
|
129
|
+
Object.assign(models, transformModel(source.id, modelId, bucket));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// The model endpoint below often remains available when quota buckets are not.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const endpoint of GOOGLE_ENDPOINTS) {
|
|
137
|
+
try {
|
|
138
|
+
const payload = await fetchJson(`${endpoint}/v1internal:fetchAvailableModels`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${accessToken}`,
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
"User-Agent": "antigravity/1.11.5 windows/amd64",
|
|
144
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify({ project: projectId }),
|
|
147
|
+
});
|
|
148
|
+
for (const [modelId, data] of Object.entries(payload?.models ?? {})) {
|
|
149
|
+
Object.assign(models, transformModel(source.id, modelId, data));
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (Object.keys(models).length === 0)
|
|
159
|
+
throw new Error("No model quota data available");
|
|
160
|
+
return result(this.id, this.name, { ok: true, configured: true, usage: { windows: {}, models } });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return result(this.id, this.name, {
|
|
164
|
+
ok: false,
|
|
165
|
+
configured: true,
|
|
166
|
+
error: error instanceof Error ? error.message : "Request failed",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
export const googleProviders = [google];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { fetchJson, getCredential, notConfigured, result, toNumber, toTimestamp, toUsageWindow } from "../shared";
|
|
2
|
+
const INACTIVE_WINDOW_STATUS = 3;
|
|
3
|
+
function pickChatModel(models) {
|
|
4
|
+
return (models.find((model) => /^minimax-m/i.test(String(model?.model_name ?? "")) && (toNumber(model?.current_interval_total_count) ?? 0) > 0) ??
|
|
5
|
+
models.find((model) => ["general", "chat", "text"].includes(String(model?.model_name ?? "").toLowerCase())) ??
|
|
6
|
+
models.find((model) => toNumber(model?.current_interval_remaining_percent) !== null) ??
|
|
7
|
+
models[0] ??
|
|
8
|
+
null);
|
|
9
|
+
}
|
|
10
|
+
function isUsablePayload(payload) {
|
|
11
|
+
if (payload?.base_resp && payload.base_resp.status_code !== 0)
|
|
12
|
+
return false;
|
|
13
|
+
return Array.isArray(payload?.model_remains) && payload.model_remains.length > 0;
|
|
14
|
+
}
|
|
15
|
+
function usedPercent(model, prefix, tokenPlan) {
|
|
16
|
+
const remainingPercent = toNumber(model?.[`current_${prefix}_remaining_percent`]);
|
|
17
|
+
if (remainingPercent !== null)
|
|
18
|
+
return 100 - remainingPercent;
|
|
19
|
+
const total = toNumber(model?.[`current_${prefix}_total_count`]);
|
|
20
|
+
const rawUsage = toNumber(model?.[`current_${prefix}_usage_count`]);
|
|
21
|
+
if (!total || rawUsage === null)
|
|
22
|
+
return null;
|
|
23
|
+
const used = tokenPlan ? total - rawUsage : rawUsage;
|
|
24
|
+
return (Math.max(0, used) / total) * 100;
|
|
25
|
+
}
|
|
26
|
+
function windowSeconds(model, prefix) {
|
|
27
|
+
const startAt = toTimestamp(prefix === "interval" ? model?.start_time : model?.weekly_start_time);
|
|
28
|
+
const resetAt = toTimestamp(prefix === "interval" ? model?.end_time : model?.weekly_end_time);
|
|
29
|
+
if (startAt !== null && resetAt !== null && resetAt > startAt)
|
|
30
|
+
return Math.floor((resetAt - startAt) / 1000);
|
|
31
|
+
const remainsMs = toNumber(prefix === "interval" ? model?.remains_time : model?.weekly_remains_time);
|
|
32
|
+
return remainsMs && remainsMs > 0 ? Math.floor(remainsMs / 1000) : null;
|
|
33
|
+
}
|
|
34
|
+
function createMiniMaxProvider(input) {
|
|
35
|
+
return {
|
|
36
|
+
id: input.id,
|
|
37
|
+
name: input.name,
|
|
38
|
+
aliases: input.aliases,
|
|
39
|
+
async fetchQuota() {
|
|
40
|
+
const key = getCredential(input.aliases, ["key", "token"]);
|
|
41
|
+
if (!key)
|
|
42
|
+
return notConfigured(this.id, this.name);
|
|
43
|
+
try {
|
|
44
|
+
let tokenPlan = true;
|
|
45
|
+
let payload = null;
|
|
46
|
+
try {
|
|
47
|
+
const tokenPayload = await fetchJson(input.tokenPlanUrl, { headers: { Authorization: `Bearer ${key}` } });
|
|
48
|
+
if (isUsablePayload(tokenPayload))
|
|
49
|
+
payload = tokenPayload;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Fall back to the legacy Coding Plan endpoint below.
|
|
53
|
+
}
|
|
54
|
+
if (!payload) {
|
|
55
|
+
tokenPlan = false;
|
|
56
|
+
const codingPayload = await fetchJson(input.codingPlanUrl, { headers: { Authorization: `Bearer ${key}` } });
|
|
57
|
+
if (isUsablePayload(codingPayload))
|
|
58
|
+
payload = codingPayload;
|
|
59
|
+
}
|
|
60
|
+
if (!payload)
|
|
61
|
+
throw new Error("Provider returned no usable quota data");
|
|
62
|
+
const model = pickChatModel(Array.isArray(payload?.model_remains) ? payload.model_remains : []);
|
|
63
|
+
if (!model)
|
|
64
|
+
throw new Error("No model quota data available");
|
|
65
|
+
const intervalResetAt = toTimestamp(model?.end_time);
|
|
66
|
+
const windows = {
|
|
67
|
+
"5h": toUsageWindow({
|
|
68
|
+
usedPercent: usedPercent(model, "interval", tokenPlan),
|
|
69
|
+
windowSeconds: windowSeconds(model, "interval"),
|
|
70
|
+
resetAt: intervalResetAt,
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
const weeklyStatus = toNumber(model?.current_weekly_status);
|
|
74
|
+
const hasWeekly = weeklyStatus !== INACTIVE_WINDOW_STATUS && (toNumber(model?.current_weekly_remaining_percent) !== null || (toNumber(model?.current_weekly_total_count) ?? 0) > 0);
|
|
75
|
+
if (hasWeekly) {
|
|
76
|
+
windows.weekly = toUsageWindow({
|
|
77
|
+
usedPercent: usedPercent(model, "weekly", tokenPlan),
|
|
78
|
+
windowSeconds: windowSeconds(model, "weekly"),
|
|
79
|
+
resetAt: toTimestamp(model?.weekly_end_time),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return result(this.id, this.name, { ok: true, configured: true, usage: { windows } });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return result(this.id, this.name, {
|
|
86
|
+
ok: false,
|
|
87
|
+
configured: true,
|
|
88
|
+
error: error instanceof Error ? error.message : "Request failed",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export const miniMaxProviders = [
|
|
95
|
+
createMiniMaxProvider({
|
|
96
|
+
id: "minimax-coding-plan",
|
|
97
|
+
name: "MiniMax Coding Plan",
|
|
98
|
+
aliases: ["minimax-coding-plan", "minimax"],
|
|
99
|
+
tokenPlanUrl: "https://api.minimax.io/v1/token_plan/remains",
|
|
100
|
+
codingPlanUrl: "https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
|
|
101
|
+
}),
|
|
102
|
+
createMiniMaxProvider({
|
|
103
|
+
id: "minimax-cn-coding-plan",
|
|
104
|
+
name: "MiniMax Coding Plan CN",
|
|
105
|
+
aliases: ["minimax-cn-coding-plan", "minimaxi"],
|
|
106
|
+
tokenPlanUrl: "https://api.minimaxi.com/v1/token_plan/remains",
|
|
107
|
+
codingPlanUrl: "https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains",
|
|
108
|
+
}),
|
|
109
|
+
];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fetchJson, getAuthEntry, getCredential, notConfigured, resolveWindowLabel, safeFetch, toNumber, toTimestamp, toUsageWindow, } from "../shared";
|
|
2
|
+
const claudeAliases = ["anthropic", "claude"];
|
|
3
|
+
const claude = {
|
|
4
|
+
id: "claude",
|
|
5
|
+
name: "Claude",
|
|
6
|
+
aliases: claudeAliases,
|
|
7
|
+
async fetchQuota() {
|
|
8
|
+
const token = getCredential(claudeAliases, ["access", "token"]);
|
|
9
|
+
if (!token)
|
|
10
|
+
return notConfigured(this.id, this.name);
|
|
11
|
+
return safeFetch(this.id, this.name, async () => {
|
|
12
|
+
const payload = await fetchJson("https://api.anthropic.com/api/oauth/usage", {
|
|
13
|
+
headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
|
|
14
|
+
});
|
|
15
|
+
const windows = {};
|
|
16
|
+
for (const [key, source] of [
|
|
17
|
+
["5h", payload?.five_hour],
|
|
18
|
+
["7d", payload?.seven_day],
|
|
19
|
+
["7d-sonnet", payload?.seven_day_sonnet],
|
|
20
|
+
["7d-opus", payload?.seven_day_opus],
|
|
21
|
+
]) {
|
|
22
|
+
if (!source)
|
|
23
|
+
continue;
|
|
24
|
+
windows[key] = toUsageWindow({ usedPercent: toNumber(source.utilization), resetAt: toTimestamp(source.resets_at) });
|
|
25
|
+
}
|
|
26
|
+
return { windows };
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const codexAliases = ["openai", "codex", "chatgpt"];
|
|
31
|
+
const codex = {
|
|
32
|
+
id: "codex",
|
|
33
|
+
name: "Codex",
|
|
34
|
+
aliases: codexAliases,
|
|
35
|
+
async fetchQuota() {
|
|
36
|
+
const entry = getAuthEntry(codexAliases);
|
|
37
|
+
const token = entry && (typeof entry.access === "string" ? entry.access : typeof entry.token === "string" ? entry.token : null);
|
|
38
|
+
if (!token)
|
|
39
|
+
return notConfigured(this.id, this.name);
|
|
40
|
+
return safeFetch(this.id, this.name, async () => {
|
|
41
|
+
const accountId = typeof entry?.accountId === "string" ? entry.accountId : null;
|
|
42
|
+
const payload = await fetchJson("https://chatgpt.com/backend-api/wham/usage", {
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: `Bearer ${token}`,
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
...(accountId ? { "ChatGPT-Account-Id": accountId } : {}),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const windows = {};
|
|
50
|
+
for (const source of [payload?.rate_limit?.primary_window, payload?.rate_limit?.secondary_window]) {
|
|
51
|
+
if (!source)
|
|
52
|
+
continue;
|
|
53
|
+
const seconds = toNumber(source.limit_window_seconds);
|
|
54
|
+
windows[resolveWindowLabel(seconds)] = toUsageWindow({
|
|
55
|
+
usedPercent: toNumber(source.used_percent),
|
|
56
|
+
windowSeconds: seconds,
|
|
57
|
+
resetAt: toTimestamp(source.reset_at),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const balance = toNumber(payload?.credits?.balance);
|
|
61
|
+
if (payload?.credits?.unlimited || balance !== null) {
|
|
62
|
+
windows.credits_balance = toUsageWindow({
|
|
63
|
+
usedPercent: null,
|
|
64
|
+
valueLabel: payload?.credits?.unlimited ? "∞" : `$${balance.toFixed(2)}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { windows };
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const copilotAliases = ["github-copilot", "copilot"];
|
|
72
|
+
const copilot = {
|
|
73
|
+
id: "github-copilot",
|
|
74
|
+
name: "GitHub Copilot",
|
|
75
|
+
aliases: copilotAliases,
|
|
76
|
+
async fetchQuota() {
|
|
77
|
+
const token = getCredential(copilotAliases, ["access", "token"]);
|
|
78
|
+
if (!token)
|
|
79
|
+
return notConfigured(this.id, this.name);
|
|
80
|
+
return safeFetch(this.id, this.name, async () => {
|
|
81
|
+
const payload = await fetchJson("https://api.github.com/copilot_internal/user", {
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `token ${token}`,
|
|
84
|
+
Accept: "application/json",
|
|
85
|
+
"Editor-Version": "vscode/1.96.2",
|
|
86
|
+
"X-Github-Api-Version": "2025-04-01",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const resetAt = toTimestamp(payload?.quota_reset_date);
|
|
90
|
+
const windows = {};
|
|
91
|
+
for (const [key, snapshot] of Object.entries({
|
|
92
|
+
chat: payload?.quota_snapshots?.chat,
|
|
93
|
+
completions: payload?.quota_snapshots?.completions,
|
|
94
|
+
premium: payload?.quota_snapshots?.premium_interactions,
|
|
95
|
+
})) {
|
|
96
|
+
if (!snapshot)
|
|
97
|
+
continue;
|
|
98
|
+
const source = snapshot;
|
|
99
|
+
const entitlement = toNumber(source.entitlement);
|
|
100
|
+
const remaining = toNumber(source.remaining);
|
|
101
|
+
windows[key] = toUsageWindow({
|
|
102
|
+
usedPercent: entitlement && remaining !== null ? 100 - (remaining / entitlement) * 100 : null,
|
|
103
|
+
resetAt,
|
|
104
|
+
valueLabel: entitlement !== null && remaining !== null ? `${remaining.toFixed(0)} / ${entitlement.toFixed(0)}` : null,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return { windows };
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
export const oauthProviders = [claude, codex, copilot];
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { fetchJson, getString, notConfigured, safeFetch, toNumber, toTimestamp, toUsageWindow } from "../shared";
|
|
2
|
+
function decodeJwtExpiration(token) {
|
|
3
|
+
try {
|
|
4
|
+
const payload = token.split(".")[1];
|
|
5
|
+
if (!payload)
|
|
6
|
+
return null;
|
|
7
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
8
|
+
return typeof parsed?.exp === "number" ? parsed.exp * 1000 : null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function resolveCursorToken() {
|
|
15
|
+
const accessToken = getString(process.env.CURSOR_ACCESS_TOKEN) ?? getString(process.env.CURSOR_TOKEN);
|
|
16
|
+
const refreshToken = getString(process.env.CURSOR_REFRESH_TOKEN);
|
|
17
|
+
const expiresAt = accessToken ? decodeJwtExpiration(accessToken) : null;
|
|
18
|
+
if (accessToken && (!expiresAt || expiresAt > Date.now() + 5 * 60000))
|
|
19
|
+
return accessToken;
|
|
20
|
+
if (!refreshToken)
|
|
21
|
+
return accessToken;
|
|
22
|
+
const payload = await fetchJson("https://api2.cursor.sh/oauth/token", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify({ grant_type: "refresh_token", client_id: "KbZUR41cY7W6zRSdpSUJ7I7mLYBKOCmB", refresh_token: refreshToken }),
|
|
26
|
+
});
|
|
27
|
+
return getString(payload?.access_token);
|
|
28
|
+
}
|
|
29
|
+
async function cursorPost(url, token) {
|
|
30
|
+
return fetchJson(url, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
|
|
33
|
+
body: "{}",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const cursor = {
|
|
37
|
+
id: "cursor",
|
|
38
|
+
name: "Cursor",
|
|
39
|
+
aliases: ["cursor"],
|
|
40
|
+
async fetchQuota() {
|
|
41
|
+
const token = await resolveCursorToken().catch(() => null);
|
|
42
|
+
if (!token)
|
|
43
|
+
return notConfigured(this.id, this.name);
|
|
44
|
+
return safeFetch(this.id, this.name, async () => {
|
|
45
|
+
const usage = await cursorPost("https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", token);
|
|
46
|
+
const plan = await cursorPost("https://api2.cursor.sh/aiserver.v1.DashboardService/GetPlanInfo", token).catch(() => null);
|
|
47
|
+
const source = usage?.planUsage ?? {};
|
|
48
|
+
const limit = toNumber(source.limit);
|
|
49
|
+
const remaining = toNumber(source.remaining);
|
|
50
|
+
const explicit = toNumber(source.totalPercentUsed);
|
|
51
|
+
const resetAt = toTimestamp(usage?.billingCycleEnd ?? plan?.planInfo?.billingCycleEnd);
|
|
52
|
+
return {
|
|
53
|
+
windows: {
|
|
54
|
+
billing_cycle: toUsageWindow({
|
|
55
|
+
usedPercent: explicit ?? (limit && remaining !== null ? ((limit - remaining) / limit) * 100 : null),
|
|
56
|
+
resetAt,
|
|
57
|
+
valueLabel: toNumber(source.totalSpend) !== null ? `$${(toNumber(source.totalSpend) / 100).toFixed(2)}` : null,
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
function parseOllamaUsage(html) {
|
|
65
|
+
const windows = {};
|
|
66
|
+
for (const [label, pattern] of [
|
|
67
|
+
["session", /Session\s+usage[^0-9]*([0-9.]+)%/i],
|
|
68
|
+
["weekly", /Weekly\s+usage[^0-9]*([0-9.]+)%/i],
|
|
69
|
+
]) {
|
|
70
|
+
const match = html.match(pattern);
|
|
71
|
+
if (match)
|
|
72
|
+
windows[label] = toUsageWindow({ usedPercent: toNumber(match[1]) });
|
|
73
|
+
}
|
|
74
|
+
const premium = html.match(/Premium[^0-9]*([0-9]+)\s*\/\s*([0-9]+)/i);
|
|
75
|
+
if (premium) {
|
|
76
|
+
const used = toNumber(premium[1]);
|
|
77
|
+
const total = toNumber(premium[2]);
|
|
78
|
+
windows.premium = toUsageWindow({
|
|
79
|
+
usedPercent: total && used !== null ? (used / total) * 100 : null,
|
|
80
|
+
valueLabel: used !== null && total !== null ? `${used} / ${total}` : null,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return windows;
|
|
84
|
+
}
|
|
85
|
+
const ollamaCloud = {
|
|
86
|
+
id: "ollama-cloud",
|
|
87
|
+
name: "Ollama Cloud",
|
|
88
|
+
aliases: ["ollama-cloud", "ollamacloud"],
|
|
89
|
+
async fetchQuota() {
|
|
90
|
+
const cookie = getString(process.env.OLLAMA_CLOUD_COOKIE);
|
|
91
|
+
if (!cookie)
|
|
92
|
+
return notConfigured(this.id, this.name);
|
|
93
|
+
return safeFetch(this.id, this.name, async () => {
|
|
94
|
+
const response = await fetch("https://ollama.com/settings", {
|
|
95
|
+
headers: { Cookie: cookie, "User-Agent": "CodeNomad usage provider" },
|
|
96
|
+
redirect: "manual",
|
|
97
|
+
signal: AbortSignal.timeout(15000),
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok)
|
|
100
|
+
throw new Error(`Provider returned HTTP ${response.status}`);
|
|
101
|
+
const windows = parseOllamaUsage(await response.text());
|
|
102
|
+
if (Object.keys(windows).length === 0)
|
|
103
|
+
throw new Error("No usage data available");
|
|
104
|
+
return { windows };
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
function parseOpenCodeGoUsage(html, now = Date.now()) {
|
|
109
|
+
const normalized = html.replace(/"|"|\\u0022|\\"/g, '"');
|
|
110
|
+
const windows = {};
|
|
111
|
+
for (const [label, field] of Object.entries({ "5h": "rollingUsage", weekly: "weeklyUsage", monthly: "monthlyUsage" })) {
|
|
112
|
+
const match = normalized.match(new RegExp(`["']?${field}["']?\\s*:\\s*(?:\\$R\\[\\d+\\]\\s*=\\s*)?\\{([^{}]*)\\}`, "s"));
|
|
113
|
+
if (!match)
|
|
114
|
+
continue;
|
|
115
|
+
const capture = (name) => toNumber(match[1]?.match(new RegExp(`["']?${name}["']?\\s*:\\s*["']?(-?\\d+(?:\\.\\d+)?)`))?.[1]);
|
|
116
|
+
const usedPercent = capture("usagePercent");
|
|
117
|
+
const resetInSec = capture("resetInSec");
|
|
118
|
+
if (usedPercent !== null && resetInSec !== null) {
|
|
119
|
+
windows[label] = toUsageWindow({ usedPercent, resetAt: now + Math.max(0, resetInSec) * 1000 });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return windows;
|
|
123
|
+
}
|
|
124
|
+
const openCodeGo = {
|
|
125
|
+
id: "opencode-go",
|
|
126
|
+
name: "OpenCode Go",
|
|
127
|
+
aliases: ["opencode-go", "opencode"],
|
|
128
|
+
async fetchQuota() {
|
|
129
|
+
const workspaceId = getString(process.env.OPENCODE_GO_WORKSPACE_ID);
|
|
130
|
+
const authCookie = getString(process.env.OPENCODE_GO_AUTH_COOKIE);
|
|
131
|
+
if (!workspaceId || !authCookie)
|
|
132
|
+
return notConfigured(this.id, this.name);
|
|
133
|
+
return safeFetch(this.id, this.name, async () => {
|
|
134
|
+
const response = await fetch(`https://opencode.ai/workspace/${encodeURIComponent(workspaceId)}/go`, {
|
|
135
|
+
headers: { Cookie: `auth=${authCookie.replace(/^auth=/, "")}`, "User-Agent": "CodeNomad usage provider" },
|
|
136
|
+
redirect: "manual",
|
|
137
|
+
signal: AbortSignal.timeout(15000),
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok)
|
|
140
|
+
throw new Error(`Provider returned HTTP ${response.status}`);
|
|
141
|
+
const windows = parseOpenCodeGoUsage(await response.text());
|
|
142
|
+
if (Object.keys(windows).length === 0)
|
|
143
|
+
throw new Error("No usage data available");
|
|
144
|
+
return { windows };
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
export const specialProviders = [cursor, ollamaCloud, openCodeGo];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { apiKeyProviders } from "./providers/api-key";
|
|
2
|
+
import { googleProviders } from "./providers/google";
|
|
3
|
+
import { miniMaxProviders } from "./providers/minimax";
|
|
4
|
+
import { oauthProviders } from "./providers/oauth";
|
|
5
|
+
import { specialProviders } from "./providers/special";
|
|
6
|
+
const CACHE_TTL_MS = 60000;
|
|
7
|
+
const providers = [...oauthProviders, ...apiKeyProviders, ...miniMaxProviders, ...googleProviders, ...specialProviders];
|
|
8
|
+
const registry = new Map();
|
|
9
|
+
for (const provider of providers) {
|
|
10
|
+
registry.set(provider.id, provider);
|
|
11
|
+
for (const alias of provider.aliases)
|
|
12
|
+
registry.set(alias.toLowerCase(), provider);
|
|
13
|
+
}
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
const pending = new Map();
|
|
16
|
+
export function resolveUsageProvider(providerId) {
|
|
17
|
+
return registry.get(providerId.trim().toLowerCase()) ?? null;
|
|
18
|
+
}
|
|
19
|
+
function normalizeModelId(value) {
|
|
20
|
+
return value.toLowerCase().replace(/^models\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
21
|
+
}
|
|
22
|
+
export function selectModelWindows(result, modelId) {
|
|
23
|
+
const usage = result.usage;
|
|
24
|
+
if (!usage)
|
|
25
|
+
return {};
|
|
26
|
+
if (!modelId || !usage.models)
|
|
27
|
+
return usage.windows;
|
|
28
|
+
const target = normalizeModelId(modelId);
|
|
29
|
+
const entries = Object.entries(usage.models);
|
|
30
|
+
const exact = entries.find(([name]) => normalizeModelId(name.split("/").pop() ?? name) === target);
|
|
31
|
+
if (exact)
|
|
32
|
+
return exact[1].windows;
|
|
33
|
+
const partial = entries.find(([name]) => {
|
|
34
|
+
const candidate = normalizeModelId(name.split("/").pop() ?? name);
|
|
35
|
+
return candidate.includes(target) || target.includes(candidate);
|
|
36
|
+
});
|
|
37
|
+
return partial?.[1].windows ?? usage.windows;
|
|
38
|
+
}
|
|
39
|
+
async function fetchProvider(provider) {
|
|
40
|
+
const cached = cache.get(provider.id);
|
|
41
|
+
if (cached && cached.expiresAt > Date.now())
|
|
42
|
+
return cached.result;
|
|
43
|
+
const inFlight = pending.get(provider.id);
|
|
44
|
+
if (inFlight)
|
|
45
|
+
return inFlight;
|
|
46
|
+
const request = provider.fetchQuota().then((result) => {
|
|
47
|
+
cache.set(provider.id, { result, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
48
|
+
return result;
|
|
49
|
+
}).finally(() => pending.delete(provider.id));
|
|
50
|
+
pending.set(provider.id, request);
|
|
51
|
+
return request;
|
|
52
|
+
}
|
|
53
|
+
export async function getProviderUsage(requestedProviderId, options = {}) {
|
|
54
|
+
const provider = resolveUsageProvider(requestedProviderId);
|
|
55
|
+
if (!provider) {
|
|
56
|
+
return {
|
|
57
|
+
requestedProviderId,
|
|
58
|
+
providerId: null,
|
|
59
|
+
providerName: requestedProviderId,
|
|
60
|
+
modelId: options.modelId,
|
|
61
|
+
supported: false,
|
|
62
|
+
configured: false,
|
|
63
|
+
ok: false,
|
|
64
|
+
windows: {},
|
|
65
|
+
fetchedAt: Date.now(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const result = await fetchProvider(provider);
|
|
69
|
+
return {
|
|
70
|
+
requestedProviderId,
|
|
71
|
+
providerId: provider.id,
|
|
72
|
+
providerName: result.providerName,
|
|
73
|
+
modelId: options.modelId,
|
|
74
|
+
supported: true,
|
|
75
|
+
configured: result.configured,
|
|
76
|
+
ok: result.ok,
|
|
77
|
+
windows: selectModelWindows(result, options.modelId),
|
|
78
|
+
fetchedAt: result.fetchedAt,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function clearProviderUsageCache() {
|
|
82
|
+
cache.clear();
|
|
83
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { getProviderUsage, resolveUsageProvider, selectModelWindows } from "./service";
|
|
4
|
+
const providerIds = [
|
|
5
|
+
"claude",
|
|
6
|
+
"codex",
|
|
7
|
+
"github-copilot",
|
|
8
|
+
"google",
|
|
9
|
+
"kimi-for-coding",
|
|
10
|
+
"nano-gpt",
|
|
11
|
+
"openrouter",
|
|
12
|
+
"zai-coding-plan",
|
|
13
|
+
"zhipuai-coding-plan",
|
|
14
|
+
"minimax-coding-plan",
|
|
15
|
+
"minimax-cn-coding-plan",
|
|
16
|
+
"ollama-cloud",
|
|
17
|
+
"wafer",
|
|
18
|
+
"opencode-go",
|
|
19
|
+
"cursor",
|
|
20
|
+
];
|
|
21
|
+
test("registers every supported usage provider", () => {
|
|
22
|
+
for (const providerId of providerIds) {
|
|
23
|
+
assert.equal(resolveUsageProvider(providerId)?.id, providerId);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
test("maps OpenCode provider aliases to their usage provider", () => {
|
|
27
|
+
assert.equal(resolveUsageProvider("anthropic")?.id, "claude");
|
|
28
|
+
assert.equal(resolveUsageProvider("openai")?.id, "codex");
|
|
29
|
+
assert.equal(resolveUsageProvider("copilot")?.id, "github-copilot");
|
|
30
|
+
assert.equal(resolveUsageProvider("gemini")?.id, "google");
|
|
31
|
+
assert.equal(resolveUsageProvider("opencode")?.id, "opencode-go");
|
|
32
|
+
});
|
|
33
|
+
test("selects model-specific windows by normalized model id", () => {
|
|
34
|
+
const result = {
|
|
35
|
+
providerId: "google",
|
|
36
|
+
providerName: "Google",
|
|
37
|
+
ok: true,
|
|
38
|
+
configured: true,
|
|
39
|
+
fetchedAt: 1,
|
|
40
|
+
usage: {
|
|
41
|
+
windows: {},
|
|
42
|
+
models: {
|
|
43
|
+
"gemini/gemini-2.5-flash": {
|
|
44
|
+
windows: {
|
|
45
|
+
daily: { usedPercent: 25, remainingPercent: 75, windowSeconds: 86400, resetAt: null },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
assert.equal(selectModelWindows(result, "models/gemini-2.5-flash").daily?.usedPercent, 25);
|
|
52
|
+
});
|
|
53
|
+
test("returns a stable unsupported response without making a provider request", async () => {
|
|
54
|
+
const response = await getProviderUsage("unknown-provider", { modelId: "model" });
|
|
55
|
+
assert.equal(response.supported, false);
|
|
56
|
+
assert.equal(response.configured, false);
|
|
57
|
+
assert.equal(response.providerId, null);
|
|
58
|
+
assert.deepEqual(response.windows, {});
|
|
59
|
+
});
|