opencode-studio-server 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +109 -40
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -204,17 +204,59 @@ function processLogLine(line) {
204
204
 
205
205
  if (isQuotaError) {
206
206
  console.log(`[LogWatcher] Detected quota exhaustion for ${namespace}`);
207
- // Mark exhausted for today
208
- metadata._quota[namespace].exhausted = true;
209
- metadata._quota[namespace].exhaustedDate = today;
210
207
 
211
- // Adaptive limit learning: if we hit a limit, maybe that's the ceiling?
212
- // Only update if we have meaningful usage (>5) to avoid false positives on glitches
213
- const currentUsage = metadata._quota[namespace][today] || 0;
208
+ // Reload metadata to ensure freshness
209
+ const currentMeta = loadPoolMetadata();
210
+ if (!currentMeta._quota) currentMeta._quota = {};
211
+ if (!currentMeta._quota[namespace]) currentMeta._quota[namespace] = {};
212
+
213
+ // Debounce check
214
+ const lastRotation = currentMeta._quota[namespace].lastRotation || 0;
215
+ if (Date.now() - lastRotation < 10000) {
216
+ console.log(`[LogWatcher] Ignoring 429 (rotation debounce active)`);
217
+ return;
218
+ }
219
+
220
+ const studio = loadStudioConfig();
221
+ const activeAccount = studio.activeProfiles?.[provider];
222
+
223
+ let rotated = false;
224
+
225
+ if (activeAccount) {
226
+ console.log(`[LogWatcher] Auto-rotating due to rate limit on ${activeAccount}`);
227
+
228
+ if (!currentMeta[namespace]) currentMeta[namespace] = {};
229
+ if (!currentMeta[namespace][activeAccount]) currentMeta[namespace][activeAccount] = {};
230
+
231
+ // Mark cooldown (1 hour)
232
+ currentMeta[namespace][activeAccount].cooldownUntil = Date.now() + 3600000;
233
+ currentMeta[namespace][activeAccount].lastCooldownReason = 'auto_429';
234
+
235
+ savePoolMetadata(currentMeta);
236
+
237
+ // Attempt rotation
238
+ const result = rotateAccount(provider, 'auto_rotation_429');
239
+ if (result.success) {
240
+ console.log(`[LogWatcher] Successfully rotated to ${result.newAccount}`);
241
+ rotated = true;
242
+ } else {
243
+ console.log(`[LogWatcher] Auto-rotation failed: ${result.error}`);
244
+ }
245
+ }
246
+
247
+ if (rotated) return;
248
+
249
+ // Fallback: Mark namespace exhausted
250
+ currentMeta._quota[namespace].exhausted = true;
251
+ currentMeta._quota[namespace].exhaustedDate = today;
252
+
253
+ const currentUsage = currentMeta._quota[namespace][today] || 0;
214
254
  if (currentUsage > 5) {
215
- // Update daily limit to current usage (maybe round up)
216
- metadata._quota[namespace].dailyLimit = currentUsage;
255
+ currentMeta._quota[namespace].dailyLimit = currentUsage;
217
256
  }
257
+
258
+ savePoolMetadata(currentMeta);
259
+ return;
218
260
  }
219
261
  }
220
262
 
@@ -1922,67 +1964,56 @@ function getPoolQuota(provider, pool) {
1922
1964
  };
1923
1965
  }
1924
1966
 
1925
- // GET /api/auth/pool - Get account pool for Google (or specified provider)
1926
- app.get('/api/auth/pool', (req, res) => {
1927
- const provider = req.query.provider || 'google';
1928
- syncAntigravityPool();
1967
+ function rotateAccount(provider, reason = 'manual_rotation') {
1929
1968
  const pool = buildAccountPool(provider);
1930
- const quota = getPoolQuota(provider, pool);
1931
- res.json({ pool, quota });
1932
- });
1933
1969
 
1934
- // POST /api/auth/pool/rotate - Rotate to next available account
1935
- app.post('/api/auth/pool/rotate', (req, res) => {
1936
- const provider = req.body.provider || 'google';
1937
- const pool = buildAccountPool(provider);
1938
-
1939
1970
  if (pool.accounts.length === 0) {
1940
- return res.status(400).json({ error: 'No accounts in pool' });
1971
+ return { success: false, error: 'No accounts in pool' };
1941
1972
  }
1942
-
1973
+
1943
1974
  const now = Date.now();
1944
1975
  const available = pool.accounts.filter(acc =>
1945
1976
  acc.status === 'ready' || (acc.status === 'cooldown' && acc.cooldownUntil && acc.cooldownUntil < now)
1946
1977
  );
1947
-
1978
+
1948
1979
  if (available.length === 0) {
1949
- return res.status(400).json({ error: 'No available accounts (all in cooldown or expired)' });
1980
+ return { success: false, error: 'No available accounts (all in cooldown or expired)' };
1950
1981
  }
1951
-
1982
+
1952
1983
  // Pick least recently used
1953
1984
  const next = available.sort((a, b) => a.lastUsed - b.lastUsed)[0];
1954
1985
  const previousActive = pool.activeAccount;
1955
-
1986
+
1956
1987
  // Activate the new account
1957
1988
  const activePlugin = getActiveGooglePlugin();
1958
1989
  const namespace = provider === 'google'
1959
1990
  ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
1960
1991
  : provider;
1961
-
1992
+
1962
1993
  const profilePath = path.join(AUTH_PROFILES_DIR, namespace, `${next.name}.json`);
1963
1994
  if (!fs.existsSync(profilePath)) {
1964
- return res.status(404).json({ error: 'Profile file not found' });
1995
+ return { success: false, error: 'Profile file not found' };
1965
1996
  }
1966
-
1997
+
1967
1998
  const profileData = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
1968
-
1999
+
1969
2000
  // Update auth.json
1970
2001
  const authCfg = loadAuthConfig() || {};
1971
2002
  authCfg[provider] = profileData;
1972
2003
  if (provider === 'google') {
1973
2004
  authCfg[namespace] = profileData;
1974
2005
  }
1975
-
2006
+
1976
2007
  const cp = getConfigPath();
1977
2008
  const ap = path.join(path.dirname(cp), 'auth.json');
1978
2009
  atomicWriteFileSync(ap, JSON.stringify(authCfg, null, 2));
1979
-
2010
+
1980
2011
  // Update studio config
1981
2012
  const studio = loadStudioConfig();
1982
2013
  if (!studio.activeProfiles) studio.activeProfiles = {};
1983
2014
  studio.activeProfiles[provider] = next.name;
1984
2015
  saveStudioConfig(studio);
1985
-
2016
+
1986
2017
  // Update metadata
1987
2018
  const metadata = loadPoolMetadata();
1988
2019
  if (!metadata[namespace]) metadata[namespace] = {};
@@ -1992,19 +2023,46 @@ app.post('/api/auth/pool/rotate', (req, res) => {
1992
2023
  usageCount: (metadata[namespace][next.name]?.usageCount || 0) + 1
1993
2024
  };
1994
2025
 
2026
+ // Unmark exhaustion if we successfully rotated
2027
+ if (metadata._quota?.[namespace]?.exhausted) {
2028
+ delete metadata._quota[namespace].exhausted;
2029
+ }
2030
+
1995
2031
  if (!metadata._quota) metadata._quota = {};
1996
2032
  if (!metadata._quota[namespace]) metadata._quota[namespace] = {};
1997
2033
  const today = new Date().toISOString().split('T')[0];
1998
2034
  metadata._quota[namespace][today] = (metadata._quota[namespace][today] || 0) + 1;
2035
+ metadata._quota[namespace].lastRotation = now;
1999
2036
 
2000
2037
  savePoolMetadata(metadata);
2001
-
2002
- res.json({
2038
+
2039
+ return {
2003
2040
  success: true,
2004
2041
  previousAccount: previousActive,
2005
2042
  newAccount: next.name,
2006
- reason: 'manual_rotation'
2007
- });
2043
+ reason: reason
2044
+ };
2045
+ }
2046
+
2047
+ // GET /api/auth/pool - Get account pool for Google (or specified provider)
2048
+ app.get('/api/auth/pool', (req, res) => {
2049
+ const provider = req.query.provider || 'google';
2050
+ syncAntigravityPool();
2051
+ const pool = buildAccountPool(provider);
2052
+ const quota = getPoolQuota(provider, pool);
2053
+ res.json({ pool, quota });
2054
+ });
2055
+
2056
+ // POST /api/auth/pool/rotate - Rotate to next available account
2057
+ app.post('/api/auth/pool/rotate', (req, res) => {
2058
+ const provider = req.body.provider || 'google';
2059
+ const result = rotateAccount(provider, 'manual_rotation');
2060
+
2061
+ if (!result.success) {
2062
+ return res.status(400).json(result);
2063
+ }
2064
+
2065
+ res.json(result);
2008
2066
  });
2009
2067
 
2010
2068
  // POST /api/auth/pool/limit - Set daily quota limit
@@ -2759,7 +2817,18 @@ app.post('/api/presets/:id/apply', (req, res) => {
2759
2817
  });
2760
2818
 
2761
2819
  // Start watcher on server start
2762
- setupLogWatcher();
2763
- importExistingAuth();
2820
+ if (require.main === module) {
2821
+ setupLogWatcher();
2822
+ importExistingAuth();
2823
+ app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
2824
+ }
2764
2825
 
2765
- app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
2826
+ module.exports = {
2827
+ rotateAccount,
2828
+ processLogLine,
2829
+ loadPoolMetadata,
2830
+ savePoolMetadata,
2831
+ loadStudioConfig,
2832
+ saveStudioConfig,
2833
+ buildAccountPool
2834
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {