antigravity-claude-proxy 2.2.0 → 2.2.1
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/package.json +1 -1
- package/src/account-manager/credentials.js +174 -26
- package/src/account-manager/index.js +2 -1
- package/src/account-manager/onboarding.js +5 -10
- package/src/account-manager/strategies/index.js +2 -8
- package/src/auth/oauth.js +37 -3
- package/src/cli/accounts.js +7 -11
- package/src/cloudcode/model-api.js +1 -1
- package/src/constants.js +8 -0
- package/src/index.js +4 -1
- package/src/webui/index.js +2 -1
package/package.json
CHANGED
|
@@ -11,11 +11,47 @@ import {
|
|
|
11
11
|
LOAD_CODE_ASSIST_HEADERS,
|
|
12
12
|
DEFAULT_PROJECT_ID
|
|
13
13
|
} from '../constants.js';
|
|
14
|
-
import { refreshAccessToken } from '../auth/oauth.js';
|
|
14
|
+
import { refreshAccessToken, parseRefreshParts, formatRefreshParts } from '../auth/oauth.js';
|
|
15
15
|
import { getAuthStatus } from '../auth/database.js';
|
|
16
16
|
import { logger } from '../utils/logger.js';
|
|
17
17
|
import { isNetworkError } from '../utils/helpers.js';
|
|
18
18
|
import { onboardUser, getDefaultTierId } from './onboarding.js';
|
|
19
|
+
import { parseTierId } from '../cloudcode/model-api.js';
|
|
20
|
+
|
|
21
|
+
// Track accounts currently fetching subscription to avoid duplicate calls
|
|
22
|
+
const subscriptionFetchInProgress = new Set();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch subscription tier and save it (blocking)
|
|
26
|
+
* Used when we have a cached project but missing subscription data
|
|
27
|
+
*
|
|
28
|
+
* @param {string} token - OAuth access token
|
|
29
|
+
* @param {Object} account - Account object
|
|
30
|
+
* @param {Function} [onSave] - Callback to save account changes
|
|
31
|
+
*/
|
|
32
|
+
async function fetchAndSaveSubscription(token, account, onSave) {
|
|
33
|
+
// Avoid duplicate fetches for the same account
|
|
34
|
+
if (subscriptionFetchInProgress.has(account.email)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
subscriptionFetchInProgress.add(account.email);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Call discoverProject just to get subscription info
|
|
41
|
+
const { subscription } = await discoverProject(token, account.projectId);
|
|
42
|
+
if (subscription && subscription.tier !== 'unknown') {
|
|
43
|
+
account.subscription = subscription;
|
|
44
|
+
if (onSave) {
|
|
45
|
+
await onSave();
|
|
46
|
+
}
|
|
47
|
+
logger.info(`[AccountManager] Updated subscription tier for ${account.email}: ${subscription.tier}`);
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
logger.debug(`[AccountManager] Subscription fetch failed for ${account.email}: ${e.message}`);
|
|
51
|
+
} finally {
|
|
52
|
+
subscriptionFetchInProgress.delete(account.email);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
19
55
|
|
|
20
56
|
/**
|
|
21
57
|
* Get OAuth token for an account
|
|
@@ -82,27 +118,92 @@ export async function getTokenForAccount(account, tokenCache, onInvalid, onSave)
|
|
|
82
118
|
|
|
83
119
|
/**
|
|
84
120
|
* Get project ID for an account
|
|
121
|
+
* Aligned with opencode-antigravity-auth: parses refresh token for stored project IDs
|
|
85
122
|
*
|
|
86
123
|
* @param {Object} account - Account object
|
|
87
124
|
* @param {string} token - OAuth access token
|
|
88
125
|
* @param {Map} projectCache - Project cache map
|
|
126
|
+
* @param {Function} [onSave] - Callback to save account changes
|
|
89
127
|
* @returns {Promise<string>} Project ID
|
|
90
128
|
*/
|
|
91
|
-
export async function getProjectForAccount(account, token, projectCache) {
|
|
129
|
+
export async function getProjectForAccount(account, token, projectCache, onSave = null) {
|
|
92
130
|
// Check cache first
|
|
93
131
|
const cached = projectCache.get(account.email);
|
|
94
132
|
if (cached) {
|
|
95
133
|
return cached;
|
|
96
134
|
}
|
|
97
135
|
|
|
98
|
-
//
|
|
136
|
+
// Parse refresh token to get stored project IDs (aligned with opencode-antigravity-auth)
|
|
137
|
+
const parts = account.refreshToken ? parseRefreshParts(account.refreshToken) : { refreshToken: null, projectId: undefined, managedProjectId: undefined };
|
|
138
|
+
|
|
139
|
+
// If we have a managedProjectId in the refresh token, use it
|
|
140
|
+
if (parts.managedProjectId) {
|
|
141
|
+
projectCache.set(account.email, parts.managedProjectId);
|
|
142
|
+
// If subscription is missing/unknown, fetch it now (blocking)
|
|
143
|
+
if (!account.subscription || account.subscription.tier === 'unknown') {
|
|
144
|
+
await fetchAndSaveSubscription(token, account, onSave);
|
|
145
|
+
}
|
|
146
|
+
return parts.managedProjectId;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Legacy: check account.projectId for backward compatibility
|
|
99
150
|
if (account.projectId) {
|
|
100
151
|
projectCache.set(account.email, account.projectId);
|
|
152
|
+
// If subscription is missing/unknown, fetch it now (blocking)
|
|
153
|
+
if (!account.subscription || account.subscription.tier === 'unknown') {
|
|
154
|
+
await fetchAndSaveSubscription(token, account, onSave);
|
|
155
|
+
}
|
|
101
156
|
return account.projectId;
|
|
102
157
|
}
|
|
103
158
|
|
|
104
|
-
// Discover project
|
|
105
|
-
|
|
159
|
+
// Discover managed project, passing projectId for metadata.duetProject
|
|
160
|
+
// Reference: opencode-antigravity-auth - discoverProject handles fallback internally
|
|
161
|
+
const { project, subscription } = await discoverProject(token, parts.projectId);
|
|
162
|
+
|
|
163
|
+
// Store managedProjectId back in refresh token (if we got a real project)
|
|
164
|
+
if (project && project !== DEFAULT_PROJECT_ID) {
|
|
165
|
+
let needsSave = false;
|
|
166
|
+
|
|
167
|
+
if (account.refreshToken) {
|
|
168
|
+
// OAuth accounts: encode in refresh token
|
|
169
|
+
account.refreshToken = formatRefreshParts({
|
|
170
|
+
refreshToken: parts.refreshToken,
|
|
171
|
+
projectId: parts.projectId,
|
|
172
|
+
managedProjectId: project,
|
|
173
|
+
});
|
|
174
|
+
needsSave = true;
|
|
175
|
+
} else if (account.source === 'database' || account.source === 'manual') {
|
|
176
|
+
// Database/manual accounts: store in projectId field
|
|
177
|
+
account.projectId = project;
|
|
178
|
+
needsSave = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Save subscription tier if discovered
|
|
182
|
+
if (subscription) {
|
|
183
|
+
account.subscription = subscription;
|
|
184
|
+
needsSave = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Trigger save to persist the updated project and subscription
|
|
188
|
+
if (needsSave && onSave) {
|
|
189
|
+
try {
|
|
190
|
+
await onSave();
|
|
191
|
+
} catch (e) {
|
|
192
|
+
logger.warn(`[AccountManager] Failed to save updated project: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else if (subscription) {
|
|
196
|
+
// Even if no project discovered, save subscription if we got it
|
|
197
|
+
account.subscription = subscription;
|
|
198
|
+
if (onSave) {
|
|
199
|
+
try {
|
|
200
|
+
await onSave();
|
|
201
|
+
} catch (e) {
|
|
202
|
+
logger.warn(`[AccountManager] Failed to save subscription: ${e.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
106
207
|
projectCache.set(account.email, project);
|
|
107
208
|
return project;
|
|
108
209
|
}
|
|
@@ -111,13 +212,27 @@ export async function getProjectForAccount(account, token, projectCache) {
|
|
|
111
212
|
* Discover project ID via Cloud Code API
|
|
112
213
|
*
|
|
113
214
|
* @param {string} token - OAuth access token
|
|
114
|
-
* @
|
|
215
|
+
* @param {string} [projectId] - Optional project ID from refresh token (for metadata.duetProject)
|
|
216
|
+
* @returns {Promise<{project: string, subscription: {tier: string, projectId: string|null, detectedAt: string}|null}>} Project and subscription info
|
|
115
217
|
*/
|
|
116
|
-
export async function discoverProject(token) {
|
|
218
|
+
export async function discoverProject(token, projectId = undefined) {
|
|
117
219
|
let lastError = null;
|
|
118
220
|
let gotSuccessfulResponse = false;
|
|
119
221
|
let loadCodeAssistData = null;
|
|
120
222
|
|
|
223
|
+
// Build metadata matching reference: only set duetProject if projectId is truthy
|
|
224
|
+
// For loadCodeAssist, reference passes projectId ?? fallbackProjectId
|
|
225
|
+
// Reference: opencode-antigravity-auth/src/plugin/project.ts buildMetadata()
|
|
226
|
+
const projectIdForLoad = projectId ?? DEFAULT_PROJECT_ID;
|
|
227
|
+
const metadata = {
|
|
228
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
229
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
230
|
+
pluginType: 'GEMINI'
|
|
231
|
+
};
|
|
232
|
+
if (projectIdForLoad) {
|
|
233
|
+
metadata.duetProject = projectIdForLoad;
|
|
234
|
+
}
|
|
235
|
+
|
|
121
236
|
for (const endpoint of LOAD_CODE_ASSIST_ENDPOINTS) {
|
|
122
237
|
try {
|
|
123
238
|
const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
@@ -127,14 +242,7 @@ export async function discoverProject(token) {
|
|
|
127
242
|
'Content-Type': 'application/json',
|
|
128
243
|
...LOAD_CODE_ASSIST_HEADERS
|
|
129
244
|
},
|
|
130
|
-
body: JSON.stringify({
|
|
131
|
-
metadata: {
|
|
132
|
-
ideType: 'IDE_UNSPECIFIED',
|
|
133
|
-
platform: 'PLATFORM_UNSPECIFIED',
|
|
134
|
-
pluginType: 'GEMINI',
|
|
135
|
-
duetProject: DEFAULT_PROJECT_ID
|
|
136
|
-
}
|
|
137
|
-
})
|
|
245
|
+
body: JSON.stringify({ metadata })
|
|
138
246
|
});
|
|
139
247
|
|
|
140
248
|
if (!response.ok) {
|
|
@@ -150,13 +258,16 @@ export async function discoverProject(token) {
|
|
|
150
258
|
|
|
151
259
|
logger.debug(`[AccountManager] loadCodeAssist response from ${endpoint}:`, JSON.stringify(data));
|
|
152
260
|
|
|
261
|
+
// Extract subscription tier from response
|
|
262
|
+
const subscription = extractSubscriptionFromResponse(data);
|
|
263
|
+
|
|
153
264
|
if (typeof data.cloudaicompanionProject === 'string') {
|
|
154
265
|
logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject}`);
|
|
155
|
-
return data.cloudaicompanionProject;
|
|
266
|
+
return { project: data.cloudaicompanionProject, subscription };
|
|
156
267
|
}
|
|
157
268
|
if (data.cloudaicompanionProject?.id) {
|
|
158
269
|
logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject.id}`);
|
|
159
|
-
return data.cloudaicompanionProject.id;
|
|
270
|
+
return { project: data.cloudaicompanionProject.id, subscription };
|
|
160
271
|
}
|
|
161
272
|
|
|
162
273
|
// No project found - log tier data and try to onboard the user
|
|
@@ -189,29 +300,66 @@ export async function discoverProject(token) {
|
|
|
189
300
|
tierId = tierId || 'free-tier';
|
|
190
301
|
logger.info(`[AccountManager] Onboarding user with tier: ${tierId} (source: ${tierSource})`);
|
|
191
302
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// For non-free tiers, pass DEFAULT_PROJECT_ID as the GCP project
|
|
196
|
-
// The API requires a project for paid tier onboarding
|
|
303
|
+
// Pass projectId for metadata.duetProject (without fallback, matching reference)
|
|
304
|
+
// Reference: opencode-antigravity-auth passes parts.projectId (not fallback) to onboardManagedProject
|
|
197
305
|
const onboardedProject = await onboardUser(
|
|
198
306
|
token,
|
|
199
307
|
tierId,
|
|
200
|
-
|
|
308
|
+
projectId // Original projectId without fallback
|
|
201
309
|
);
|
|
202
310
|
if (onboardedProject) {
|
|
203
311
|
logger.success(`[AccountManager] Successfully onboarded, project: ${onboardedProject}`);
|
|
204
|
-
|
|
312
|
+
const subscription = extractSubscriptionFromResponse(loadCodeAssistData);
|
|
313
|
+
return { project: onboardedProject, subscription };
|
|
205
314
|
}
|
|
206
315
|
|
|
207
|
-
logger.warn(`[AccountManager] Onboarding failed
|
|
316
|
+
logger.warn(`[AccountManager] Onboarding failed - account may not work correctly`);
|
|
208
317
|
}
|
|
209
318
|
|
|
210
319
|
// Only warn if all endpoints failed with errors (not just missing project)
|
|
211
320
|
if (!gotSuccessfulResponse) {
|
|
212
321
|
logger.warn(`[AccountManager] loadCodeAssist failed for all endpoints: ${lastError}`);
|
|
213
322
|
}
|
|
214
|
-
|
|
323
|
+
|
|
324
|
+
// Fallback: use projectId if available, otherwise use default
|
|
325
|
+
// Reference: opencode-antigravity-auth/src/plugin/project.ts
|
|
326
|
+
if (projectId) {
|
|
327
|
+
return { project: projectId, subscription: null };
|
|
328
|
+
}
|
|
329
|
+
return { project: DEFAULT_PROJECT_ID, subscription: null };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Extract subscription tier from loadCodeAssist response
|
|
334
|
+
*
|
|
335
|
+
* @param {Object} data - loadCodeAssist response data
|
|
336
|
+
* @returns {{tier: string, projectId: string|null, detectedAt: string}|null} Subscription info
|
|
337
|
+
*/
|
|
338
|
+
function extractSubscriptionFromResponse(data) {
|
|
339
|
+
if (!data) return null;
|
|
340
|
+
|
|
341
|
+
// Priority: paidTier > currentTier (consistent with model-api.js)
|
|
342
|
+
let tier = 'free';
|
|
343
|
+
let cloudProject = null;
|
|
344
|
+
|
|
345
|
+
if (data.paidTier?.id) {
|
|
346
|
+
tier = parseTierId(data.paidTier.id);
|
|
347
|
+
} else if (data.currentTier?.id) {
|
|
348
|
+
tier = parseTierId(data.currentTier.id);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Get project ID
|
|
352
|
+
if (typeof data.cloudaicompanionProject === 'string') {
|
|
353
|
+
cloudProject = data.cloudaicompanionProject;
|
|
354
|
+
} else if (data.cloudaicompanionProject?.id) {
|
|
355
|
+
cloudProject = data.cloudaicompanionProject.id;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
tier,
|
|
360
|
+
projectId: cloudProject,
|
|
361
|
+
detectedAt: new Date().toISOString()
|
|
362
|
+
};
|
|
215
363
|
}
|
|
216
364
|
|
|
217
365
|
/**
|
|
@@ -297,7 +297,8 @@ export class AccountManager {
|
|
|
297
297
|
* @returns {Promise<string>} Project ID
|
|
298
298
|
*/
|
|
299
299
|
async getProjectForAccount(account, token) {
|
|
300
|
-
|
|
300
|
+
// Pass onSave callback to persist managedProjectId in refresh token
|
|
301
|
+
return fetchProject(account, token, this.#projectCache, () => this.saveToDisk());
|
|
301
302
|
}
|
|
302
303
|
|
|
303
304
|
/**
|
|
@@ -43,7 +43,7 @@ export function getDefaultTierId(allowedTiers) {
|
|
|
43
43
|
* @param {number} [delayMs=5000] - Delay between polling attempts
|
|
44
44
|
* @returns {Promise<string|null>} Managed project ID or null if failed
|
|
45
45
|
*/
|
|
46
|
-
export async function onboardUser(token, tierId, projectId =
|
|
46
|
+
export async function onboardUser(token, tierId, projectId = undefined, maxAttempts = 10, delayMs = 5000) {
|
|
47
47
|
const metadata = {
|
|
48
48
|
ideType: 'IDE_UNSPECIFIED',
|
|
49
49
|
platform: 'PLATFORM_UNSPECIFIED',
|
|
@@ -58,16 +58,11 @@ export async function onboardUser(token, tierId, projectId = null, maxAttempts =
|
|
|
58
58
|
tierId,
|
|
59
59
|
metadata
|
|
60
60
|
};
|
|
61
|
+
// Note: Do NOT add cloudaicompanionProject to requestBody
|
|
62
|
+
// Reference implementation only sets metadata.duetProject, not the body field
|
|
63
|
+
// Adding cloudaicompanionProject causes 400 errors for auto-provisioned tiers (g1-pro, g1-ultra)
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
const isFree = tierId.toLowerCase().includes('free');
|
|
64
|
-
|
|
65
|
-
// Non-free tiers require a cloudaicompanionProject
|
|
66
|
-
if (!isFree && projectId) {
|
|
67
|
-
requestBody.cloudaicompanionProject = projectId;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}, isFree: ${isFree}`);
|
|
65
|
+
logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}`);
|
|
71
66
|
|
|
72
67
|
for (const endpoint of ONBOARD_USER_ENDPOINTS) {
|
|
73
68
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -10,20 +10,14 @@ import { HybridStrategy } from './hybrid-strategy.js';
|
|
|
10
10
|
import { logger } from '../../utils/logger.js';
|
|
11
11
|
import {
|
|
12
12
|
SELECTION_STRATEGIES,
|
|
13
|
-
DEFAULT_SELECTION_STRATEGY
|
|
13
|
+
DEFAULT_SELECTION_STRATEGY,
|
|
14
|
+
STRATEGY_LABELS
|
|
14
15
|
} from '../../constants.js';
|
|
15
16
|
|
|
16
17
|
// Re-export strategy constants for convenience
|
|
17
18
|
export const STRATEGY_NAMES = SELECTION_STRATEGIES;
|
|
18
19
|
export const DEFAULT_STRATEGY = DEFAULT_SELECTION_STRATEGY;
|
|
19
20
|
|
|
20
|
-
// Strategy display labels
|
|
21
|
-
export const STRATEGY_LABELS = {
|
|
22
|
-
'sticky': 'Sticky (Cache Optimized)',
|
|
23
|
-
'round-robin': 'Round Robin (Load Balanced)',
|
|
24
|
-
'hybrid': 'Hybrid (Smart Distribution)'
|
|
25
|
-
};
|
|
26
|
-
|
|
27
21
|
/**
|
|
28
22
|
* Create a strategy instance
|
|
29
23
|
* @param {string} strategyName - Name of the strategy ('sticky', 'round-robin', 'hybrid')
|
package/src/auth/oauth.js
CHANGED
|
@@ -17,6 +17,34 @@ import {
|
|
|
17
17
|
import { logger } from '../utils/logger.js';
|
|
18
18
|
import { onboardUser, getDefaultTierId } from '../account-manager/onboarding.js';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Parse refresh token parts (aligned with opencode-antigravity-auth)
|
|
22
|
+
* Format: refreshToken|projectId|managedProjectId
|
|
23
|
+
*
|
|
24
|
+
* @param {string} refresh - Composite refresh token string
|
|
25
|
+
* @returns {{refreshToken: string, projectId: string|undefined, managedProjectId: string|undefined}}
|
|
26
|
+
*/
|
|
27
|
+
export function parseRefreshParts(refresh) {
|
|
28
|
+
const [refreshToken = '', projectId = '', managedProjectId = ''] = (refresh ?? '').split('|');
|
|
29
|
+
return {
|
|
30
|
+
refreshToken,
|
|
31
|
+
projectId: projectId || undefined,
|
|
32
|
+
managedProjectId: managedProjectId || undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format refresh token parts back into composite string
|
|
38
|
+
*
|
|
39
|
+
* @param {{refreshToken: string, projectId?: string|undefined, managedProjectId?: string|undefined}} parts
|
|
40
|
+
* @returns {string} Composite refresh token
|
|
41
|
+
*/
|
|
42
|
+
export function formatRefreshParts(parts) {
|
|
43
|
+
const projectSegment = parts.projectId ?? '';
|
|
44
|
+
const base = `${parts.refreshToken}|${projectSegment}`;
|
|
45
|
+
return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
/**
|
|
21
49
|
* Generate PKCE code verifier and challenge
|
|
22
50
|
*/
|
|
@@ -267,11 +295,15 @@ export async function exchangeCode(code, verifier) {
|
|
|
267
295
|
|
|
268
296
|
/**
|
|
269
297
|
* Refresh access token using refresh token
|
|
298
|
+
* Handles composite refresh tokens (refreshToken|projectId|managedProjectId)
|
|
270
299
|
*
|
|
271
|
-
* @param {string}
|
|
300
|
+
* @param {string} compositeRefresh - OAuth refresh token (may be composite)
|
|
272
301
|
* @returns {Promise<{accessToken: string, expiresIn: number}>} New access token
|
|
273
302
|
*/
|
|
274
|
-
export async function refreshAccessToken(
|
|
303
|
+
export async function refreshAccessToken(compositeRefresh) {
|
|
304
|
+
// Parse the composite refresh token to extract the actual OAuth token
|
|
305
|
+
const parts = parseRefreshParts(compositeRefresh);
|
|
306
|
+
|
|
275
307
|
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
276
308
|
method: 'POST',
|
|
277
309
|
headers: {
|
|
@@ -280,7 +312,7 @@ export async function refreshAccessToken(refreshToken) {
|
|
|
280
312
|
body: new URLSearchParams({
|
|
281
313
|
client_id: OAUTH_CONFIG.clientId,
|
|
282
314
|
client_secret: OAUTH_CONFIG.clientSecret,
|
|
283
|
-
refresh_token: refreshToken,
|
|
315
|
+
refresh_token: parts.refreshToken, // Use the actual OAuth token
|
|
284
316
|
grant_type: 'refresh_token'
|
|
285
317
|
})
|
|
286
318
|
});
|
|
@@ -408,6 +440,8 @@ export async function completeOAuthFlow(code, verifier) {
|
|
|
408
440
|
}
|
|
409
441
|
|
|
410
442
|
export default {
|
|
443
|
+
parseRefreshParts,
|
|
444
|
+
formatRefreshParts,
|
|
411
445
|
getAuthorizationUrl,
|
|
412
446
|
extractCodeFromInput,
|
|
413
447
|
startCallbackServer,
|
package/src/cli/accounts.js
CHANGED
|
@@ -95,7 +95,7 @@ function openBrowser(url) {
|
|
|
95
95
|
args = [url];
|
|
96
96
|
} else if (platform === 'win32') {
|
|
97
97
|
command = 'cmd';
|
|
98
|
-
args = ['/c', 'start', '', url];
|
|
98
|
+
args = ['/c', 'start', '', url.replace(/&/g, '^&')];
|
|
99
99
|
} else {
|
|
100
100
|
command = 'xdg-open';
|
|
101
101
|
args = [url];
|
|
@@ -210,20 +210,18 @@ async function addAccount(existingAccounts) {
|
|
|
210
210
|
if (existing) {
|
|
211
211
|
console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
|
|
212
212
|
existing.refreshToken = result.refreshToken;
|
|
213
|
-
|
|
213
|
+
// Note: projectId will be discovered and stored in refresh token on first use
|
|
214
214
|
existing.addedAt = new Date().toISOString();
|
|
215
215
|
return null; // Don't add duplicate
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
console.log(`\n✓ Successfully authenticated: ${result.email}`);
|
|
219
|
-
|
|
220
|
-
console.log(` Project ID: ${result.projectId}`);
|
|
221
|
-
}
|
|
219
|
+
console.log(' Project will be discovered on first API request.');
|
|
222
220
|
|
|
223
221
|
return {
|
|
224
222
|
email: result.email,
|
|
225
223
|
refreshToken: result.refreshToken,
|
|
226
|
-
|
|
224
|
+
// Note: projectId stored in refresh token, not as separate field
|
|
227
225
|
addedAt: new Date().toISOString(),
|
|
228
226
|
modelRateLimits: {}
|
|
229
227
|
};
|
|
@@ -267,20 +265,18 @@ async function addAccountNoBrowser(existingAccounts, rl) {
|
|
|
267
265
|
if (existing) {
|
|
268
266
|
console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
|
|
269
267
|
existing.refreshToken = result.refreshToken;
|
|
270
|
-
|
|
268
|
+
// Note: projectId will be discovered and stored in refresh token on first use
|
|
271
269
|
existing.addedAt = new Date().toISOString();
|
|
272
270
|
return null; // Don't add duplicate
|
|
273
271
|
}
|
|
274
272
|
|
|
275
273
|
console.log(`\n✓ Successfully authenticated: ${result.email}`);
|
|
276
|
-
|
|
277
|
-
console.log(` Project ID: ${result.projectId}`);
|
|
278
|
-
}
|
|
274
|
+
console.log(' Project will be discovered on first API request.');
|
|
279
275
|
|
|
280
276
|
return {
|
|
281
277
|
email: result.email,
|
|
282
278
|
refreshToken: result.refreshToken,
|
|
283
|
-
|
|
279
|
+
// Note: projectId stored in refresh token, not as separate field
|
|
284
280
|
addedAt: new Date().toISOString(),
|
|
285
281
|
modelRateLimits: {}
|
|
286
282
|
};
|
|
@@ -128,7 +128,7 @@ export async function getModelQuotas(token, projectId = null) {
|
|
|
128
128
|
* @param {string} tierId - The tier ID from the API
|
|
129
129
|
* @returns {'free' | 'pro' | 'ultra' | 'unknown'} The subscription tier
|
|
130
130
|
*/
|
|
131
|
-
function parseTierId(tierId) {
|
|
131
|
+
export function parseTierId(tierId) {
|
|
132
132
|
if (!tierId) return 'unknown';
|
|
133
133
|
const lower = tierId.toLowerCase();
|
|
134
134
|
|
package/src/constants.js
CHANGED
|
@@ -121,6 +121,13 @@ export const MIN_SIGNATURE_LENGTH = 50; // Minimum valid thinking signature leng
|
|
|
121
121
|
export const SELECTION_STRATEGIES = ['sticky', 'round-robin', 'hybrid'];
|
|
122
122
|
export const DEFAULT_SELECTION_STRATEGY = 'hybrid';
|
|
123
123
|
|
|
124
|
+
// Strategy display labels
|
|
125
|
+
export const STRATEGY_LABELS = {
|
|
126
|
+
'sticky': 'Sticky (Cache Optimized)',
|
|
127
|
+
'round-robin': 'Round Robin (Load Balanced)',
|
|
128
|
+
'hybrid': 'Hybrid (Smart Distribution)'
|
|
129
|
+
};
|
|
130
|
+
|
|
124
131
|
// Gemini-specific limits
|
|
125
132
|
export const GEMINI_MAX_OUTPUT_TOKENS = 16384;
|
|
126
133
|
|
|
@@ -263,6 +270,7 @@ export default {
|
|
|
263
270
|
isThinkingModel,
|
|
264
271
|
OAUTH_CONFIG,
|
|
265
272
|
OAUTH_REDIRECT_URI,
|
|
273
|
+
STRATEGY_LABELS,
|
|
266
274
|
MODEL_FALLBACK_MAP,
|
|
267
275
|
TEST_MODELS,
|
|
268
276
|
DEFAULT_PRESETS,
|
package/src/index.js
CHANGED
|
@@ -60,8 +60,11 @@ const server = app.listen(PORT, () => {
|
|
|
60
60
|
const align4 = (text) => text + ' '.repeat(Math.max(0, 58 - text.length));
|
|
61
61
|
|
|
62
62
|
// Build Control section dynamically
|
|
63
|
+
const strategyOptions = `(${STRATEGY_NAMES.join('/')})`;
|
|
64
|
+
const strategyLine2 = ' ' + strategyOptions;
|
|
63
65
|
let controlSection = '║ Control: ║\n';
|
|
64
|
-
controlSection += '║ --strategy=<s> Set selection strategy
|
|
66
|
+
controlSection += '║ --strategy=<s> Set account selection strategy ║\n';
|
|
67
|
+
controlSection += `${border} ${align(strategyLine2)}${border}\n`;
|
|
65
68
|
if (!isDebug) {
|
|
66
69
|
controlSection += '║ --debug Enable debug logging ║\n';
|
|
67
70
|
}
|
package/src/webui/index.js
CHANGED
|
@@ -668,10 +668,11 @@ export function mountWebUI(app, dirname, accountManager) {
|
|
|
668
668
|
const accountData = await completeOAuthFlow(code, verifier);
|
|
669
669
|
|
|
670
670
|
// Add or update the account
|
|
671
|
+
// Note: Don't set projectId here - it will be discovered and stored
|
|
672
|
+
// in the refresh token via getProjectForAccount() on first use
|
|
671
673
|
await addAccount({
|
|
672
674
|
email: accountData.email,
|
|
673
675
|
refreshToken: accountData.refreshToken,
|
|
674
|
-
projectId: accountData.projectId,
|
|
675
676
|
source: 'oauth'
|
|
676
677
|
});
|
|
677
678
|
|