ag-quota 0.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/LICENSE +15 -0
- package/README.md +115 -0
- package/bin/ag-quota.js +466 -0
- package/dist/cli-auth.d.ts +12 -0
- package/dist/cli-auth.d.ts.map +1 -0
- package/dist/cli-auth.js +89 -0
- package/dist/cloud.d.ts +40 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +110 -0
- package/dist/config.d.ts +101 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +88 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +248 -0
- package/package.json +57 -0
- package/src/cli-auth.ts +136 -0
- package/src/cli.ts +154 -0
- package/src/cloud.ts +195 -0
- package/src/config.ts +193 -0
- package/src/index.test.ts +96 -0
- package/src/index.ts +379 -0
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ag-quota",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Antigravity quota fetching library and CLI",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ag-quota": "./bin/ag-quota.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"bin",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bun run build:lib && bun run build:cli",
|
|
26
|
+
"build:lib": "tsc",
|
|
27
|
+
"build:cli": "bun build src/cli.ts --outfile bin/ag-quota.js --target node",
|
|
28
|
+
"prepublishOnly": "bun run build",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"ag-quota": "bun src/cli.ts"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"antigravity",
|
|
34
|
+
"quota",
|
|
35
|
+
"windsurf",
|
|
36
|
+
"codeium",
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
39
|
+
"author": "Philipp",
|
|
40
|
+
"license": "ISC",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/phibkro/opencode-quota-display.git",
|
|
44
|
+
"directory": "packages/ag-quota"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/phibkro/opencode-quota-display/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/phibkro/opencode-quota-display/tree/main/packages/ag-quota#readme",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.0.0",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/cli-auth.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Authentication helper for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* This logic is duplicated here specifically for the CLI to be standalone
|
|
5
|
+
* and user-friendly, without polluting the core library exports which
|
|
6
|
+
* should remain pure and environment-agnostic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Constants
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
const ANTIGRAVITY_CLIENT_ID =
|
|
18
|
+
"1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
|
19
|
+
const ANTIGRAVITY_CLIENT_SECRET = "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";
|
|
20
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Types
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
interface StoredAccount {
|
|
27
|
+
email: string;
|
|
28
|
+
refreshToken: string;
|
|
29
|
+
projectId?: string;
|
|
30
|
+
managedProjectId?: string;
|
|
31
|
+
addedAt: number;
|
|
32
|
+
lastUsed: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AccountsFile {
|
|
36
|
+
version: number;
|
|
37
|
+
accounts: StoredAccount[];
|
|
38
|
+
activeIndex: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface TokenResponse {
|
|
42
|
+
access_token: string;
|
|
43
|
+
expires_in: number;
|
|
44
|
+
token_type: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CLICloudCredentials {
|
|
48
|
+
accessToken: string;
|
|
49
|
+
projectId?: string;
|
|
50
|
+
email: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Logic
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
function getAccountsFilePath(): string {
|
|
58
|
+
return join(homedir(), ".config", "opencode", "antigravity-accounts.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadAccounts(): AccountsFile {
|
|
62
|
+
const accountsPath = getAccountsFilePath();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const content = readFileSync(accountsPath, "utf-8");
|
|
66
|
+
const data = JSON.parse(content) as AccountsFile;
|
|
67
|
+
|
|
68
|
+
if (!data.accounts || data.accounts.length === 0) {
|
|
69
|
+
throw new Error("No accounts found in antigravity-accounts.json");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return data;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
75
|
+
throw new Error("Antigravity accounts file not found.");
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function refreshAccessToken(refreshToken: string): Promise<string> {
|
|
82
|
+
const response = await fetch(TOKEN_URL, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
86
|
+
},
|
|
87
|
+
body: new URLSearchParams({
|
|
88
|
+
client_id: ANTIGRAVITY_CLIENT_ID,
|
|
89
|
+
client_secret: ANTIGRAVITY_CLIENT_SECRET,
|
|
90
|
+
refresh_token: refreshToken,
|
|
91
|
+
grant_type: "refresh_token",
|
|
92
|
+
}).toString(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const errorText = await response.text();
|
|
97
|
+
throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = (await response.json()) as TokenResponse;
|
|
101
|
+
return data.access_token;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Attempt to get cloud credentials from the local environment.
|
|
106
|
+
* Returns null if no credentials found or file missing.
|
|
107
|
+
* Throws if file exists but is invalid or refresh fails.
|
|
108
|
+
*/
|
|
109
|
+
export async function getCLICloudCredentials(): Promise<CLICloudCredentials | null> {
|
|
110
|
+
try {
|
|
111
|
+
// Load accounts
|
|
112
|
+
const accountsFile = loadAccounts();
|
|
113
|
+
const activeAccount =
|
|
114
|
+
accountsFile.accounts[accountsFile.activeIndex] ?? accountsFile.accounts[0];
|
|
115
|
+
|
|
116
|
+
if (!activeAccount) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get access token
|
|
121
|
+
const accessToken = await refreshAccessToken(activeAccount.refreshToken);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
accessToken,
|
|
125
|
+
projectId: activeAccount.projectId,
|
|
126
|
+
email: activeAccount.email,
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// If file not found, just return null (not available)
|
|
130
|
+
if (error instanceof Error && error.message.includes("not found")) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// If other error (parsing, network), throw it
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* ISC License
|
|
4
|
+
* Copyright (c) 2026 Philipp
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import {
|
|
9
|
+
fetchQuota,
|
|
10
|
+
formatRelativeTime,
|
|
11
|
+
type ShellRunner,
|
|
12
|
+
type QuotaSource,
|
|
13
|
+
type CloudAuthCredentials,
|
|
14
|
+
} from "./index";
|
|
15
|
+
import { getCLICloudCredentials } from "./cli-auth";
|
|
16
|
+
|
|
17
|
+
const shellRunner: ShellRunner = async (cmd: string) =>
|
|
18
|
+
execSync(cmd).toString();
|
|
19
|
+
|
|
20
|
+
function parseArgs(): { source: QuotaSource; json: boolean; help: boolean; token: string; projectId: string } {
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
let source: QuotaSource = "auto";
|
|
23
|
+
let json = false;
|
|
24
|
+
let help = false;
|
|
25
|
+
let token = "";
|
|
26
|
+
let projectId = "";
|
|
27
|
+
|
|
28
|
+
for (const arg of args) {
|
|
29
|
+
if (arg === "--json") {
|
|
30
|
+
json = true;
|
|
31
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
32
|
+
help = true;
|
|
33
|
+
} else if (arg === "--source=cloud" || arg === "-s=cloud") {
|
|
34
|
+
source = "cloud";
|
|
35
|
+
} else if (arg === "--source=local" || arg === "-s=local") {
|
|
36
|
+
source = "local";
|
|
37
|
+
} else if (arg === "--source=auto" || arg === "-s=auto") {
|
|
38
|
+
source = "auto";
|
|
39
|
+
} else if (arg.startsWith("--token=")) {
|
|
40
|
+
token = arg.split("=")[1];
|
|
41
|
+
} else if (arg.startsWith("--project-id=")) {
|
|
42
|
+
projectId = arg.split("=")[1];
|
|
43
|
+
} else if (arg.startsWith("--source=") || arg.startsWith("-s=")) {
|
|
44
|
+
const value = arg.split("=")[1];
|
|
45
|
+
console.error(`Invalid source: ${value}. Use 'cloud', 'local', or 'auto'.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { source, json, help, token, projectId };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function run() {
|
|
54
|
+
const { source, json: isJson, help: isHelp, token, projectId } = parseArgs();
|
|
55
|
+
|
|
56
|
+
if (isHelp) {
|
|
57
|
+
console.log(`
|
|
58
|
+
Usage: ag-quota [options]
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--source=<cloud|local|auto> Quota source (default: auto)
|
|
62
|
+
-s=<cloud|local|auto> Alias for --source
|
|
63
|
+
--token=<token> Access token (override auto-discovery)
|
|
64
|
+
--project-id=<id> Google Cloud Project ID (optional)
|
|
65
|
+
--json Output result as JSON
|
|
66
|
+
-h, --help Show this help message
|
|
67
|
+
|
|
68
|
+
Sources:
|
|
69
|
+
cloud Fetch from Cloud Code API (uses auto-discovery or --token)
|
|
70
|
+
local Fetch from local language server process
|
|
71
|
+
auto Try cloud first, fallback to local (default)
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
ag-quota # Auto-detect (tries cloud then local)
|
|
75
|
+
ag-quota --source=cloud # Force cloud (auto-discover token)
|
|
76
|
+
ag-quota --token=... # Force cloud with specific token
|
|
77
|
+
ag-quota --source=local # Force local source
|
|
78
|
+
ag-quota --json # Output as JSON
|
|
79
|
+
`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Resolve credentials
|
|
85
|
+
let cloudAuth: CloudAuthCredentials | undefined = token ? { accessToken: token, projectId } : undefined;
|
|
86
|
+
|
|
87
|
+
// If no token provided, try to auto-discover
|
|
88
|
+
if (!cloudAuth && (source === "cloud" || source === "auto")) {
|
|
89
|
+
const creds = await getCLICloudCredentials();
|
|
90
|
+
if (creds) {
|
|
91
|
+
cloudAuth = { accessToken: creds.accessToken, projectId: creds.projectId };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (source === "cloud" && !cloudAuth) {
|
|
96
|
+
throw new Error("Cloud credentials not found. Run 'opencode auth login' or provide --token.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = await fetchQuota(source, shellRunner, cloudAuth);
|
|
100
|
+
|
|
101
|
+
if (isJson) {
|
|
102
|
+
console.log(
|
|
103
|
+
JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
source: result.source,
|
|
106
|
+
timestamp: result.timestamp,
|
|
107
|
+
categories: result.categories.map((cat) => ({
|
|
108
|
+
name: cat.category,
|
|
109
|
+
remainingFraction: cat.remainingFraction,
|
|
110
|
+
remainingPercentage: parseFloat(
|
|
111
|
+
(cat.remainingFraction * 100).toFixed(1),
|
|
112
|
+
),
|
|
113
|
+
resetTime: cat.resetTime?.toISOString() ?? null,
|
|
114
|
+
resetsIn: cat.resetTime
|
|
115
|
+
? formatRelativeTime(cat.resetTime)
|
|
116
|
+
: null,
|
|
117
|
+
})),
|
|
118
|
+
},
|
|
119
|
+
null,
|
|
120
|
+
2,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const sourceLabel = result.source === "cloud" ? "Cloud API" : "Local Server";
|
|
127
|
+
console.log(
|
|
128
|
+
`\nAntigravity Quotas (Source: ${sourceLabel}, ${new Date(result.timestamp).toLocaleTimeString()}):`,
|
|
129
|
+
);
|
|
130
|
+
console.log(
|
|
131
|
+
"------------------------------------------------------------",
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
for (const cat of result.categories) {
|
|
135
|
+
const remaining = (cat.remainingFraction * 100).toFixed(1);
|
|
136
|
+
let output = `${cat.category.padEnd(20)}: ${remaining.padStart(5)}% remaining`;
|
|
137
|
+
if (cat.resetTime) {
|
|
138
|
+
output += ` (Resets in: ${formatRelativeTime(cat.resetTime)})`;
|
|
139
|
+
}
|
|
140
|
+
console.log(output);
|
|
141
|
+
}
|
|
142
|
+
console.log("");
|
|
143
|
+
} catch (error: unknown) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
if (isJson) {
|
|
146
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
147
|
+
} else {
|
|
148
|
+
console.error("Error:", message);
|
|
149
|
+
}
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
run();
|
package/src/cloud.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ISC License
|
|
3
|
+
* Copyright (c) 2025, Cristian Militaru
|
|
4
|
+
* Copyright (c) 2026, Philipp
|
|
5
|
+
*
|
|
6
|
+
* Cloud quota fetching via Google Cloud Code API.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants from opencode-antigravity-auth
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
// Endpoint fallback order (daily → autopush → prod)
|
|
14
|
+
const CLOUDCODE_ENDPOINTS = [
|
|
15
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
16
|
+
"https://autopush-cloudcode-pa.sandbox.googleapis.com",
|
|
17
|
+
"https://cloudcode-pa.googleapis.com",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
// Headers matching opencode-antigravity-auth
|
|
21
|
+
const CLOUDCODE_HEADERS = {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"User-Agent": "antigravity/1.11.5 windows/amd64",
|
|
24
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
25
|
+
"Client-Metadata":
|
|
26
|
+
'{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Types
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
interface CloudQuotaInfo {
|
|
34
|
+
remainingFraction?: number;
|
|
35
|
+
resetTime?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CloudModelInfo {
|
|
39
|
+
displayName?: string;
|
|
40
|
+
model?: string;
|
|
41
|
+
quotaInfo?: CloudQuotaInfo;
|
|
42
|
+
supportsImages?: boolean;
|
|
43
|
+
supportsThinking?: boolean;
|
|
44
|
+
recommended?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface FetchModelsResponse {
|
|
48
|
+
models?: Record<string, CloudModelInfo>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Unified quota info structure (shared between cloud and local)
|
|
53
|
+
*/
|
|
54
|
+
export interface QuotaInfo {
|
|
55
|
+
remainingFraction: number;
|
|
56
|
+
resetTime?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Unified model config structure (shared between cloud and local)
|
|
61
|
+
*/
|
|
62
|
+
export interface ModelConfig {
|
|
63
|
+
modelName: string;
|
|
64
|
+
label?: string;
|
|
65
|
+
quotaInfo?: QuotaInfo;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cloud-specific account info
|
|
70
|
+
*/
|
|
71
|
+
export interface CloudAccountInfo {
|
|
72
|
+
email?: string;
|
|
73
|
+
projectId?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Result from cloud quota fetch
|
|
78
|
+
*/
|
|
79
|
+
export interface CloudQuotaResult {
|
|
80
|
+
account: CloudAccountInfo;
|
|
81
|
+
models: ModelConfig[];
|
|
82
|
+
timestamp: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// API Fetching
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Fetch available models with quota from the Cloud Code API.
|
|
91
|
+
*/
|
|
92
|
+
async function fetchAvailableModels(
|
|
93
|
+
accessToken: string,
|
|
94
|
+
projectId?: string,
|
|
95
|
+
): Promise<FetchModelsResponse> {
|
|
96
|
+
const payload = projectId ? { project: projectId } : {};
|
|
97
|
+
let lastError: Error | null = null;
|
|
98
|
+
|
|
99
|
+
const headers: Record<string, string> = {
|
|
100
|
+
...CLOUDCODE_HEADERS,
|
|
101
|
+
Authorization: `Bearer ${accessToken}`,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const endpoint of CLOUDCODE_ENDPOINTS) {
|
|
105
|
+
try {
|
|
106
|
+
const url = `${endpoint}/v1internal:fetchAvailableModels`;
|
|
107
|
+
|
|
108
|
+
const response = await fetch(url, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers,
|
|
111
|
+
body: JSON.stringify(payload),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (response.status === 401) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Authorization expired or invalid.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (response.status === 403) {
|
|
121
|
+
throw new Error("Access forbidden (403). Check your account permissions.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const text = await response.text();
|
|
126
|
+
throw new Error(`Cloud Code API error ${response.status}: ${text.slice(0, 200)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (await response.json()) as FetchModelsResponse;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
132
|
+
// Auth errors should not fallback to other endpoints
|
|
133
|
+
if (
|
|
134
|
+
lastError.message.includes("Authorization") ||
|
|
135
|
+
lastError.message.includes("forbidden") ||
|
|
136
|
+
lastError.message.includes("invalid_grant")
|
|
137
|
+
) {
|
|
138
|
+
throw lastError;
|
|
139
|
+
}
|
|
140
|
+
// Try next endpoint
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw lastError || new Error("All Cloud Code API endpoints failed");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Main Export
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Fetch quota information from the Cloud Code API.
|
|
153
|
+
*
|
|
154
|
+
* @param accessToken - Valid OAuth2 access token
|
|
155
|
+
* @param projectId - Optional Google Cloud project ID
|
|
156
|
+
* @returns CloudQuotaResult with account info and model quotas
|
|
157
|
+
* @throws Error if API fails
|
|
158
|
+
*/
|
|
159
|
+
export async function fetchCloudQuota(
|
|
160
|
+
accessToken: string,
|
|
161
|
+
projectId?: string,
|
|
162
|
+
): Promise<CloudQuotaResult> {
|
|
163
|
+
if (!accessToken) {
|
|
164
|
+
throw new Error("Access token is required for cloud quota fetching");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Fetch quota data
|
|
168
|
+
const response = await fetchAvailableModels(accessToken, projectId);
|
|
169
|
+
|
|
170
|
+
// Convert to unified format
|
|
171
|
+
const models: ModelConfig[] = [];
|
|
172
|
+
|
|
173
|
+
if (response.models) {
|
|
174
|
+
for (const [modelKey, info] of Object.entries(response.models)) {
|
|
175
|
+
if (!info.quotaInfo) continue;
|
|
176
|
+
|
|
177
|
+
models.push({
|
|
178
|
+
modelName: info.model || modelKey,
|
|
179
|
+
label: info.displayName || modelKey,
|
|
180
|
+
quotaInfo: {
|
|
181
|
+
remainingFraction: info.quotaInfo.remainingFraction ?? 0,
|
|
182
|
+
resetTime: info.quotaInfo.resetTime,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
account: {
|
|
190
|
+
projectId,
|
|
191
|
+
},
|
|
192
|
+
models,
|
|
193
|
+
timestamp: Date.now(),
|
|
194
|
+
};
|
|
195
|
+
}
|