mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 11:43:10 +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:
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user