mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 11:12:17 +03:00
feat(alibaba): free-tier routing with live quota sync (#8893)
* feat(alibaba): add free-tier routing with console quota and builtin allowlist Classify DashScope free vs paid models via console quota API, a hardcoded operator allowlist fallback, and per-connection drained tracking. Wire wildcard combo expansion, model refresh, combo exhaustion, and audit redaction for Alibaba console credentials. * fix(routing): reset forced connection pin and persist Alibaba free-tier drain Drop session affinity pins when a forced connection is excluded after 429, and record Alibaba free-tier exhaustion on upstream 403 so per-key drained lists stay accurate without blocking sibling keys. * fix(alibaba): prefer live quota sync over static free-tier allowlist Stop unioning the builtin text allowlist when a console quota snapshot exists, treat expired quotaValidityPeriod as not_capable, and add a dated JSON pack plus sync-alibaba-allowlist script for operator refresh without code edits. * docs(alibaba): document free-tier console path + allowlist env overrides Adds the 4 ALIBABA_FREE_TIER_*_FE_PATH / ALIBABA_FREE_TIER_ALLOWLIST_PATH env vars (referenced by alibabaFreeTierQuotaFetcher.ts and alibabaFreeTierAllowlist.ts) to .env.example and docs/reference/ENVIRONMENT.md so the env/docs contract check passes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(open-sse): split alibabaFreeTierQuotaFetcher.ts under file-size cap Extract pure parsing/classification/eligibility-filtering logic into alibabaFreeTierQuotaClassify.ts and shared types/primitives into alibabaFreeTierQuotaTypes.ts, leaving the HTTP/console-fetch flow in the original file. Public API is unchanged (re-exported), behavior is identical. Co-authored-by: AndrianBalanescu <AndrianBalanescu@users.noreply.github.com> * fix: resolve typecheck errors in alibaba-free-tier routing --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: AndrianBalanescu <AndrianBalanescu@users.noreply.github.com> Co-authored-by: AndrianBalanescu <andrian@balanescu.dev>
This commit is contained in:
@@ -7,12 +7,16 @@ export type QuotaScrapingFieldValues = {
|
||||
opencodeGoWorkspaceId: string;
|
||||
opencodeGoAuthCookie: string;
|
||||
ollamaCloudUsageCookie: string;
|
||||
alibabaConsoleCookie: string;
|
||||
alibabaConsoleSecToken: string;
|
||||
};
|
||||
|
||||
export const EMPTY_QUOTA_SCRAPING_FIELDS: QuotaScrapingFieldValues = {
|
||||
opencodeGoWorkspaceId: "",
|
||||
opencodeGoAuthCookie: "",
|
||||
ollamaCloudUsageCookie: "",
|
||||
alibabaConsoleCookie: "",
|
||||
alibabaConsoleSecToken: "",
|
||||
};
|
||||
|
||||
export function assignQuotaScrapingProviderData(
|
||||
@@ -27,6 +31,14 @@ export function assignQuotaScrapingProviderData(
|
||||
}
|
||||
} else if (provider === "ollama-cloud" && values.ollamaCloudUsageCookie.trim()) {
|
||||
target.ollamaCloudUsageCookie = values.ollamaCloudUsageCookie.trim();
|
||||
} else if (
|
||||
(provider === "alibaba" || provider === "alibaba-cn") &&
|
||||
values.alibabaConsoleCookie.trim()
|
||||
) {
|
||||
target.alibabaConsoleCookie = values.alibabaConsoleCookie.trim();
|
||||
if (values.alibabaConsoleSecToken.trim()) {
|
||||
target.alibabaConsoleSecToken = values.alibabaConsoleSecToken.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,5 +121,54 @@ export default function QuotaScrapingFields({
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === "alibaba" || provider === "alibaba-cn") {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border/50 bg-surface/20 p-4">
|
||||
<Input
|
||||
label={providerText(
|
||||
t,
|
||||
"alibabaConsoleCookieLabel",
|
||||
"Alibaba console cookie (free-tier sync)"
|
||||
)}
|
||||
name="alibabaConsoleCookie"
|
||||
type="password"
|
||||
value={values.alibabaConsoleCookie}
|
||||
onChange={(e) => onChange({ alibabaConsoleCookie: e.target.value })}
|
||||
placeholder="login_aliyunid_ticket=..."
|
||||
hint={providerText(
|
||||
t,
|
||||
"alibabaConsoleCookieHint",
|
||||
editMode
|
||||
? "Leave blank to keep the stored cookie. Paste login_aliyunid_ticket or the full Cookie header from modelstudio.console.alibabacloud.com."
|
||||
: "Paste login_aliyunid_ticket or the full Cookie header from the Model Studio console Free Quota page."
|
||||
)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
autoCapitalize="off"
|
||||
/>
|
||||
<Input
|
||||
label={providerText(
|
||||
t,
|
||||
"alibabaConsoleSecTokenLabel",
|
||||
"Alibaba console sec_token (optional)"
|
||||
)}
|
||||
name="alibabaConsoleSecToken"
|
||||
type="password"
|
||||
value={values.alibabaConsoleSecToken}
|
||||
onChange={(e) => onChange({ alibabaConsoleSecToken: e.target.value })}
|
||||
placeholder="KmdQ..."
|
||||
hint={providerText(
|
||||
t,
|
||||
"alibabaConsoleSecTokenHint",
|
||||
"Optional. Copy sec_token from the Free Quota network request if cookie-only sync fails."
|
||||
)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
autoCapitalize="off"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,35 +20,18 @@ import {
|
||||
} from "@omniroute/open-sse/config/providers/registry/kimi/coding/runtime.ts";
|
||||
import { ALIBABA_MODEL_STUDIO_MODELS } from "@omniroute/open-sse/config/providers/registry/alibaba/index.ts";
|
||||
import { QWEN_CLOUD_TEXT_MODELS } from "@omniroute/open-sse/config/providers/registry/qwen-cloud/index.ts";
|
||||
import { filterAlibabaFreeEligibleModels } from "@omniroute/open-sse/services/alibabaFreeTierDiscovery.ts";
|
||||
import { shouldUseLiveAlibabaFreeModelDiscovery } from "@omniroute/open-sse/services/alibabaFreeTier.ts";
|
||||
import { isDashscopeTextModelId } from "@omniroute/open-sse/services/dashscopeTextModels.ts";
|
||||
import { extractZaiToken } from "@omniroute/open-sse/executors/zai-web.ts";
|
||||
import { normalizeOpenAiLikeModelsResponse } from "./normalizers";
|
||||
|
||||
const DASHSCOPE_TEXT_MODEL_PREFIXES = [
|
||||
"qwen",
|
||||
"qwq-",
|
||||
"deepseek-",
|
||||
"glm-",
|
||||
"kimi-",
|
||||
"minimax-",
|
||||
] as const;
|
||||
|
||||
// DashScope's OpenAI-compatible /models response contains only the standard
|
||||
// id/object/owned_by fields for Alibaba and Qwen Cloud, so there is no upstream
|
||||
// modality field to filter on. Keep known text-generation families and reject IDs
|
||||
// whose tokenized names identify media, speech, embedding, reranking, or vision-only lines.
|
||||
const DASHSCOPE_NON_TEXT_MODEL_TOKEN =
|
||||
/(?:^|[-_.\/])(?:asr|audio|captioner|embedding|image|livetranslate|omni|ocr|realtime|rerank|s2s|speech|tts|video|vl)(?:$|[-_.\/])/i;
|
||||
const QWEN_CLOUD_TEXT_MODEL_IDS = new Set(QWEN_CLOUD_TEXT_MODELS.map((model) => model.id));
|
||||
const ALIBABA_MODEL_STUDIO_MODEL_IDS = new Set(
|
||||
ALIBABA_MODEL_STUDIO_MODELS.map((model) => model.id)
|
||||
);
|
||||
|
||||
export function isDashscopeTextModelId(value: unknown): value is string {
|
||||
if (typeof value !== "string") return false;
|
||||
const modelId = value.trim().toLowerCase();
|
||||
if (!modelId || DASHSCOPE_NON_TEXT_MODEL_TOKEN.test(modelId)) return false;
|
||||
return DASHSCOPE_TEXT_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix));
|
||||
}
|
||||
export { isDashscopeTextModelId };
|
||||
|
||||
export function parseDashscopeTextModels(data: any): any[] {
|
||||
const models = Array.isArray(data?.data)
|
||||
@@ -83,6 +66,23 @@ export function parseAlibabaModelStudioModels(data: any): any[] {
|
||||
);
|
||||
}
|
||||
|
||||
export function parseAlibabaModelStudioModelsForConnection(
|
||||
data: any,
|
||||
providerSpecificData?: Record<string, unknown> | null
|
||||
): any[] {
|
||||
if (shouldUseLiveAlibabaFreeModelDiscovery(providerSpecificData)) {
|
||||
const models = parseDashscopeTextModels(data);
|
||||
const eligibleIds = new Set(
|
||||
filterAlibabaFreeEligibleModels(
|
||||
models.map((model: { id?: string }) => model.id).filter(Boolean) as string[],
|
||||
providerSpecificData
|
||||
)
|
||||
);
|
||||
return models.filter((model: { id?: string }) => model.id && eligibleIds.has(model.id));
|
||||
}
|
||||
return parseAlibabaModelStudioModels(data);
|
||||
}
|
||||
|
||||
export function parseQwenCloudTextModels(data: any): any[] {
|
||||
return parseCuratedDashscopeModels(data, QWEN_CLOUD_TEXT_MODELS, QWEN_CLOUD_TEXT_MODEL_IDS);
|
||||
}
|
||||
|
||||
@@ -2322,7 +2322,15 @@ export async function GET(
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const pageModels = config.parseResponse(data);
|
||||
let pageModels = config.parseResponse(data);
|
||||
if (provider === "alibaba" || provider === "alibaba-cn") {
|
||||
const { parseAlibabaModelStudioModelsForConnection } =
|
||||
await import("./discovery/providerModelsConfig.ts");
|
||||
pageModels = parseAlibabaModelStudioModelsForConnection(
|
||||
data,
|
||||
connection.providerSpecificData as Record<string, unknown> | null | undefined
|
||||
);
|
||||
}
|
||||
allModels = allModels.concat(pageModels);
|
||||
|
||||
const nextPageToken = data.nextPageToken;
|
||||
@@ -2344,6 +2352,45 @@ export async function GET(
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === "alibaba" || provider === "alibaba-cn") {
|
||||
const { shouldUseLiveAlibabaFreeModelDiscovery } =
|
||||
await import("@omniroute/open-sse/services/alibabaFreeTier.ts");
|
||||
const { scheduleAlibabaFreeTierProbeRefresh } =
|
||||
await import("@omniroute/open-sse/services/alibabaFreeTierDiscovery.ts");
|
||||
const { scheduleAlibabaFreeTierQuotaRefresh, hasAlibabaConsoleFreeTierAuth } =
|
||||
await import("@omniroute/open-sse/services/alibabaFreeTierQuotaFetcher.ts");
|
||||
const { resolveAlibabaProviderBaseUrl } =
|
||||
await import("@/shared/constants/alibabaProviderRegions.ts");
|
||||
const providerSpecificData = connection.providerSpecificData as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
if (shouldUseLiveAlibabaFreeModelDiscovery(providerSpecificData)) {
|
||||
if (hasAlibabaConsoleFreeTierAuth(providerSpecificData)) {
|
||||
scheduleAlibabaFreeTierQuotaRefresh(provider, {
|
||||
id: connectionId,
|
||||
providerSpecificData,
|
||||
});
|
||||
} else {
|
||||
const baseUrl = resolveAlibabaProviderBaseUrl(
|
||||
provider,
|
||||
providerSpecificData,
|
||||
paginationBaseUrl.replace(/\/models$/, "")
|
||||
);
|
||||
scheduleAlibabaFreeTierProbeRefresh(
|
||||
provider,
|
||||
{
|
||||
id: connectionId,
|
||||
apiKey: token,
|
||||
providerSpecificData,
|
||||
},
|
||||
allModels,
|
||||
`${baseUrl.replace(/\/$/, "")}/chat/completions`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buildApiDiscoveryResponse(allModels);
|
||||
} catch (error) {
|
||||
if (error instanceof SafeOutboundFetchError && error.code === "URL_GUARD_BLOCKED") {
|
||||
|
||||
@@ -76,6 +76,8 @@ export function summarizeProviderConnectionForAudit(connection: unknown) {
|
||||
if (Object.keys(providerSpecificData).length > 0) {
|
||||
const sanitizedProviderSpecificData = { ...providerSpecificData };
|
||||
delete sanitizedProviderSpecificData.consoleApiKey;
|
||||
delete sanitizedProviderSpecificData.alibabaConsoleCookie;
|
||||
delete sanitizedProviderSpecificData.alibabaConsoleSecToken;
|
||||
sanitized.providerSpecificData = sanitizedProviderSpecificData;
|
||||
}
|
||||
|
||||
|
||||
@@ -311,6 +311,8 @@ export function validateProviderSpecificData(
|
||||
"ollamaCloudUsageCookie",
|
||||
"ollamaCloudCookie",
|
||||
"usageCookie",
|
||||
"alibabaConsoleCookie",
|
||||
"alibabaConsoleSecToken",
|
||||
] as const) {
|
||||
const value = data[key];
|
||||
if (value !== undefined && value !== null && typeof value !== "string") {
|
||||
|
||||
@@ -107,6 +107,7 @@ import { isSubscriptionQuotaText } from "@omniroute/open-sse/services/quotaTextC
|
||||
import { resolveUseUpstream429BreakerHints } from "@/shared/utils/providerHints";
|
||||
import { getCircuitBreaker, isLocalStreamLifecycleError } from "../../shared/utils/circuitBreaker";
|
||||
import { markAccountExhaustedFrom429 } from "../../domain/quotaCache";
|
||||
import { resolveForcedConnectionForCredentialPool } from "../services/sessionAffinityPin.ts";
|
||||
import { RequestTelemetry, recordTelemetry } from "../../shared/utils/requestTelemetry";
|
||||
import { generateRequestId } from "../../shared/utils/requestId";
|
||||
import { logAuditEvent } from "../../lib/compliance/index";
|
||||
@@ -1310,9 +1311,19 @@ async function handleSingleModelChat(
|
||||
...(!forceLiveComboTest && bypassProviderQuotaPolicy
|
||||
? { bypassQuotaPolicy: true }
|
||||
: {}),
|
||||
...(runtimeOptions.forcedConnectionId
|
||||
? { forcedConnectionId: runtimeOptions.forcedConnectionId }
|
||||
: {}),
|
||||
...(() => {
|
||||
const effectiveForcedId = resolveForcedConnectionForCredentialPool({
|
||||
forcedConnectionId: runtimeOptions.forcedConnectionId ?? null,
|
||||
excludedConnectionIds,
|
||||
connections: [],
|
||||
allowRateLimitedConnections:
|
||||
runtimeOptions.allowRateLimitedConnection === true || forceLiveComboTest,
|
||||
bypassQuotaPolicy: forceLiveComboTest || bypassProviderQuotaPolicy,
|
||||
isQuotaExhausted: () => false,
|
||||
isQuotaPolicyBlocked: () => false,
|
||||
});
|
||||
return effectiveForcedId ? { forcedConnectionId: effectiveForcedId } : {};
|
||||
})(),
|
||||
}
|
||||
);
|
||||
preselectedCredentials = null;
|
||||
|
||||
@@ -56,6 +56,15 @@ import {
|
||||
classifyProviderError,
|
||||
PROVIDER_ERROR_TYPES,
|
||||
} from "@omniroute/open-sse/services/errorClassifier.ts";
|
||||
import {
|
||||
ALIBABA_FREE_DRAINED_LOCK_MS,
|
||||
getAlibabaBillingMode,
|
||||
isAlibabaFreeQuotaExhaustedError,
|
||||
isAlibabaModelFreeDrained,
|
||||
isAlibabaModelStudioProvider,
|
||||
mergeAlibabaFreeDrainedModels,
|
||||
rehydrateAlibabaFreeDrainedModelLocks,
|
||||
} from "@omniroute/open-sse/services/alibabaFreeTier.ts";
|
||||
|
||||
import {
|
||||
getCodexModelScope,
|
||||
@@ -74,6 +83,7 @@ import { isModelExcludedByConnection } from "@/domain/connectionModelRules";
|
||||
import {
|
||||
applySessionAffinityPin,
|
||||
formatSessionKeyForLog,
|
||||
resolveForcedConnectionForCredentialPool,
|
||||
resolveSessionAffinityTtlMs,
|
||||
selectSessionAffinityConnection,
|
||||
} from "./sessionAffinityPin";
|
||||
@@ -1038,6 +1048,15 @@ export async function getProviderCredentials(
|
||||
let connections = (Array.isArray(connectionsRaw) ? connectionsRaw : [])
|
||||
.map(createLazyConnectionView)
|
||||
.filter((conn) => conn.id.length > 0);
|
||||
if (isAlibabaModelStudioProvider(provider)) {
|
||||
for (const conn of connections) {
|
||||
rehydrateAlibabaFreeDrainedModelLocks(
|
||||
provider,
|
||||
conn.id,
|
||||
conn.providerSpecificData as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
}
|
||||
// allowedConnections: restrict to specific connection IDs (from API key policy, #363)
|
||||
if (allowedConnections && allowedConnections.length > 0) {
|
||||
connections = connections.filter((conn) => allowedConnections.includes(conn.id));
|
||||
@@ -1060,6 +1079,19 @@ export async function getProviderCredentials(
|
||||
evaluateQuotaLimitPolicy(provider, c as ProviderConnectionView, requestedModel).blocked,
|
||||
}) ?? forcedConnectionId;
|
||||
|
||||
forcedConnectionId = resolveForcedConnectionForCredentialPool({
|
||||
forcedConnectionId,
|
||||
excludedConnectionIds,
|
||||
connections,
|
||||
allowRateLimitedConnections,
|
||||
bypassQuotaPolicy,
|
||||
isQuotaExhausted: (connectionId) =>
|
||||
isQuotaExhaustedForRequest(connectionId, provider, requestedModel),
|
||||
isQuotaPolicyBlocked: (connection) =>
|
||||
evaluateQuotaLimitPolicy(provider, connection as ProviderConnectionView, requestedModel)
|
||||
.blocked,
|
||||
});
|
||||
|
||||
if (forcedConnectionId) {
|
||||
connections = connections.filter((conn) => conn.id === forcedConnectionId);
|
||||
}
|
||||
@@ -1205,7 +1237,15 @@ export async function getProviderCredentials(
|
||||
return false;
|
||||
}
|
||||
// Per-model lockout: if this specific model/family is locked on this connection, skip it
|
||||
if (requestedModel && isModelLocked(provider, c.id, requestedModel)) {
|
||||
if (
|
||||
requestedModel &&
|
||||
(isModelLocked(provider, c.id, requestedModel) ||
|
||||
isAlibabaModelFreeDrained(
|
||||
provider,
|
||||
c.providerSpecificData as Record<string, unknown>,
|
||||
requestedModel
|
||||
))
|
||||
) {
|
||||
connectionFilterStatus.set(c.id, "modelLocked");
|
||||
if (
|
||||
provider === "antigravity" &&
|
||||
@@ -1987,6 +2027,9 @@ export async function markAccountUnavailable(
|
||||
|
||||
// Read passthroughModels from connection config (user-configured per-model quota)
|
||||
const connProviderSpecificData = (conn?.providerSpecificData as Record<string, unknown>) || {};
|
||||
if (provider && conn) {
|
||||
rehydrateAlibabaFreeDrainedModelLocks(provider, connectionId, connProviderSpecificData);
|
||||
}
|
||||
const connectionPassthroughModels = connProviderSpecificData.passthroughModels as
|
||||
boolean | undefined;
|
||||
// #2997: per-connection opt-out of the TRANSIENT connection cooldown. When set,
|
||||
@@ -2096,6 +2139,51 @@ export async function markAccountUnavailable(
|
||||
if (!shouldFallback) return { shouldFallback: false, cooldownMs: 0 };
|
||||
const providerErrorType = classifyProviderError(status, errorText, provider);
|
||||
|
||||
if (
|
||||
isAlibabaModelStudioProvider(provider) &&
|
||||
status === 403 &&
|
||||
model &&
|
||||
isAlibabaFreeQuotaExhaustedError(errorText)
|
||||
) {
|
||||
const billingMode = getAlibabaBillingMode(connProviderSpecificData);
|
||||
if (billingMode === "free") {
|
||||
const persistedProviderSpecificData = mergeAlibabaFreeDrainedModels(
|
||||
connProviderSpecificData,
|
||||
model
|
||||
);
|
||||
await updateProviderConnection(connectionId, {
|
||||
providerSpecificData: persistedProviderSpecificData,
|
||||
lastErrorType: "free_quota_exhausted",
|
||||
lastError: `Model ${model} free quota exhausted`,
|
||||
lastErrorAt: new Date().toISOString(),
|
||||
errorCode: status,
|
||||
});
|
||||
rehydrateAlibabaFreeDrainedModelLocks(
|
||||
provider!,
|
||||
connectionId,
|
||||
persistedProviderSpecificData
|
||||
);
|
||||
recordModelLockoutFailure(
|
||||
provider!,
|
||||
connectionId,
|
||||
model!,
|
||||
"free_quota_exhausted",
|
||||
status,
|
||||
0,
|
||||
effectiveProviderProfile,
|
||||
{
|
||||
exactCooldownMs: ALIBABA_FREE_DRAINED_LOCK_MS,
|
||||
maxCooldownMs: ALIBABA_FREE_DRAINED_LOCK_MS,
|
||||
}
|
||||
);
|
||||
log.info(
|
||||
"AUTH",
|
||||
`Alibaba free-tier drain for ${provider}:${model} — model permanently removed from routing (billingMode=free)`
|
||||
);
|
||||
return { shouldFallback: true, cooldownMs: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
if (provider && resolveProviderId(provider) === "grok-web" && status === 403 && model) {
|
||||
const lockout = recordModelLockoutFailure(
|
||||
provider,
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
* connection, so the existing 429-driven `deleteSessionAccountAffinity`
|
||||
* failover still owns rotating away from a pin that stops working.
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-24] [Composer] - Drop forcedConnectionId when excluded or ineligible (429 loop fix)
|
||||
*
|
||||
* This module stays decoupled from auth.ts internals: the three predicates that
|
||||
* live in (or would cause a cycle back into) auth.ts —
|
||||
* `isTerminalConnectionStatus`, `isCodexScopeUnavailable`, and the quota-policy
|
||||
@@ -163,9 +166,7 @@ export function resolveSessionAffinityTtlMs(
|
||||
): number {
|
||||
const override = Number(options.sessionAffinityTtlMs);
|
||||
if (Number.isFinite(override) && override > 0) return override;
|
||||
const configured = Number(
|
||||
settings.sessionAffinityTtlMs ?? settings.codexSessionAffinityTtlMs
|
||||
);
|
||||
const configured = Number(settings.sessionAffinityTtlMs ?? settings.codexSessionAffinityTtlMs);
|
||||
if (Number.isFinite(configured) && configured > 0) return configured;
|
||||
return 0;
|
||||
}
|
||||
@@ -255,3 +256,41 @@ export function applySessionAffinityPin(params: ApplySessionAffinityPinParams):
|
||||
);
|
||||
return pinned.connectionId;
|
||||
}
|
||||
|
||||
export interface ResolveForcedConnectionForPoolParams {
|
||||
forcedConnectionId: string | null;
|
||||
excludedConnectionIds: ReadonlySet<string>;
|
||||
connections: AffinityPinConnection[];
|
||||
allowRateLimitedConnections: boolean;
|
||||
bypassQuotaPolicy: boolean;
|
||||
isQuotaExhausted: (connectionId: string) => boolean;
|
||||
isQuotaPolicyBlocked: (connection: AffinityPinConnection) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset-aware combo routing pins a single `forcedConnectionId` per target. When
|
||||
* that account 429s (quota exhausted / cooldown), the chat retry loop excludes
|
||||
* it — but keeping the force would narrow the pool back to the same dead
|
||||
* account. Drop the pin whenever the forced id is excluded or no longer eligible.
|
||||
*/
|
||||
export function resolveForcedConnectionForCredentialPool(
|
||||
params: ResolveForcedConnectionForPoolParams
|
||||
): string | null {
|
||||
const forced = params.forcedConnectionId?.trim() || null;
|
||||
if (!forced || params.excludedConnectionIds.has(forced)) return null;
|
||||
|
||||
if (params.connections.length === 0) {
|
||||
return forced;
|
||||
}
|
||||
|
||||
const forcedConn = params.connections.find((conn) => conn.id === forced);
|
||||
if (!forcedConn) return null;
|
||||
|
||||
if (!params.allowRateLimitedConnections && isAccountUnavailable(forcedConn.rateLimitedUntil)) {
|
||||
return null;
|
||||
}
|
||||
if (params.isQuotaExhausted(forced)) return null;
|
||||
if (!params.bypassQuotaPolicy && params.isQuotaPolicyBlocked(forcedConn)) return null;
|
||||
|
||||
return forced;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user