Merge branch 'pr-2019' into release/v3.8.0

# Conflicts:
#	open-sse/handlers/chatCore.ts
#	open-sse/services/comboConfig.ts
#	open-sse/services/usage.ts
#	src/app/(dashboard)/dashboard/providers/[id]/page.tsx
#	src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx
#	src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx
#	src/app/api/usage/analytics/route.ts
#	src/lib/db/migrationRunner.ts
#	src/lib/usage/providerLimits.ts
#	src/shared/constants/providers.ts
#	src/sse/handlers/chat.ts
#	tests/unit/provider-limits-ui.test.ts
#	tests/unit/usage-analytics.test.ts
#	tests/unit/usage-service-hardening.test.ts
This commit is contained in:
diegosouzapw
2026-05-10 01:05:11 -03:00
10 changed files with 108 additions and 10 deletions

View File

@@ -26,6 +26,11 @@ const QUOTA_LABEL_MAP: Record<string, string> = {
"search-prime": "Web Search",
"web-reader": "Web Reader",
zread: "Zread",
"5 Hours Quota": "5 Hours",
"Weekly Quota": "Weekly",
"Monthly Tools": "Monthly Tools",
tokens: "Tokens",
time_limit: "Time Limit",
};
const GLM_QUOTA_ORDER: Record<string, number> = {

View File

@@ -555,6 +555,7 @@ export async function GET(request: Request) {
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
LOWER(provider) as provider,
LOWER(model) as model,
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
COUNT(*) as requests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
@@ -563,6 +564,7 @@ export async function GET(request: Request) {
COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens
FROM usage_history
${whereClause}
GROUP BY serviceTier, LOWER(provider), LOWER(model)
`

View File

@@ -22,7 +22,22 @@ function isOpenAiCompatiblePath(pathname: string): boolean {
return OPENAI_COMPAT_PATHS.some((pattern) => pattern.test(pathname));
}
function requestWantsStreaming(req: IncomingMessage): boolean {
const accept = String(req.headers.accept || "").toLowerCase();
if (accept.includes("text/event-stream")) return true;
const pathname = (req.url || "/").split("?")[0] || "/";
return /^\/(?:v1\/)?(?:responses|chat\/completions)(?:\/|$)/.test(pathname);
}
function getProxyTimeoutMs(req: IncomingMessage): number {
if (!requestWantsStreaming(req)) return API_BRIDGE_TIMEOUTS.proxyTimeoutMs;
return Math.max(API_BRIDGE_TIMEOUTS.proxyTimeoutMs, API_BRIDGE_TIMEOUTS.serverRequestTimeoutMs);
}
function proxyRequest(req: IncomingMessage, res: ServerResponse, dashboardPort: number): void {
const proxyTimeoutMs = getProxyTimeoutMs(req);
const targetReq = http.request(
{
hostname: "127.0.0.1",
@@ -33,9 +48,14 @@ function proxyRequest(req: IncomingMessage, res: ServerResponse, dashboardPort:
...req.headers,
host: `127.0.0.1:${dashboardPort}`,
},
timeout: API_BRIDGE_TIMEOUTS.proxyTimeoutMs,
timeout: proxyTimeoutMs,
},
(targetRes) => {
const contentType = String(targetRes.headers["content-type"] || "").toLowerCase();
if (contentType.includes("text/event-stream")) {
targetReq.setTimeout(0);
}
res.writeHead(targetRes.statusCode || 502, targetRes.headers);
targetRes.pipe(res);
}
@@ -48,7 +68,7 @@ function proxyRequest(req: IncomingMessage, res: ServerResponse, dashboardPort:
res.end(
JSON.stringify({
error: "api_bridge_timeout",
detail: `Proxy request timed out after ${API_BRIDGE_TIMEOUTS.proxyTimeoutMs}ms`,
detail: `Proxy request timed out after ${proxyTimeoutMs}ms`,
})
);
});

View File

@@ -47,6 +47,7 @@ interface ProviderConnectionLike {
const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
"glm",
"glm-cn",
"zai",
"glmt",
"minimax",
"minimax-cn",

View File

@@ -1982,6 +1982,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
"kimi-coding",
"glm",
"glm-cn",
"zai",
"glmt",
"minimax",
"minimax-cn",

View File

@@ -11,7 +11,7 @@ export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 600_000;
export const DEFAULT_STREAM_READINESS_TIMEOUT_MS = 30_000;
export const DEFAULT_FETCH_CONNECT_TIMEOUT_MS = 30_000;
export const DEFAULT_FETCH_KEEPALIVE_TIMEOUT_MS = 4_000;
export const DEFAULT_API_BRIDGE_PROXY_TIMEOUT_MS = 30_000;
export const DEFAULT_API_BRIDGE_PROXY_TIMEOUT_MS = 600_000;
export const DEFAULT_API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS = 300_000;
export const DEFAULT_API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS = 60_000;
export const DEFAULT_API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS = 5_000;

View File

@@ -874,6 +874,25 @@ async function handleSingleModelChat(
return result.response;
}
if (result.errorType === "account_semaphore_capacity") {
// Local concurrency pressure is not an upstream quota failure. Prefer another
// account when possible; pinned combo steps fall through to combo orchestration.
if (hasForcedConnection) {
return result.response;
}
log.warn(
"AUTH",
`Account ${accountId}... at local concurrency cap, trying fallback account`
);
excludedConnectionIds.add(credentials.connectionId);
lastError = result.error;
lastStatus = result.status;
requestRetryLastError = result.error;
requestRetryLastStatus = result.status;
continue;
}
// Emergency fallback for budget exhaustion (402 / billing / quota keywords):
// reroute to a free model (default provider/model: nvidia + openai/gpt-oss-120b) exactly once.
if (!runtimeOptions.emergencyFallbackTried) {