Files
OmniRoute/open-sse/services/alibabaFreeTier.ts
Andrew B. 21fd0a94f8 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>
2026-08-11 04:25:23 -03:00

165 lines
5.7 KiB
TypeScript

/**
* @file alibabaFreeTier.ts
* @description Alibaba Model Studio free-tier drain detection, billing mode, and persisted model lockouts.
*
* @changes
* - [2026-07-25] [Composer] - Delegate free-eligible filtering to probe-based discovery module
* - [2026-07-24] [Composer] - Add free-vs-paid billing mode and permanent free-tier model drain handling
*/
import { isModelLocked, lockModel } from "./accountFallback.ts";
export type AlibabaBillingMode = "free" | "paid";
type AlibabaConnectionLike = {
id: string;
providerSpecificData?: Record<string, unknown> | null;
};
/** ~10 years — free-tier drains are permanent until the operator clears connection state. */
export const ALIBABA_FREE_DRAINED_LOCK_MS = 10 * 365 * 24 * 60 * 60 * 1000;
const ALIBABA_FREE_QUOTA_EXHAUSTED_PATTERNS = [
/\bfree quota has been exhausted\b/i,
/\bfree tier of the model has been exhausted\b/i,
/\buse free tier only\b/i,
] as const;
const ALIBABA_MODEL_STUDIO_PROVIDER_IDS = new Set(["alibaba", "alibaba-cn", "ali"]);
export {
filterAlibabaFreeEligibleModels,
isAlibabaFreeTierCapableModel,
} from "./alibabaFreeTierDiscovery.ts";
export {
filterAlibabaFreeVisionEligibleModels,
filterAlibabaFreeMultimodalEligibleModels,
filterAlibabaFreeAudioEligibleModels,
isAlibabaFreeTierVisionCapableModel,
isAlibabaFreeTierMultimodalCapableModel,
isAlibabaFreeTierAudioCapableModel,
} from "./alibabaFreeTierQuotaFetcher.ts";
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
export function isAlibabaModelStudioProvider(provider: string | null | undefined): boolean {
if (!provider) return false;
const normalized = provider.toLowerCase();
return ALIBABA_MODEL_STUDIO_PROVIDER_IDS.has(normalized);
}
export function isAlibabaFreeQuotaExhaustedError(errorText: string): boolean {
const text = String(errorText || "");
if (!text) return false;
return ALIBABA_FREE_QUOTA_EXHAUSTED_PATTERNS.some((pattern) => pattern.test(text));
}
export function getAlibabaBillingMode(
providerSpecificData: Record<string, unknown> | null | undefined
): AlibabaBillingMode {
const raw = asRecord(providerSpecificData).alibabaBillingMode;
return raw === "free" ? "free" : "paid";
}
export function getAlibabaFreeDrainedModels(
providerSpecificData: Record<string, unknown> | null | undefined
): string[] {
const raw = asRecord(providerSpecificData).alibabaFreeDrainedModels;
if (!Array.isArray(raw)) return [];
return raw.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
}
export function isAlibabaModelFreeDrained(
provider: string | null | undefined,
providerSpecificData: Record<string, unknown> | null | undefined,
model: string | null | undefined
): boolean {
if (!isAlibabaModelStudioProvider(provider) || !model) return false;
return getAlibabaFreeDrainedModels(providerSpecificData).includes(model);
}
export function mergeAlibabaFreeDrainedModels(
providerSpecificData: Record<string, unknown> | null | undefined,
model: string
): Record<string, unknown> {
const base = asRecord(providerSpecificData);
const existing = new Set(getAlibabaFreeDrainedModels(base));
existing.add(model);
return {
...base,
alibabaFreeDrainedModels: [...existing],
};
}
export function shouldUseLiveAlibabaFreeModelDiscovery(
providerSpecificData: Record<string, unknown> | null | undefined
): boolean {
return getAlibabaBillingMode(providerSpecificData) === "free";
}
export function filterAlibabaFreeTierModels(
modelIds: readonly string[],
providerSpecificData: Record<string, unknown> | null | undefined
): string[] {
const drained = new Set(getAlibabaFreeDrainedModels(providerSpecificData));
return modelIds.filter((id) => !drained.has(id));
}
export function rehydrateAlibabaFreeDrainedModelLocks(
provider: string,
connectionId: string,
providerSpecificData: Record<string, unknown> | null | undefined
): void {
if (
!isAlibabaModelStudioProvider(provider) ||
getAlibabaBillingMode(providerSpecificData) !== "free"
) {
return;
}
for (const model of getAlibabaFreeDrainedModels(providerSpecificData)) {
if (!isModelLocked(provider, connectionId, model)) {
lockModel(
provider,
connectionId,
model,
"free_quota_exhausted",
ALIBABA_FREE_DRAINED_LOCK_MS
);
}
}
}
export async function isAlibabaFreeTierModelRoutable(
provider: string,
connectionId: string,
model: string
): Promise<boolean> {
if (!isAlibabaModelStudioProvider(provider) || !model) return true;
if (isModelLocked(provider, connectionId, model)) return false;
try {
const { getProviderConnections } = await import("../../src/lib/db/providers.ts");
const { buildAlibabaFreeTierFilterContext, isAlibabaFreeTierCapableModel } =
await import("./alibabaFreeTierDiscovery.ts");
const connections = await getProviderConnections({ provider });
const connection = connections.find((entry) => entry.id === connectionId);
if (!connection) return true;
const providerSpecificData = connection.providerSpecificData as Record<string, unknown>;
rehydrateAlibabaFreeDrainedModelLocks(provider, connectionId, providerSpecificData);
if (getAlibabaBillingMode(providerSpecificData) === "free") {
const filterContext = buildAlibabaFreeTierFilterContext(
connections as unknown as readonly AlibabaConnectionLike[],
connectionId
);
if (!isAlibabaFreeTierCapableModel(model, filterContext)) return false;
}
return !isAlibabaModelFreeDrained(provider, providerSpecificData, model);
} catch {
return !isModelLocked(provider, connectionId, model);
}
}