feat: Implement provider groupings with account calculator and compre… (#113)
* feat: Implement provider groupings with account calculator and comprehensive fixes - Add provider detection utilities (detectProvider, PROVIDER_REGISTRY) for Claude/Gemini/Others categorization - Implement grouping logic (groupModelsByProvider, calculateProviderStats, calculateAccountStats) with health status calculation - Create useProviderGrouping hook for collapsible state management and persistence - Add ProviderGroup component with health indicators, progress bars, and model visibility integration - Enhance CloudAccountList with account-level calculator showing overall percentage and earliest reset time - Implement grid layout selector (auto/2-col/3-col/list) with config persistence and tooltips - Add global quota aggregate bar across all accounts in top statistics - Fix model visibility settings regression by accessing account.quota.models instead of account.quota - Show 'Rate Limited' instead of '0%' for exhausted quotas with consistent getQuotaLabel function - Add comprehensive i18n translations for layout selector and rate limited labels (en, zh-CN, ru) - Disable batch operations when provider groupings are enabled to prevent conflicts - Include unit tests for provider detection logic and grouping functionality Addresses critical issues: model list regression, rate limited display, layout control, global quota visibility, and consistent quota labeling across all components. * fix: resolve ESLint no-ex-assign violations in proxy service - Fix exception parameter reassignment in catch blocks - Rename catch parameters to avoid no-ex-assign rule violations - Maintain same error handling logic while complying with linting rules * fix: add missing Models tab to settings page - Add Models tab between General and Proxy tabs - Import and integrate ModelVisibilitySettings component - Fix critical regression where model visibility settings were inaccessible * fix: add missing React hook imports in settings route * test: remove obsolete tests for removed controller APIs --------- Co-authored-by: Draculabo <[email protected]>
This commit is contained in:
1 parent
118fad567e
commit
1e59a1c901
12 files changed
+1237
-97
No files matched your search
@@ -119,8 +119,8 @@ export class ProxyService {
|
||||
);
|
||||
return this.toAnthropicChatResponse(transformResponse(response));
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
error = fallbackError;
|
||||
} catch (fallbackErr) {
|
||||
lastError = fallbackErr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,8 +158,8 @@ export class ProxyService {
|
||||
model: request.model,
|
||||
};
|
||||
}
|
||||
} catch (downgradeError) {
|
||||
error = downgradeError;
|
||||
} catch (downgradeErr) {
|
||||
lastError = downgradeErr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,10 +313,10 @@ export class ProxyService {
|
||||
);
|
||||
|
||||
return this.normalizeGeminiGenerateResponse(response);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && this.isProjectContextError(error.message)) {
|
||||
} catch (err) {
|
||||
if (err instanceof Error && this.isProjectContextError(err.message)) {
|
||||
this.logger.warn(
|
||||
`Gemini request hit project context issue, retrying without project: ${error.message}`,
|
||||
`Gemini request hit project context issue, retrying without project: ${err.message}`,
|
||||
);
|
||||
try {
|
||||
const fallbackBody = this.createGeminiInternalRequest(
|
||||
@@ -332,15 +332,16 @@ export class ProxyService {
|
||||
extraHeaders,
|
||||
);
|
||||
return this.normalizeGeminiGenerateResponse(response);
|
||||
} catch (fallbackError) {
|
||||
error = fallbackError;
|
||||
} catch (fallbackErr) {
|
||||
lastError = fallbackErr;
|
||||
}
|
||||
} else {
|
||||
lastError = err;
|
||||
}
|
||||
|
||||
lastError = error;
|
||||
if (error instanceof Error) {
|
||||
this.logger.warn(`Gemini request failed with account ${token.email}: ${error.message}`);
|
||||
const decision = this.classifyUpstreamError(error.message);
|
||||
if (lastError instanceof Error) {
|
||||
this.logger.warn(`Gemini request failed with account ${token.email}: ${lastError.message}`);
|
||||
const decision = this.classifyUpstreamError(lastError.message);
|
||||
if (decision.retry) {
|
||||
if (decision.markAsForbidden) {
|
||||
this.tokenManager.markAsForbidden(token.id);
|
||||
@@ -400,10 +401,10 @@ export class ProxyService {
|
||||
extraHeaders,
|
||||
);
|
||||
return this.passthroughSseStream(stream);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && this.isProjectContextError(error.message)) {
|
||||
} catch (err) {
|
||||
if (err instanceof Error && this.isProjectContextError(err.message)) {
|
||||
this.logger.warn(
|
||||
`Gemini stream request hit project context issue, retrying without project: ${error.message}`,
|
||||
`Gemini stream request hit project context issue, retrying without project: ${err.message}`,
|
||||
);
|
||||
try {
|
||||
const fallbackBody = this.createGeminiInternalRequest(
|
||||
@@ -419,17 +420,18 @@ export class ProxyService {
|
||||
extraHeaders,
|
||||
);
|
||||
return this.passthroughSseStream(stream);
|
||||
} catch (fallbackError) {
|
||||
error = fallbackError;
|
||||
} catch (fallbackErr) {
|
||||
lastError = fallbackErr;
|
||||
}
|
||||
} else {
|
||||
lastError = err;
|
||||
}
|
||||
|
||||
lastError = error;
|
||||
if (error instanceof Error) {
|
||||
if (lastError instanceof Error) {
|
||||
this.logger.warn(
|
||||
`Gemini stream request failed with account ${token.email}: ${error.message}`,
|
||||
`Gemini stream request failed with account ${token.email}: ${lastError.message}`,
|
||||
);
|
||||
const decision = this.classifyUpstreamError(error.message);
|
||||
const decision = this.classifyUpstreamError(lastError.message);
|
||||
if (decision.retry) {
|
||||
if (decision.markAsForbidden) {
|
||||
this.tokenManager.markAsForbidden(token.id);
|
||||
@@ -627,10 +629,10 @@ export class ProxyService {
|
||||
this.logger.log(`Claude Response: ${JSON.stringify(claudeResponse).substring(0, 500)}`);
|
||||
return this.convertClaudeToOpenAIResponse(claudeResponse, request.model);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && this.isProjectContextError(error.message)) {
|
||||
} catch (err) {
|
||||
if (err instanceof Error && this.isProjectContextError(err.message)) {
|
||||
this.logger.warn(
|
||||
`OpenAI compatibility request hit project context issue, retrying without project: ${error.message}`,
|
||||
`OpenAI compatibility request hit project context issue, retrying without project: ${err.message}`,
|
||||
);
|
||||
try {
|
||||
const claudeRequest = this.convertOpenAIToClaude(request);
|
||||
@@ -653,16 +655,17 @@ export class ProxyService {
|
||||
);
|
||||
const claudeResponse = transformResponse(response);
|
||||
return this.convertClaudeToOpenAIResponse(claudeResponse, request.model);
|
||||
} catch (fallbackError) {
|
||||
error = fallbackError;
|
||||
} catch (fallbackErr) {
|
||||
lastError = fallbackErr;
|
||||
}
|
||||
} else {
|
||||
lastError = err;
|
||||
}
|
||||
|
||||
lastError = error;
|
||||
if (error instanceof Error) {
|
||||
this.logger.warn(`Request failed with account ${token.email}: ${error.message}`);
|
||||
if (lastError instanceof Error) {
|
||||
this.logger.warn(`Request failed with account ${token.email}: ${lastError.message}`);
|
||||
|
||||
const decision = this.classifyUpstreamError(error.message);
|
||||
const decision = this.classifyUpstreamError(lastError.message);
|
||||
if (decision.retry) {
|
||||
if (decision.markAsForbidden) {
|
||||
this.tokenManager.markAsForbidden(token.id);
|
||||
|
||||
Reference in new issue
Block a user