Selective fixes from PR #35: Model-specific rate limits & robustness improvements (#37)

* feat: apply local user changes and fixes

* ;D

* Implement OpenAI support, model-specific rate limiting, and robustness fixes

* docs: update pr title

* feat: ensure unique openai models endpoint

* fix: startup banner alignment and removed duplicates

* feat: add model fallback system with --fallback flag

* fix: accounts cli hanging after completion

* feat: add exit option to accounts cli menu

* fix: remove circular dependency warning for fallback flag

* feat: show active modes in banner and hide their flags

* Remove OpenAI compatibility and fallback features from PR #35

Cherry-picked selective fixes from PR #35 while removing:
- OpenAI-compatible API endpoints (/openai/v1/*)
- Model fallback system (fallback-config.js)
- Thinking block skip for Gemini models
- Unnecessary files (pullrequest.md, test-fix.js, test-openai.js)

Retained improvements:
- Network error handling with retry logic
- Model-specific rate limiting
- Enhanced health check with quota info
- CLI fixes (exit option, process.exit)
- Startup banner alignment (debug mode only)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* banner alignment fix

* Refactor: Model-specific rate limits and cleanup deprecated code

- Remove global rate limit fields (isRateLimited, rateLimitResetTime) in favor of model-specific limits (modelRateLimits[modelId])
- Remove deprecated wrapper functions (is429Error, isAuthInvalidError) from handlers
- Filter fetchAvailableModels to only return Claude and Gemini models
- Fix getCurrentStickyAccount() to pass model param after waiting
- Update /account-limits endpoint to show model-specific limits
- Remove multi-account OAuth flow to avoid state mismatch errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: show (x/y) limited status in account-limits table

- Status is now "ok" only when all models are available
- Shows "(x/y) limited" when x out of y models are exhausted
- Provides better visibility into partial rate limiting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: update CLAUDE.md with model-specific rate limiting

- Document modelRateLimits[modelId] for per-model rate tracking
- Add isNetworkError() helper to utilities section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: M1noa <minoa@minoa.cat>
Co-authored-by: Minoa <altgithub@minoa.cat>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Badri Narayanan S
2026-01-03 15:33:49 +05:30
committed by GitHub
parent 2d05dd5b62
commit 9c4a712a9a
15 changed files with 474 additions and 194 deletions

View File

@@ -81,18 +81,20 @@ export class AccountManager {
/**
* Check if all accounts are rate-limited
* @param {string} [modelId] - Optional model ID
* @returns {boolean} True if all accounts are rate-limited
*/
isAllRateLimited() {
return checkAllRateLimited(this.#accounts);
isAllRateLimited(modelId = null) {
return checkAllRateLimited(this.#accounts, modelId);
}
/**
* Get list of available (non-rate-limited, non-invalid) accounts
* @param {string} [modelId] - Optional model ID
* @returns {Array<Object>} Array of available account objects
*/
getAvailableAccounts() {
return getAvailable(this.#accounts);
getAvailableAccounts(modelId = null) {
return getAvailable(this.#accounts, modelId);
}
/**
@@ -127,10 +129,11 @@ export class AccountManager {
/**
* Pick the next available account (fallback when current is unavailable).
* Sets activeIndex to the selected account's index.
* @param {string} [modelId] - Optional model ID
* @returns {Object|null} The next available account or null if none available
*/
pickNext() {
const { account, newIndex } = selectNext(this.#accounts, this.#currentIndex, () => this.saveToDisk());
pickNext(modelId = null) {
const { account, newIndex } = selectNext(this.#accounts, this.#currentIndex, () => this.saveToDisk(), modelId);
this.#currentIndex = newIndex;
return account;
}
@@ -138,10 +141,11 @@ export class AccountManager {
/**
* Get the current account without advancing the index (sticky selection).
* Used for cache continuity - sticks to the same account until rate-limited.
* @param {string} [modelId] - Optional model ID
* @returns {Object|null} The current account or null if unavailable/rate-limited
*/
getCurrentStickyAccount() {
const { account, newIndex } = getSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk());
getCurrentStickyAccount(modelId = null) {
const { account, newIndex } = getSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk(), modelId);
this.#currentIndex = newIndex;
return account;
}
@@ -149,10 +153,11 @@ export class AccountManager {
/**
* Check if we should wait for the current account's rate limit to reset.
* Used for sticky account selection - wait if rate limit is short (≤ threshold).
* @param {string} [modelId] - Optional model ID
* @returns {{shouldWait: boolean, waitMs: number, account: Object|null}}
*/
shouldWaitForCurrentAccount() {
return shouldWait(this.#accounts, this.#currentIndex);
shouldWaitForCurrentAccount(modelId = null) {
return shouldWait(this.#accounts, this.#currentIndex, modelId);
}
/**
@@ -160,10 +165,11 @@ export class AccountManager {
* Prefers the current account for cache continuity, only switches when:
* - Current account is rate-limited for > 2 minutes
* - Current account is invalid
* @param {string} [modelId] - Optional model ID
* @returns {{account: Object|null, waitMs: number}} Account to use and optional wait time
*/
pickStickyAccount() {
const { account, waitMs, newIndex } = selectSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk());
pickStickyAccount(modelId = null) {
const { account, waitMs, newIndex } = selectSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk(), modelId);
this.#currentIndex = newIndex;
return { account, waitMs };
}
@@ -172,9 +178,10 @@ export class AccountManager {
* Mark an account as rate-limited
* @param {string} email - Email of the account to mark
* @param {number|null} resetMs - Time in ms until rate limit resets (optional)
* @param {string} [modelId] - Optional model ID to mark specific limit
*/
markRateLimited(email, resetMs = null) {
markLimited(this.#accounts, email, resetMs, this.#settings);
markRateLimited(email, resetMs = null, modelId = null) {
markLimited(this.#accounts, email, resetMs, this.#settings, modelId);
this.saveToDisk();
}
@@ -190,10 +197,11 @@ export class AccountManager {
/**
* Get the minimum wait time until any account becomes available
* @param {string} [modelId] - Optional model ID
* @returns {number} Wait time in milliseconds
*/
getMinWaitTimeMs() {
return getMinWait(this.#accounts);
getMinWaitTimeMs(modelId = null) {
return getMinWait(this.#accounts, modelId);
}
/**
@@ -251,9 +259,16 @@ export class AccountManager {
*/
getStatus() {
const available = this.getAvailableAccounts();
const rateLimited = this.#accounts.filter(a => a.isRateLimited);
const invalid = this.getInvalidAccounts();
// Count accounts that have any active model-specific rate limits
const rateLimited = this.#accounts.filter(a => {
if (!a.modelRateLimits) return false;
return Object.values(a.modelRateLimits).some(
limit => limit.isRateLimited && limit.resetTime > Date.now()
);
});
return {
total: this.#accounts.length,
available: available.length,
@@ -263,8 +278,7 @@ export class AccountManager {
accounts: this.#accounts.map(a => ({
email: a.email,
source: a.source,
isRateLimited: a.isRateLimited,
rateLimitResetTime: a.rateLimitResetTime,
modelRateLimits: a.modelRateLimits || {},
isInvalid: a.isInvalid || false,
invalidReason: a.invalidReason || null,
lastUsed: a.lastUsed