Initial commit: restructure to flat layout with ui/ and web/ at root
This commit is contained in:
92
web/server/lib/quota/providers/google/api.js
Normal file
92
web/server/lib/quota/providers/google/api.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Google Provider - API
|
||||
*
|
||||
* API calls for Google quota providers.
|
||||
* @module quota/providers/google/api
|
||||
*/
|
||||
|
||||
const GOOGLE_PRIMARY_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
|
||||
|
||||
const GOOGLE_ENDPOINTS = [
|
||||
'https://daily-cloudcode-pa.sandbox.googleapis.com',
|
||||
'https://autopush-cloudcode-pa.sandbox.googleapis.com',
|
||||
GOOGLE_PRIMARY_ENDPOINT
|
||||
];
|
||||
|
||||
const GOOGLE_HEADERS = {
|
||||
'User-Agent': 'antigravity/1.11.5 windows/amd64',
|
||||
'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
|
||||
'Client-Metadata':
|
||||
'{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}'
|
||||
};
|
||||
|
||||
export const refreshGoogleAccessToken = async (refreshToken, clientId, clientSecret) => {
|
||||
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return typeof data?.access_token === 'string' ? data.access_token : null;
|
||||
};
|
||||
|
||||
export const fetchGoogleQuotaBuckets = async (accessToken, projectId) => {
|
||||
const body = projectId ? { project: projectId } : {};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${GOOGLE_PRIMARY_ENDPOINT}/v1internal:retrieveUserQuota`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(15000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchGoogleModels = async (accessToken, projectId) => {
|
||||
const body = projectId ? { project: projectId } : {};
|
||||
|
||||
for (const endpoint of GOOGLE_ENDPOINTS) {
|
||||
try {
|
||||
const response = await fetch(`${endpoint}/v1internal:fetchAvailableModels`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
...GOOGLE_HEADERS
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(15000)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
108
web/server/lib/quota/providers/google/auth.js
Normal file
108
web/server/lib/quota/providers/google/auth.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Google Provider - Auth
|
||||
*
|
||||
* Authentication resolution logic for Google quota providers.
|
||||
* @module quota/providers/google/auth
|
||||
*/
|
||||
|
||||
import {
|
||||
ANTIGRAVITY_ACCOUNTS_PATHS,
|
||||
readJsonFile,
|
||||
getAuthEntry,
|
||||
normalizeAuthEntry,
|
||||
asObject,
|
||||
asNonEmptyString,
|
||||
toTimestamp
|
||||
} from '../../utils/index.js';
|
||||
import { readAuthFile } from '../../../opencode/auth.js';
|
||||
import { parseGoogleRefreshToken } from './transforms.js';
|
||||
|
||||
const ANTIGRAVITY_GOOGLE_CLIENT_ID =
|
||||
'1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
|
||||
const ANTIGRAVITY_GOOGLE_CLIENT_SECRET = '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';
|
||||
const GEMINI_GOOGLE_CLIENT_ID =
|
||||
'681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
|
||||
const GEMINI_GOOGLE_CLIENT_SECRET = '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';
|
||||
export const DEFAULT_PROJECT_ID = 'rising-fact-p41fc';
|
||||
|
||||
export const resolveGoogleOAuthClient = (sourceId) => {
|
||||
if (sourceId === 'gemini') {
|
||||
return {
|
||||
clientId: GEMINI_GOOGLE_CLIENT_ID,
|
||||
clientSecret: GEMINI_GOOGLE_CLIENT_SECRET
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
clientId: ANTIGRAVITY_GOOGLE_CLIENT_ID,
|
||||
clientSecret: ANTIGRAVITY_GOOGLE_CLIENT_SECRET
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveGeminiCliAuth = (auth) => {
|
||||
const entry = normalizeAuthEntry(getAuthEntry(auth, ['google', 'google.oauth']));
|
||||
const entryObject = asObject(entry);
|
||||
if (!entryObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const oauthObject = asObject(entryObject.oauth) ?? entryObject;
|
||||
const accessToken = asNonEmptyString(oauthObject.access) ?? asNonEmptyString(oauthObject.token);
|
||||
const refreshParts = parseGoogleRefreshToken(oauthObject.refresh);
|
||||
|
||||
if (!accessToken && !refreshParts.refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
sourceId: 'gemini',
|
||||
sourceLabel: 'Gemini',
|
||||
accessToken,
|
||||
refreshToken: refreshParts.refreshToken,
|
||||
projectId: refreshParts.projectId ?? refreshParts.managedProjectId,
|
||||
expires: toTimestamp(oauthObject.expires)
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveAntigravityAuth = () => {
|
||||
for (const filePath of ANTIGRAVITY_ACCOUNTS_PATHS) {
|
||||
const data = readJsonFile(filePath);
|
||||
const accounts = data?.accounts;
|
||||
if (Array.isArray(accounts) && accounts.length > 0) {
|
||||
const index = typeof data.activeIndex === 'number' ? data.activeIndex : 0;
|
||||
const account = accounts[index] ?? accounts[0];
|
||||
if (account?.refreshToken) {
|
||||
const refreshParts = parseGoogleRefreshToken(account.refreshToken);
|
||||
return {
|
||||
sourceId: 'antigravity',
|
||||
sourceLabel: 'Antigravity',
|
||||
refreshToken: refreshParts.refreshToken,
|
||||
projectId: asNonEmptyString(account.projectId)
|
||||
?? asNonEmptyString(account.managedProjectId)
|
||||
?? refreshParts.projectId
|
||||
?? refreshParts.managedProjectId,
|
||||
email: account.email
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const resolveGoogleAuthSources = () => {
|
||||
const auth = readAuthFile();
|
||||
const sources = [];
|
||||
|
||||
const geminiAuth = resolveGeminiCliAuth(auth);
|
||||
if (geminiAuth) {
|
||||
sources.push(geminiAuth);
|
||||
}
|
||||
|
||||
const antigravityAuth = resolveAntigravityAuth();
|
||||
if (antigravityAuth) {
|
||||
sources.push(antigravityAuth);
|
||||
}
|
||||
|
||||
return sources;
|
||||
};
|
||||
124
web/server/lib/quota/providers/google/index.js
Normal file
124
web/server/lib/quota/providers/google/index.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Google Provider
|
||||
*
|
||||
* Google quota provider implementation.
|
||||
* @module quota/providers/google
|
||||
*/
|
||||
|
||||
export {
|
||||
resolveGoogleOAuthClient,
|
||||
resolveGeminiCliAuth,
|
||||
resolveAntigravityAuth,
|
||||
resolveGoogleAuthSources,
|
||||
DEFAULT_PROJECT_ID
|
||||
} from './auth.js';
|
||||
|
||||
export {
|
||||
resolveGoogleWindow,
|
||||
transformQuotaBucket,
|
||||
transformModelData
|
||||
} from './transforms.js';
|
||||
|
||||
export {
|
||||
refreshGoogleAccessToken,
|
||||
fetchGoogleQuotaBuckets,
|
||||
fetchGoogleModels
|
||||
} from './api.js';
|
||||
|
||||
import { buildResult } from '../../utils/index.js';
|
||||
import {
|
||||
resolveGoogleAuthSources,
|
||||
resolveGoogleOAuthClient,
|
||||
DEFAULT_PROJECT_ID
|
||||
} from './auth.js';
|
||||
import { transformQuotaBucket, transformModelData } from './transforms.js';
|
||||
import {
|
||||
refreshGoogleAccessToken,
|
||||
fetchGoogleQuotaBuckets,
|
||||
fetchGoogleModels
|
||||
} from './api.js';
|
||||
|
||||
export const fetchGoogleQuota = async () => {
|
||||
const authSources = resolveGoogleAuthSources();
|
||||
if (!authSources.length) {
|
||||
return buildResult({
|
||||
providerId: 'google',
|
||||
providerName: 'Google',
|
||||
ok: false,
|
||||
configured: false,
|
||||
error: 'Not configured'
|
||||
});
|
||||
}
|
||||
|
||||
const models = {};
|
||||
const sourceErrors = [];
|
||||
|
||||
for (const source of authSources) {
|
||||
const now = Date.now();
|
||||
let accessToken = source.accessToken;
|
||||
|
||||
if (!accessToken || (typeof source.expires === 'number' && source.expires <= now)) {
|
||||
if (!source.refreshToken) {
|
||||
sourceErrors.push(`${source.sourceLabel}: Missing refresh token`);
|
||||
continue;
|
||||
}
|
||||
const { clientId, clientSecret } = resolveGoogleOAuthClient(source.sourceId);
|
||||
accessToken = await refreshGoogleAccessToken(source.refreshToken, clientId, clientSecret);
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
sourceErrors.push(`${source.sourceLabel}: Failed to refresh OAuth token`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const projectId = source.projectId ?? DEFAULT_PROJECT_ID;
|
||||
let mergedAnyModel = false;
|
||||
|
||||
if (source.sourceId === 'gemini') {
|
||||
const quotaPayload = await fetchGoogleQuotaBuckets(accessToken, projectId);
|
||||
const buckets = Array.isArray(quotaPayload?.buckets) ? quotaPayload.buckets : [];
|
||||
|
||||
for (const bucket of buckets) {
|
||||
const transformed = transformQuotaBucket(bucket, source.sourceId);
|
||||
if (transformed) {
|
||||
Object.assign(models, transformed);
|
||||
mergedAnyModel = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await fetchGoogleModels(accessToken, projectId);
|
||||
if (payload) {
|
||||
for (const [modelName, modelData] of Object.entries(payload.models ?? {})) {
|
||||
const transformed = transformModelData(modelName, modelData, source.sourceId);
|
||||
Object.assign(models, transformed);
|
||||
mergedAnyModel = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mergedAnyModel) {
|
||||
sourceErrors.push(`${source.sourceLabel}: Failed to fetch models`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(models).length) {
|
||||
return buildResult({
|
||||
providerId: 'google',
|
||||
providerName: 'Google',
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: sourceErrors[0] ?? 'Failed to fetch models'
|
||||
});
|
||||
}
|
||||
|
||||
return buildResult({
|
||||
providerId: 'google',
|
||||
providerName: 'Google',
|
||||
ok: true,
|
||||
configured: true,
|
||||
usage: {
|
||||
windows: {},
|
||||
models: Object.keys(models).length ? models : undefined
|
||||
}
|
||||
});
|
||||
};
|
||||
109
web/server/lib/quota/providers/google/transforms.js
Normal file
109
web/server/lib/quota/providers/google/transforms.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Google Provider - Transforms
|
||||
*
|
||||
* Data transformation functions for Google quota responses.
|
||||
* @module quota/providers/google/transforms
|
||||
*/
|
||||
|
||||
import {
|
||||
asNonEmptyString,
|
||||
toNumber,
|
||||
toTimestamp,
|
||||
toUsageWindow
|
||||
} from '../../utils/index.js';
|
||||
|
||||
const GOOGLE_FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
|
||||
const GOOGLE_DAILY_WINDOW_SECONDS = 24 * 60 * 60;
|
||||
|
||||
export const parseGoogleRefreshToken = (rawRefreshToken) => {
|
||||
const refreshToken = asNonEmptyString(rawRefreshToken);
|
||||
if (!refreshToken) {
|
||||
return { refreshToken: null, projectId: null, managedProjectId: null };
|
||||
}
|
||||
|
||||
const [rawToken = '', rawProject = '', rawManagedProject = ''] = refreshToken.split('|');
|
||||
return {
|
||||
refreshToken: asNonEmptyString(rawToken),
|
||||
projectId: asNonEmptyString(rawProject),
|
||||
managedProjectId: asNonEmptyString(rawManagedProject)
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveGoogleWindow = (sourceId, resetAt) => {
|
||||
if (sourceId === 'gemini') {
|
||||
return { label: 'daily', seconds: GOOGLE_DAILY_WINDOW_SECONDS };
|
||||
}
|
||||
|
||||
if (sourceId === 'antigravity') {
|
||||
const remainingSeconds = typeof resetAt === 'number'
|
||||
? Math.max(0, Math.round((resetAt - Date.now()) / 1000))
|
||||
: null;
|
||||
|
||||
if (remainingSeconds !== null && remainingSeconds > 10 * 60 * 60) {
|
||||
return { label: 'daily', seconds: GOOGLE_DAILY_WINDOW_SECONDS };
|
||||
}
|
||||
|
||||
return { label: '5h', seconds: GOOGLE_FIVE_HOUR_WINDOW_SECONDS };
|
||||
}
|
||||
|
||||
return { label: 'daily', seconds: GOOGLE_DAILY_WINDOW_SECONDS };
|
||||
};
|
||||
|
||||
export const transformQuotaBucket = (bucket, sourceId) => {
|
||||
const modelId = asNonEmptyString(bucket?.modelId);
|
||||
if (!modelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scopedName = modelId.startsWith(`${sourceId}/`)
|
||||
? modelId
|
||||
: `${sourceId}/${modelId}`;
|
||||
|
||||
const remainingFraction = toNumber(bucket?.remainingFraction);
|
||||
const remainingPercent = remainingFraction !== null
|
||||
? Math.round(remainingFraction * 100)
|
||||
: null;
|
||||
const usedPercent = remainingPercent !== null ? Math.max(0, 100 - remainingPercent) : null;
|
||||
const resetAt = toTimestamp(bucket?.resetTime);
|
||||
const window = resolveGoogleWindow(sourceId, resetAt);
|
||||
|
||||
return {
|
||||
[scopedName]: {
|
||||
windows: {
|
||||
[window.label]: toUsageWindow({
|
||||
usedPercent,
|
||||
windowSeconds: window.seconds,
|
||||
resetAt
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const transformModelData = (modelName, modelData, sourceId) => {
|
||||
const scopedName = modelName.startsWith(`${sourceId}/`)
|
||||
? modelName
|
||||
: `${sourceId}/${modelName}`;
|
||||
|
||||
const remainingFraction = modelData?.quotaInfo?.remainingFraction;
|
||||
const remainingPercent = typeof remainingFraction === 'number'
|
||||
? Math.round(remainingFraction * 100)
|
||||
: null;
|
||||
const usedPercent = remainingPercent !== null ? Math.max(0, 100 - remainingPercent) : null;
|
||||
const resetAt = modelData?.quotaInfo?.resetTime
|
||||
? new Date(modelData.quotaInfo.resetTime).getTime()
|
||||
: null;
|
||||
const window = resolveGoogleWindow(sourceId, resetAt);
|
||||
|
||||
return {
|
||||
[scopedName]: {
|
||||
windows: {
|
||||
[window.label]: toUsageWindow({
|
||||
usedPercent,
|
||||
windowSeconds: window.seconds,
|
||||
resetAt
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user