mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +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:
13
.env.example
13
.env.example
@@ -1978,6 +1978,19 @@ APP_LOG_TO_FILE=true
|
||||
# ALIBABA_CODING_PLAN_HOST=
|
||||
# ALIBABA_CODING_PLAN_QUOTA_URL=
|
||||
|
||||
# ── Alibaba Model Studio free-tier quota sync ──
|
||||
# Console front-end path overrides for the free-tier quota fetcher. Used by:
|
||||
# open-sse/services/alibabaFreeTierQuotaFetcher.ts. When unset, the fetcher
|
||||
# uses the production Bailian console paths.
|
||||
# ALIBABA_FREE_TIER_VISION_FE_PATH=
|
||||
# ALIBABA_FREE_TIER_MULTIMODAL_FE_PATH=
|
||||
# ALIBABA_FREE_TIER_AUDIO_FE_PATH=
|
||||
# Optional path to a local JSON override for the built-in text free-tier
|
||||
# allowlist. Used by: open-sse/services/alibabaFreeTierAllowlist.ts. When
|
||||
# unset, the fetcher falls back to $DATA_DIR/alibaba-free-tier-allowlist.json
|
||||
# then config/alibaba-free-tier-allowlist.json.
|
||||
# ALIBABA_FREE_TIER_ALLOWLIST_PATH=
|
||||
|
||||
# ── Context window tuning ──
|
||||
# Tokens reserved for completion output when computing prompt budgets.
|
||||
# Used by: open-sse/services/contextManager.ts. Default: 1024.
|
||||
|
||||
82
config/alibaba-free-tier-allowlist.json
Normal file
82
config/alibaba-free-tier-allowlist.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"asOf": "2026-07-28",
|
||||
"validUntil": "2026-08-27",
|
||||
"capable": [
|
||||
"deepseek-v3.2",
|
||||
"deepseek-v4-pro",
|
||||
"glm-5.2",
|
||||
"qwen-flash",
|
||||
"qwen-flash-2025-07-28",
|
||||
"qwen-flash-character",
|
||||
"qwen-max",
|
||||
"qwen-mt-flash",
|
||||
"qwen-mt-lite",
|
||||
"qwen-mt-plus",
|
||||
"qwen-mt-turbo",
|
||||
"qwen-plus-2025-04-28",
|
||||
"qwen-plus-2025-07-14",
|
||||
"qwen-plus-2025-07-28",
|
||||
"qwen-plus-2025-09-11",
|
||||
"qwen-plus-character",
|
||||
"qwen-plus-latest",
|
||||
"qwen3-14b",
|
||||
"qwen3-235b-a22b",
|
||||
"qwen3-235b-a22b-instruct-2507",
|
||||
"qwen3-235b-a22b-thinking-2507",
|
||||
"qwen3-30b-a3b",
|
||||
"qwen3-30b-a3b-instruct-2507",
|
||||
"qwen3-30b-a3b-thinking-2507",
|
||||
"qwen3-32b",
|
||||
"qwen3-8b",
|
||||
"qwen3-coder-30b-a3b-instruct",
|
||||
"qwen3-coder-480b-a35b-instruct",
|
||||
"qwen3-coder-flash",
|
||||
"qwen3-coder-flash-2025-07-28",
|
||||
"qwen3-coder-next",
|
||||
"qwen3-coder-plus",
|
||||
"qwen3-coder-plus-2025-07-22",
|
||||
"qwen3-coder-plus-2025-09-23",
|
||||
"qwen3-max",
|
||||
"qwen3-max-2025-09-23",
|
||||
"qwen3-max-2026-01-23",
|
||||
"qwen3-max-preview",
|
||||
"qwen3-next-80b-a3b-instruct",
|
||||
"qwen3-next-80b-a3b-thinking",
|
||||
"qwen3.5-122b-a10b",
|
||||
"qwen3.5-27b",
|
||||
"qwen3.5-397b-a17b",
|
||||
"qwen3.5-flash",
|
||||
"qwen3.5-flash-2026-02-23",
|
||||
"qwen3.5-plus",
|
||||
"qwen3.5-plus-2026-02-15",
|
||||
"qwen3.5-plus-2026-04-20",
|
||||
"qwen3.6-27b",
|
||||
"qwen3.6-35b-a3b",
|
||||
"qwen3.6-flash",
|
||||
"qwen3.6-flash-2026-04-16",
|
||||
"qwen3.6-max-preview",
|
||||
"qwen3.6-plus",
|
||||
"qwen3.6-plus-2026-04-02",
|
||||
"qwen3.7-flash",
|
||||
"qwen3.7-flash-2026-07-15",
|
||||
"qwen3.7-max-2026-05-17",
|
||||
"qwen3.7-max-2026-05-20",
|
||||
"qwen3.7-max-2026-06-08",
|
||||
"qwen3.7-max-preview",
|
||||
"qwen3.7-plus-2026-05-26",
|
||||
"qwq-plus"
|
||||
],
|
||||
"noFreeTier": [
|
||||
"deepseek-v4-flash",
|
||||
"glm-5.1",
|
||||
"glm-5.2-fast-preview",
|
||||
"kimi-k2.7-code",
|
||||
"qwen-plus",
|
||||
"qwen-plus-2025-01-25",
|
||||
"qwen-plus-character-ja",
|
||||
"qwen-turbo",
|
||||
"qwen3.5-35b-a3b",
|
||||
"qwen3.7-max",
|
||||
"qwen3.7-plus"
|
||||
]
|
||||
}
|
||||
@@ -1116,6 +1116,10 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
|
||||
| `REDIS_URL` | `redis://localhost:6379` | `src/shared/utils/rateLimiter.ts` | Redis connection string for the rate limiter backend. |
|
||||
| `ALIBABA_CODING_PLAN_HOST` | _(production host)_ | `open-sse/services/bailianQuotaFetcher.ts` | Override the host used to fetch Alibaba Bailian coding-plan quotas. |
|
||||
| `ALIBABA_CODING_PLAN_QUOTA_URL` | derived from host | `open-sse/services/bailianQuotaFetcher.ts` | Full quota URL override for Alibaba Bailian. |
|
||||
| `ALIBABA_FREE_TIER_VISION_FE_PATH` | `/costing-balance/free-quota-image-video` | `open-sse/services/alibabaFreeTierQuotaFetcher.ts` | Console front-end path override for fetching Alibaba Model Studio free-tier vision/media quota. |
|
||||
| `ALIBABA_FREE_TIER_MULTIMODAL_FE_PATH` | `/costing-balance/free-quota-multimodal` | `open-sse/services/alibabaFreeTierQuotaFetcher.ts` | Console front-end path override for fetching Alibaba Model Studio free-tier multimodal quota. |
|
||||
| `ALIBABA_FREE_TIER_AUDIO_FE_PATH` | `/costing-balance/free-quota-audio` | `open-sse/services/alibabaFreeTierQuotaFetcher.ts` | Console front-end path override for fetching Alibaba Model Studio free-tier audio quota. |
|
||||
| `ALIBABA_FREE_TIER_ALLOWLIST_PATH` | _(unset)_ | `open-sse/services/alibabaFreeTierAllowlist.ts` | Optional path to a local JSON override for the built-in Alibaba free-tier text-model allowlist. Falls back to `$DATA_DIR/alibaba-free-tier-allowlist.json`, then `config/alibaba-free-tier-allowlist.json`. |
|
||||
| `CONTEXT_RESERVE_TOKENS` | `1024` | `open-sse/services/contextManager.ts` | Tokens reserved for completion output when computing prompt budgets. |
|
||||
| `CONTEXT_KEEP_LATEST_IMAGES` | `2` | `open-sse/services/contextManager.ts` | How many of the newest inline images to keep when pruning older ones to fit the context window (#8560). |
|
||||
| `MODEL_ALIAS_COMPAT_ENABLED` | enabled | `open-sse/services/model.ts` | Toggle the legacy model-alias compatibility layer used by older clients. |
|
||||
|
||||
164
open-sse/services/alibabaFreeTier.ts
Normal file
164
open-sse/services/alibabaFreeTier.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
202
open-sse/services/alibabaFreeTierAllowlist.ts
Normal file
202
open-sse/services/alibabaFreeTierAllowlist.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* @file alibabaFreeTierAllowlist.ts
|
||||
* @description Offline fallback allowlist for Alibaba Model Studio free-tier text models.
|
||||
*
|
||||
* Prefer live console quota sync (`alibabaFreeTierQuotaFetcher.ts`). This pack is used
|
||||
* only when no recent console snapshot exists on the connection.
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-28] [Composer] - Load dated JSON pack from DATA_DIR/config with embedded fallback
|
||||
* - [2026-07-25] [Composer] - Hardcode text free/paid model lists from operator console quota export
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export type AlibabaFreeTierAllowlistPack = {
|
||||
asOf: string;
|
||||
validUntil?: string;
|
||||
capable: string[];
|
||||
noFreeTier: string[];
|
||||
};
|
||||
|
||||
export const ALIBABA_FREE_TIER_TEXT_CAPABLE_MODELS = [
|
||||
"deepseek-v3.2",
|
||||
"deepseek-v4-pro",
|
||||
"glm-5.2",
|
||||
"qwen-flash",
|
||||
"qwen-flash-2025-07-28",
|
||||
"qwen-flash-character",
|
||||
"qwen-max",
|
||||
"qwen-mt-flash",
|
||||
"qwen-mt-lite",
|
||||
"qwen-mt-plus",
|
||||
"qwen-mt-turbo",
|
||||
"qwen-plus-2025-04-28",
|
||||
"qwen-plus-2025-07-14",
|
||||
"qwen-plus-2025-07-28",
|
||||
"qwen-plus-2025-09-11",
|
||||
"qwen-plus-character",
|
||||
"qwen-plus-latest",
|
||||
"qwen3-14b",
|
||||
"qwen3-235b-a22b",
|
||||
"qwen3-235b-a22b-instruct-2507",
|
||||
"qwen3-235b-a22b-thinking-2507",
|
||||
"qwen3-30b-a3b",
|
||||
"qwen3-30b-a3b-instruct-2507",
|
||||
"qwen3-30b-a3b-thinking-2507",
|
||||
"qwen3-32b",
|
||||
"qwen3-8b",
|
||||
"qwen3-coder-30b-a3b-instruct",
|
||||
"qwen3-coder-480b-a35b-instruct",
|
||||
"qwen3-coder-flash",
|
||||
"qwen3-coder-flash-2025-07-28",
|
||||
"qwen3-coder-next",
|
||||
"qwen3-coder-plus",
|
||||
"qwen3-coder-plus-2025-07-22",
|
||||
"qwen3-coder-plus-2025-09-23",
|
||||
"qwen3-max",
|
||||
"qwen3-max-2025-09-23",
|
||||
"qwen3-max-2026-01-23",
|
||||
"qwen3-max-preview",
|
||||
"qwen3-next-80b-a3b-instruct",
|
||||
"qwen3-next-80b-a3b-thinking",
|
||||
"qwen3.5-122b-a10b",
|
||||
"qwen3.5-27b",
|
||||
"qwen3.5-397b-a17b",
|
||||
"qwen3.5-flash",
|
||||
"qwen3.5-flash-2026-02-23",
|
||||
"qwen3.5-plus",
|
||||
"qwen3.5-plus-2026-02-15",
|
||||
"qwen3.5-plus-2026-04-20",
|
||||
"qwen3.6-27b",
|
||||
"qwen3.6-35b-a3b",
|
||||
"qwen3.6-flash",
|
||||
"qwen3.6-flash-2026-04-16",
|
||||
"qwen3.6-max-preview",
|
||||
"qwen3.6-plus",
|
||||
"qwen3.6-plus-2026-04-02",
|
||||
"qwen3.7-flash",
|
||||
"qwen3.7-flash-2026-07-15",
|
||||
"qwen3.7-max-2026-05-17",
|
||||
"qwen3.7-max-2026-05-20",
|
||||
"qwen3.7-max-2026-06-08",
|
||||
"qwen3.7-max-preview",
|
||||
"qwen3.7-plus-2026-05-26",
|
||||
"qwq-plus",
|
||||
] as const;
|
||||
|
||||
export const ALIBABA_NO_FREE_TIER_TEXT_MODELS = [
|
||||
"deepseek-v4-flash",
|
||||
"glm-5.1",
|
||||
"glm-5.2-fast-preview",
|
||||
"kimi-k2.7-code",
|
||||
"qwen-plus",
|
||||
"qwen-plus-2025-01-25",
|
||||
"qwen-plus-character-ja",
|
||||
"qwen-turbo",
|
||||
"qwen3.5-35b-a3b",
|
||||
"qwen3.7-max",
|
||||
"qwen3.7-plus",
|
||||
] as const;
|
||||
|
||||
const EMBEDDED_ALLOWLIST_PACK: AlibabaFreeTierAllowlistPack = {
|
||||
asOf: "2026-07-25",
|
||||
capable: [...ALIBABA_FREE_TIER_TEXT_CAPABLE_MODELS],
|
||||
noFreeTier: [...ALIBABA_NO_FREE_TIER_TEXT_MODELS],
|
||||
};
|
||||
|
||||
let cachedPack: AlibabaFreeTierAllowlistPack | null | undefined;
|
||||
|
||||
export function resetAlibabaFreeTierAllowlistCache(): void {
|
||||
cachedPack = undefined;
|
||||
}
|
||||
|
||||
function resolveAllowlistPaths(): string[] {
|
||||
const paths: string[] = [];
|
||||
const envPath = process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH?.trim();
|
||||
if (envPath) paths.push(envPath);
|
||||
|
||||
const dataDir = process.env.DATA_DIR?.trim() || path.join(os.homedir(), ".omniroute");
|
||||
paths.push(path.join(dataDir, "alibaba-free-tier-allowlist.json"));
|
||||
paths.push(path.join(process.cwd(), "config", "alibaba-free-tier-allowlist.json"));
|
||||
return paths;
|
||||
}
|
||||
|
||||
function parseAllowlistPack(raw: unknown): AlibabaFreeTierAllowlistPack | null {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
const record = raw as Record<string, unknown>;
|
||||
const capable = Array.isArray(record.capable)
|
||||
? record.capable.filter(
|
||||
(entry): entry is string => typeof entry === "string" && entry.length > 0
|
||||
)
|
||||
: [];
|
||||
const noFreeTier = Array.isArray(record.noFreeTier)
|
||||
? record.noFreeTier.filter(
|
||||
(entry): entry is string => typeof entry === "string" && entry.length > 0
|
||||
)
|
||||
: [];
|
||||
const asOf = typeof record.asOf === "string" && record.asOf.trim() ? record.asOf.trim() : null;
|
||||
if (!asOf || capable.length === 0) return null;
|
||||
|
||||
return {
|
||||
asOf,
|
||||
validUntil:
|
||||
typeof record.validUntil === "string" && record.validUntil.trim()
|
||||
? record.validUntil.trim()
|
||||
: undefined,
|
||||
capable,
|
||||
noFreeTier,
|
||||
};
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierAllowlistPackValid(
|
||||
pack: AlibabaFreeTierAllowlistPack,
|
||||
nowMs: number = Date.now()
|
||||
): boolean {
|
||||
if (!pack.validUntil) return true;
|
||||
const expiresAt = Date.parse(pack.validUntil);
|
||||
if (!Number.isFinite(expiresAt)) return true;
|
||||
return expiresAt >= nowMs;
|
||||
}
|
||||
|
||||
export function loadAlibabaFreeTierAllowlistPack(): AlibabaFreeTierAllowlistPack | null {
|
||||
if (cachedPack !== undefined) return cachedPack;
|
||||
|
||||
for (const candidatePath of resolveAllowlistPaths()) {
|
||||
try {
|
||||
if (!fs.existsSync(candidatePath)) continue;
|
||||
const parsed = parseAllowlistPack(JSON.parse(fs.readFileSync(candidatePath, "utf8")));
|
||||
if (parsed && isAlibabaFreeTierAllowlistPackValid(parsed)) {
|
||||
cachedPack = parsed;
|
||||
return cachedPack;
|
||||
}
|
||||
} catch {
|
||||
// Try next path.
|
||||
}
|
||||
}
|
||||
|
||||
cachedPack = null;
|
||||
return cachedPack;
|
||||
}
|
||||
|
||||
function resolveActiveAllowlistPack(): AlibabaFreeTierAllowlistPack {
|
||||
return loadAlibabaFreeTierAllowlistPack() ?? EMBEDDED_ALLOWLIST_PACK;
|
||||
}
|
||||
|
||||
export function getAlibabaBuiltinFreeTierTextCapableModels(): readonly string[] {
|
||||
return resolveActiveAllowlistPack().capable;
|
||||
}
|
||||
|
||||
export function getAlibabaBuiltinNoFreeTierTextModels(): readonly string[] {
|
||||
return resolveActiveAllowlistPack().noFreeTier;
|
||||
}
|
||||
|
||||
export function isAlibabaBuiltinFreeTierTextModel(modelId: string): boolean {
|
||||
return getAlibabaBuiltinFreeTierTextCapableModels().includes(modelId);
|
||||
}
|
||||
|
||||
export function isAlibabaBuiltinNoFreeTierTextModel(modelId: string): boolean {
|
||||
return getAlibabaBuiltinNoFreeTierTextModels().includes(modelId);
|
||||
}
|
||||
359
open-sse/services/alibabaFreeTierDiscovery.ts
Normal file
359
open-sse/services/alibabaFreeTierDiscovery.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* @file alibabaFreeTierDiscovery.ts
|
||||
* @description Probe-based Alibaba Model Studio free-tier eligibility discovery.
|
||||
*
|
||||
* DashScope /models does not expose free-quota metadata. We classify models with
|
||||
* minimal chat probes and persist results on the connection:
|
||||
* - AllocationQuota.FreeTierOnly → model offers free tier (drained on this account)
|
||||
* - 200 on native promo families (qwen/glm/deepseek/…) → free tier with remaining quota
|
||||
* - 200 on third-party paid families (kimi/moonshot) → no free tier (paid billing only)
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-25] [Composer] - Always union built-in text free-tier allowlist into capable/block checks
|
||||
* - [2026-07-25] [Composer] - Delegate text filter context to canonical shared eligibility builder
|
||||
* - [2026-07-25] [Composer] - Strict allowlist for alibabafree combos (no optimistic qwen/glm guessing)
|
||||
* - [2026-07-25] [Composer] - Merge free-tier state across provider connections for wildcard filtering
|
||||
* - [2026-07-25] [Composer] - Add probe-based free-tier model discovery for Alibaba connections
|
||||
*/
|
||||
|
||||
import {
|
||||
getAlibabaBillingMode,
|
||||
getAlibabaFreeDrainedModels,
|
||||
isAlibabaFreeQuotaExhaustedError,
|
||||
isAlibabaModelStudioProvider,
|
||||
mergeAlibabaFreeDrainedModels,
|
||||
type AlibabaBillingMode,
|
||||
} from "./alibabaFreeTier.ts";
|
||||
import {
|
||||
getAlibabaBuiltinFreeTierTextCapableModels,
|
||||
getAlibabaBuiltinNoFreeTierTextModels,
|
||||
} from "./alibabaFreeTierAllowlist.ts";
|
||||
import {
|
||||
buildAlibabaFreeTierTextFilterContext,
|
||||
getAlibabaFreeTierQuotaLastSyncAt,
|
||||
} from "./alibabaFreeTierQuotaFetcher.ts";
|
||||
|
||||
export type AlibabaFreeTierProbeVerdict =
|
||||
"capable_available" | "capable_drained" | "not_capable" | "unknown";
|
||||
|
||||
export type AlibabaFreeTierProbeResult = {
|
||||
modelId: string;
|
||||
verdict: AlibabaFreeTierProbeVerdict;
|
||||
status: number;
|
||||
errorCode?: string;
|
||||
};
|
||||
|
||||
/** Third-party models listed on DashScope that bill paid-only on international (no free quota toggle). */
|
||||
const ALIBABA_THIRD_PARTY_PAID_PREFIXES = [/^kimi-/i, /^moonshot-/i] as const;
|
||||
|
||||
/** Native families that participate in DashScope new-user free-quota promos. */
|
||||
const ALIBABA_NATIVE_FREE_TIER_PREFIXES = [
|
||||
/^qwen/i,
|
||||
/^qwq/i,
|
||||
/^glm-/i,
|
||||
/^deepseek-/i,
|
||||
/^minimax-/i,
|
||||
/^MiniMax-/i,
|
||||
] as const;
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function normalizeModelIdList(raw: unknown): string[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
||||
}
|
||||
|
||||
export function getAlibabaFreeTierCapableModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
const raw = asRecord(providerSpecificData).alibabaFreeTierCapableModels;
|
||||
const drained = getAlibabaFreeDrainedModels(providerSpecificData);
|
||||
return [...new Set([...normalizeModelIdList(raw), ...drained])];
|
||||
}
|
||||
|
||||
export function getAlibabaNoFreeTierModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(asRecord(providerSpecificData).alibabaNoFreeTierModels);
|
||||
}
|
||||
|
||||
export function isAlibabaThirdPartyPaidModelFamily(modelId: string): boolean {
|
||||
return ALIBABA_THIRD_PARTY_PAID_PREFIXES.some((pattern) => pattern.test(modelId));
|
||||
}
|
||||
|
||||
export function isAlibabaNativeFreeTierModelFamily(modelId: string): boolean {
|
||||
return ALIBABA_NATIVE_FREE_TIER_PREFIXES.some((pattern) => pattern.test(modelId));
|
||||
}
|
||||
|
||||
export function parseAlibabaFreeTierProbeError(bodyText: string): {
|
||||
code: string;
|
||||
message: string;
|
||||
} {
|
||||
try {
|
||||
const parsed = JSON.parse(bodyText) as {
|
||||
error?: { code?: string; type?: string; message?: string };
|
||||
};
|
||||
const error = parsed?.error;
|
||||
const code = String(error?.code || error?.type || "");
|
||||
const message = String(error?.message || bodyText || "");
|
||||
return { code, message };
|
||||
} catch {
|
||||
return { code: "", message: bodyText };
|
||||
}
|
||||
}
|
||||
|
||||
export function classifyAlibabaFreeTierProbe(
|
||||
modelId: string,
|
||||
status: number,
|
||||
bodyText: string
|
||||
): AlibabaFreeTierProbeResult {
|
||||
const { code, message } = parseAlibabaFreeTierProbeError(bodyText);
|
||||
const combined = `${code} ${message}`;
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
if (isAlibabaThirdPartyPaidModelFamily(modelId)) {
|
||||
return { modelId, verdict: "not_capable", status, errorCode: code || undefined };
|
||||
}
|
||||
if (isAlibabaNativeFreeTierModelFamily(modelId)) {
|
||||
return { modelId, verdict: "capable_available", status, errorCode: code || undefined };
|
||||
}
|
||||
return { modelId, verdict: "unknown", status, errorCode: code || undefined };
|
||||
}
|
||||
|
||||
if (
|
||||
status === 403 &&
|
||||
(code === "AllocationQuota.FreeTierOnly" || isAlibabaFreeQuotaExhaustedError(combined))
|
||||
) {
|
||||
return { modelId, verdict: "capable_drained", status, errorCode: code || undefined };
|
||||
}
|
||||
|
||||
if (status === 403 || status === 400) {
|
||||
return { modelId, verdict: "not_capable", status, errorCode: code || undefined };
|
||||
}
|
||||
|
||||
return { modelId, verdict: "unknown", status, errorCode: code || undefined };
|
||||
}
|
||||
|
||||
export function mergeAlibabaFreeTierProbeResults(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined,
|
||||
results: readonly AlibabaFreeTierProbeResult[],
|
||||
billingMode: AlibabaBillingMode = getAlibabaBillingMode(providerSpecificData)
|
||||
): Record<string, unknown> {
|
||||
const base = asRecord(providerSpecificData);
|
||||
const capable = new Set(getAlibabaFreeTierCapableModels(base));
|
||||
const noFreeTier = new Set(getAlibabaNoFreeTierModels(base));
|
||||
let next = base;
|
||||
|
||||
for (const result of results) {
|
||||
switch (result.verdict) {
|
||||
case "capable_available":
|
||||
capable.add(result.modelId);
|
||||
noFreeTier.delete(result.modelId);
|
||||
break;
|
||||
case "capable_drained":
|
||||
capable.add(result.modelId);
|
||||
noFreeTier.delete(result.modelId);
|
||||
if (billingMode === "free") {
|
||||
next = mergeAlibabaFreeDrainedModels(next, result.modelId);
|
||||
}
|
||||
break;
|
||||
case "not_capable":
|
||||
noFreeTier.add(result.modelId);
|
||||
capable.delete(result.modelId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...next,
|
||||
alibabaFreeTierCapableModels: [...capable],
|
||||
alibabaNoFreeTierModels: [...noFreeTier],
|
||||
alibabaFreeTierProbeLastRunAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
type AlibabaConnectionLike = {
|
||||
id: string;
|
||||
providerSpecificData?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a merged free-tier filter context for one Alibaba connection.
|
||||
*
|
||||
* Free-tier eligibility (capable / no-free-tier) is account-agnostic — one synced
|
||||
* console snapshot applies to every `alibabaBillingMode: free` key. Only drained
|
||||
* models stay per-connection (quota exhaustion is key-specific).
|
||||
*/
|
||||
export function buildAlibabaFreeTierFilterContext(
|
||||
connections: readonly AlibabaConnectionLike[],
|
||||
connectionId: string
|
||||
): Record<string, unknown> {
|
||||
return buildAlibabaFreeTierTextFilterContext(connections, connectionId);
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierCapableModel(
|
||||
modelId: string,
|
||||
providerSpecificData: Record<string, unknown> | null | undefined,
|
||||
options: { strictAllowlist?: boolean } = {}
|
||||
): boolean {
|
||||
const noFreeTier = new Set([
|
||||
...getAlibabaNoFreeTierModels(providerSpecificData),
|
||||
...getAlibabaBuiltinNoFreeTierTextModels(),
|
||||
]);
|
||||
if (noFreeTier.has(modelId)) return false;
|
||||
|
||||
const drained = new Set(getAlibabaFreeDrainedModels(providerSpecificData));
|
||||
if (drained.has(modelId)) return false;
|
||||
|
||||
const capable = new Set([
|
||||
...normalizeModelIdList(asRecord(providerSpecificData).alibabaFreeTierCapableModels),
|
||||
...getAlibabaBuiltinFreeTierTextCapableModels(),
|
||||
]);
|
||||
if (capable.has(modelId)) return true;
|
||||
|
||||
// Console quota API is authoritative when present — never guess past it.
|
||||
if (getAlibabaFreeTierQuotaLastSyncAt(providerSpecificData) || options.strictAllowlist) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optimistic inclusion for native families until a probe proves otherwise.
|
||||
if (isAlibabaNativeFreeTierModelFamily(modelId) && !isAlibabaThirdPartyPaidModelFamily(modelId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function filterAlibabaFreeEligibleModels(
|
||||
modelIds: readonly string[],
|
||||
providerSpecificData: Record<string, unknown> | null | undefined,
|
||||
options: { strictAllowlist?: boolean } = {}
|
||||
): string[] {
|
||||
const drained = new Set(getAlibabaFreeDrainedModels(providerSpecificData));
|
||||
return modelIds.filter((id) => {
|
||||
if (drained.has(id)) return false;
|
||||
return isAlibabaFreeTierCapableModel(id, providerSpecificData, options);
|
||||
});
|
||||
}
|
||||
|
||||
type ProbeConnection = {
|
||||
id: string;
|
||||
apiKey?: string | null;
|
||||
providerSpecificData?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export async function probeAlibabaFreeTierModel(
|
||||
connection: ProbeConnection,
|
||||
modelId: string,
|
||||
chatCompletionsUrl: string
|
||||
): Promise<AlibabaFreeTierProbeResult> {
|
||||
if (!connection.apiKey) {
|
||||
return { modelId, verdict: "unknown", status: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(chatCompletionsUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: modelId,
|
||||
messages: [{ role: "user", content: "ping" }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
});
|
||||
const bodyText = await response.text();
|
||||
return classifyAlibabaFreeTierProbe(modelId, response.status, bodyText);
|
||||
} catch {
|
||||
return { modelId, verdict: "unknown", status: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function probeAlibabaFreeTierModels(
|
||||
connection: ProbeConnection,
|
||||
modelIds: readonly string[],
|
||||
chatCompletionsUrl: string,
|
||||
options: { concurrency?: number } = {}
|
||||
): Promise<AlibabaFreeTierProbeResult[]> {
|
||||
const concurrency = Math.max(1, Math.min(options.concurrency ?? 4, 8));
|
||||
const results: AlibabaFreeTierProbeResult[] = [];
|
||||
let index = 0;
|
||||
|
||||
async function worker() {
|
||||
while (index < modelIds.length) {
|
||||
const current = modelIds[index];
|
||||
index += 1;
|
||||
results.push(await probeAlibabaFreeTierModel(connection, current, chatCompletionsUrl));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(Array.from({ length: Math.min(concurrency, modelIds.length) }, () => worker()));
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function refreshAlibabaFreeTierModelClassification(
|
||||
provider: string,
|
||||
connection: ProbeConnection,
|
||||
modelIds: readonly string[],
|
||||
chatCompletionsUrl: string
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
if (
|
||||
!isAlibabaModelStudioProvider(provider) ||
|
||||
getAlibabaBillingMode(connection.providerSpecificData) !== "free"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (getAlibabaFreeTierQuotaLastSyncAt(connection.providerSpecificData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capable = new Set(getAlibabaFreeTierCapableModels(connection.providerSpecificData));
|
||||
const noFreeTier = new Set(getAlibabaNoFreeTierModels(connection.providerSpecificData));
|
||||
const pending = modelIds.filter((id) => !capable.has(id) && !noFreeTier.has(id));
|
||||
if (pending.length === 0) return null;
|
||||
|
||||
const probeResults = await probeAlibabaFreeTierModels(connection, pending, chatCompletionsUrl, {
|
||||
concurrency: 4,
|
||||
});
|
||||
return mergeAlibabaFreeTierProbeResults(connection.providerSpecificData, probeResults);
|
||||
}
|
||||
|
||||
export function scheduleAlibabaFreeTierProbeRefresh(
|
||||
provider: string,
|
||||
connection: ProbeConnection,
|
||||
models: ReadonlyArray<{ id?: string | null }>,
|
||||
chatCompletionsUrl: string
|
||||
): void {
|
||||
const modelIds = models
|
||||
.map((model) => (typeof model?.id === "string" ? model.id.trim() : ""))
|
||||
.filter((id) => id.length > 0);
|
||||
if (modelIds.length === 0) return;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const merged = await refreshAlibabaFreeTierModelClassification(
|
||||
provider,
|
||||
connection,
|
||||
modelIds,
|
||||
chatCompletionsUrl
|
||||
);
|
||||
if (!merged) return;
|
||||
const { updateProviderConnection } = await import("../../src/lib/db/providers.ts");
|
||||
await updateProviderConnection(connection.id, { providerSpecificData: merged });
|
||||
} catch (error) {
|
||||
console.warn("[alibaba-free-tier] background probe refresh failed", {
|
||||
connectionId: connection.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
648
open-sse/services/alibabaFreeTierQuotaClassify.ts
Normal file
648
open-sse/services/alibabaFreeTierQuotaClassify.ts
Normal file
@@ -0,0 +1,648 @@
|
||||
/**
|
||||
* @file alibabaFreeTierQuotaClassify.ts
|
||||
* @description Parsing + classification + eligibility-filtering logic for Alibaba Model
|
||||
* Studio free-tier quota entries. Extracted from alibabaFreeTierQuotaFetcher.ts (which
|
||||
* exceeded the file-size cap) to isolate the pure parsing/classification helpers from
|
||||
* the HTTP/console-fetch flow. Behavior is unchanged.
|
||||
*/
|
||||
|
||||
import { getAlibabaBillingMode } from "./alibabaFreeTier.ts";
|
||||
import {
|
||||
getAlibabaBuiltinFreeTierTextCapableModels,
|
||||
getAlibabaBuiltinNoFreeTierTextModels,
|
||||
} from "./alibabaFreeTierAllowlist.ts";
|
||||
import { toNumberOrNull } from "@/shared/utils/numeric";
|
||||
import {
|
||||
isDashscopeAudioModelId,
|
||||
isDashscopeMultimodalModelId,
|
||||
isDashscopeTextModelId,
|
||||
isDashscopeVisionModelId,
|
||||
} from "./dashscopeTextModels.ts";
|
||||
import {
|
||||
asRecord,
|
||||
getAlibabaFreeTierQuotaLastSyncAt,
|
||||
isAlibabaLiveQuotaSyncAt,
|
||||
normalizeModelIdList,
|
||||
toTrimmedString,
|
||||
type AlibabaFreeTierQuotaClassification,
|
||||
type AlibabaFreeTierQuotaEntry,
|
||||
} from "./alibabaFreeTierQuotaTypes.ts";
|
||||
|
||||
export function isAlibabaQuotaValidityExpired(
|
||||
entry: AlibabaFreeTierQuotaEntry,
|
||||
nowMs: number = Date.now()
|
||||
): boolean {
|
||||
if (
|
||||
typeof entry.quotaValidityPeriod !== "number" ||
|
||||
!Number.isFinite(entry.quotaValidityPeriod)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return entry.quotaValidityPeriod < nowMs;
|
||||
}
|
||||
|
||||
function parseQuotaEntry(value: unknown): AlibabaFreeTierQuotaEntry | null {
|
||||
const record = asRecord(value);
|
||||
const model = toTrimmedString(record.model);
|
||||
if (!model) return null;
|
||||
|
||||
return {
|
||||
model,
|
||||
freeTierOnly: record.freeTierOnly === true,
|
||||
quotaStatus: toTrimmedString(record.quotaStatus) || "UNKNOWN",
|
||||
quotaTotal: toNumberOrNull(record.quotaTotal) ?? undefined,
|
||||
quotaInitTotal: toNumberOrNull(record.quotaInitTotal) ?? undefined,
|
||||
quotaTotalPercentage: toNumberOrNull(record.quotaTotalPercentage) ?? undefined,
|
||||
quotaValidityPeriod: toNumberOrNull(record.quotaValidityPeriod) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseAlibabaFreeTierQuotaEntries(payload: unknown): AlibabaFreeTierQuotaEntry[] {
|
||||
const root = asRecord(payload);
|
||||
const dataV2 = asRecord(asRecord(root.data).DataV2 ?? root.DataV2);
|
||||
const inner = asRecord(dataV2.data);
|
||||
const payloadData = asRecord(inner.data ?? inner);
|
||||
const quotas = payloadData.freeTierQuotas;
|
||||
|
||||
if (!Array.isArray(quotas)) return [];
|
||||
return quotas
|
||||
.map((entry) => parseQuotaEntry(entry))
|
||||
.filter((entry): entry is AlibabaFreeTierQuotaEntry => entry !== null);
|
||||
}
|
||||
|
||||
export function classifyAlibabaFreeTierQuotaEntry(
|
||||
entry: AlibabaFreeTierQuotaEntry,
|
||||
nowMs: number = Date.now()
|
||||
): "available" | "capable_unknown" | "drained" | "not_capable" {
|
||||
if (isAlibabaQuotaValidityExpired(entry, nowMs)) {
|
||||
return "not_capable";
|
||||
}
|
||||
|
||||
if (!entry.freeTierOnly) {
|
||||
return "not_capable";
|
||||
}
|
||||
|
||||
if (entry.quotaStatus === "VALID") {
|
||||
if (typeof entry.quotaTotal === "number") {
|
||||
return entry.quotaTotal > 0 ? "available" : "drained";
|
||||
}
|
||||
return "capable_unknown";
|
||||
}
|
||||
|
||||
if (entry.quotaStatus === "UNKNOWN") {
|
||||
return "capable_unknown";
|
||||
}
|
||||
|
||||
return "not_capable";
|
||||
}
|
||||
|
||||
export function classifyAlibabaVisionFreeTierQuotaEntry(
|
||||
entry: AlibabaFreeTierQuotaEntry,
|
||||
nowMs: number = Date.now()
|
||||
): "available" | "drained" | "not_capable" {
|
||||
if (isAlibabaQuotaValidityExpired(entry, nowMs)) {
|
||||
return "not_capable";
|
||||
}
|
||||
|
||||
if (!isDashscopeVisionModelId(entry.model)) {
|
||||
return "not_capable";
|
||||
}
|
||||
|
||||
if (entry.quotaStatus === "VALID") {
|
||||
if (typeof entry.quotaTotal === "number") {
|
||||
return entry.quotaTotal > 0 ? "available" : "drained";
|
||||
}
|
||||
if (typeof entry.quotaInitTotal === "number") {
|
||||
return entry.quotaInitTotal > 0 ? "available" : "drained";
|
||||
}
|
||||
return "not_capable";
|
||||
}
|
||||
|
||||
return "not_capable";
|
||||
}
|
||||
|
||||
export function classifyAlibabaVisionFreeTierQuotaEntries(
|
||||
entries: readonly AlibabaFreeTierQuotaEntry[]
|
||||
): AlibabaFreeTierQuotaClassification {
|
||||
return classifyAlibabaFreeTierQuotaEntriesByModelFilter(entries, isDashscopeVisionModelId, {
|
||||
useVisionRules: true,
|
||||
});
|
||||
}
|
||||
|
||||
function classifyAlibabaFreeTierQuotaEntriesByModelFilter(
|
||||
entries: readonly AlibabaFreeTierQuotaEntry[],
|
||||
modelFilter: (modelId: string) => boolean,
|
||||
options: { useVisionRules?: boolean } = {}
|
||||
): AlibabaFreeTierQuotaClassification {
|
||||
const capableModels: string[] = [];
|
||||
const noFreeTierModels: string[] = [];
|
||||
const drainedModels: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!modelFilter(entry.model)) continue;
|
||||
|
||||
const verdict = options.useVisionRules
|
||||
? classifyAlibabaVisionFreeTierQuotaEntry(entry)
|
||||
: classifyAlibabaFreeTierQuotaEntry(entry);
|
||||
|
||||
switch (verdict) {
|
||||
case "available":
|
||||
capableModels.push(entry.model);
|
||||
break;
|
||||
case "capable_unknown":
|
||||
capableModels.push(entry.model);
|
||||
break;
|
||||
case "drained":
|
||||
if (options.useVisionRules) {
|
||||
drainedModels.push(entry.model);
|
||||
} else {
|
||||
capableModels.push(entry.model);
|
||||
drainedModels.push(entry.model);
|
||||
}
|
||||
break;
|
||||
case "not_capable":
|
||||
noFreeTierModels.push(entry.model);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
capableModels: [...new Set(capableModels)],
|
||||
noFreeTierModels: [...new Set(noFreeTierModels)],
|
||||
drainedModels: [...new Set(drainedModels)],
|
||||
entries: entries.filter((entry) => modelFilter(entry.model)),
|
||||
};
|
||||
}
|
||||
|
||||
export function classifyAlibabaMultimodalFreeTierQuotaEntries(
|
||||
entries: readonly AlibabaFreeTierQuotaEntry[]
|
||||
): AlibabaFreeTierQuotaClassification {
|
||||
return classifyAlibabaFreeTierQuotaEntriesByModelFilter(entries, isDashscopeMultimodalModelId);
|
||||
}
|
||||
|
||||
export function classifyAlibabaAudioFreeTierQuotaEntries(
|
||||
entries: readonly AlibabaFreeTierQuotaEntry[]
|
||||
): AlibabaFreeTierQuotaClassification {
|
||||
return classifyAlibabaFreeTierQuotaEntriesByModelFilter(entries, isDashscopeAudioModelId);
|
||||
}
|
||||
|
||||
export function classifyAlibabaFreeTierQuotaEntries(
|
||||
entries: readonly AlibabaFreeTierQuotaEntry[],
|
||||
options: { textOnly?: boolean } = {}
|
||||
): AlibabaFreeTierQuotaClassification {
|
||||
const capableModels: string[] = [];
|
||||
const noFreeTierModels: string[] = [];
|
||||
const drainedModels: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (options.textOnly && !isDashscopeTextModelId(entry.model)) continue;
|
||||
|
||||
const verdict = classifyAlibabaFreeTierQuotaEntry(entry);
|
||||
switch (verdict) {
|
||||
case "available":
|
||||
case "capable_unknown":
|
||||
capableModels.push(entry.model);
|
||||
break;
|
||||
case "drained":
|
||||
capableModels.push(entry.model);
|
||||
drainedModels.push(entry.model);
|
||||
break;
|
||||
case "not_capable":
|
||||
noFreeTierModels.push(entry.model);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
capableModels: [...new Set(capableModels)],
|
||||
noFreeTierModels: [...new Set(noFreeTierModels)],
|
||||
drainedModels: [...new Set(drainedModels)],
|
||||
entries: [...entries],
|
||||
};
|
||||
}
|
||||
|
||||
function unionModelIdLists(lists: readonly (readonly string[])[]): string[] {
|
||||
return [...new Set(lists.flat())];
|
||||
}
|
||||
|
||||
/** Eligibility is account-agnostic; only drained/quota exhaustion is per-connection. */
|
||||
const ALIBABA_SHARED_FREE_TIER_ELIGIBILITY_KEYS = [
|
||||
"alibabaFreeTierCapableModels",
|
||||
"alibabaNoFreeTierModels",
|
||||
"alibabaFreeTierVisionCapableModels",
|
||||
"alibabaNoFreeTierVisionModels",
|
||||
"alibabaFreeTierMultimodalCapableModels",
|
||||
"alibabaNoFreeTierMultimodalModels",
|
||||
"alibabaFreeTierAudioCapableModels",
|
||||
"alibabaNoFreeTierAudioModels",
|
||||
"alibabaFreeTierQuotaEntries",
|
||||
"alibabaFreeTierVisionQuotaEntries",
|
||||
"alibabaFreeTierMultimodalQuotaEntries",
|
||||
"alibabaFreeTierAudioQuotaEntries",
|
||||
"alibabaFreeTierQuotaLastSyncAt",
|
||||
"alibabaFreeTierDiscoverySource",
|
||||
] as const;
|
||||
|
||||
export function extractAlibabaSharedFreeTierEligibility(
|
||||
providerSpecificData: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const source = asRecord(providerSpecificData);
|
||||
const shared: Record<string, unknown> = {};
|
||||
for (const key of ALIBABA_SHARED_FREE_TIER_ELIGIBILITY_KEYS) {
|
||||
if (source[key] !== undefined) {
|
||||
shared[key] = source[key];
|
||||
}
|
||||
}
|
||||
return shared;
|
||||
}
|
||||
|
||||
export function applyAlibabaSharedFreeTierEligibility(
|
||||
targetPsd: Record<string, unknown>,
|
||||
shared: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
return { ...targetPsd, ...shared };
|
||||
}
|
||||
|
||||
export type AlibabaConnectionLike = {
|
||||
id: string;
|
||||
providerSpecificData?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export type AlibabaFreeTierEligibilityFields = {
|
||||
capableKey: string;
|
||||
noFreeTierKey: string;
|
||||
drainedKey: string;
|
||||
};
|
||||
|
||||
export function pickCanonicalAlibabaFreeTierConnection(
|
||||
connections: readonly AlibabaConnectionLike[],
|
||||
fields: AlibabaFreeTierEligibilityFields
|
||||
): AlibabaConnectionLike | undefined {
|
||||
const freeConnections = connections.filter(
|
||||
(connection) => getAlibabaBillingMode(connection.providerSpecificData) === "free"
|
||||
);
|
||||
const synced = freeConnections.filter((connection) =>
|
||||
Boolean(getAlibabaFreeTierQuotaLastSyncAt(connection.providerSpecificData))
|
||||
);
|
||||
if (synced.length === 0) return undefined;
|
||||
|
||||
const withEligibility = synced.filter((connection) => {
|
||||
const psd = asRecord(connection.providerSpecificData);
|
||||
const capable = normalizeModelIdList(psd[fields.capableKey]);
|
||||
const blocked = normalizeModelIdList(psd[fields.noFreeTierKey]);
|
||||
return capable.length > 0 || blocked.length > 0;
|
||||
});
|
||||
|
||||
const pool = withEligibility.length > 0 ? withEligibility : synced;
|
||||
return pool.reduce<AlibabaConnectionLike | undefined>((best, current) => {
|
||||
if (!best) return current;
|
||||
const bestTime = getAlibabaFreeTierQuotaLastSyncAt(best.providerSpecificData) || "";
|
||||
const currentTime = getAlibabaFreeTierQuotaLastSyncAt(current.providerSpecificData) || "";
|
||||
return currentTime.localeCompare(bestTime) > 0 ? current : best;
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
function resolveAlibabaFreeTierEligibilityLists(
|
||||
connections: readonly AlibabaConnectionLike[],
|
||||
fields: AlibabaFreeTierEligibilityFields
|
||||
): { capable: string[]; noFreeTier: string[]; hasQuotaSync: boolean; quotaSyncAt?: string } {
|
||||
const freeConnections = connections.filter(
|
||||
(connection) => getAlibabaBillingMode(connection.providerSpecificData) === "free"
|
||||
);
|
||||
const hasQuotaSync = freeConnections.some((connection) =>
|
||||
Boolean(getAlibabaFreeTierQuotaLastSyncAt(connection.providerSpecificData))
|
||||
);
|
||||
const canonical = pickCanonicalAlibabaFreeTierConnection(freeConnections, fields);
|
||||
|
||||
if (canonical) {
|
||||
const psd = asRecord(canonical.providerSpecificData);
|
||||
return {
|
||||
capable: normalizeModelIdList(psd[fields.capableKey]),
|
||||
noFreeTier: normalizeModelIdList(psd[fields.noFreeTierKey]),
|
||||
hasQuotaSync,
|
||||
quotaSyncAt: getAlibabaFreeTierQuotaLastSyncAt(psd) || "provider-canonical",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
capable: unionModelIdLists(
|
||||
freeConnections.map((connection) =>
|
||||
normalizeModelIdList(asRecord(connection.providerSpecificData)[fields.capableKey])
|
||||
)
|
||||
),
|
||||
noFreeTier: unionModelIdLists(
|
||||
freeConnections.map((connection) =>
|
||||
normalizeModelIdList(asRecord(connection.providerSpecificData)[fields.noFreeTierKey])
|
||||
)
|
||||
),
|
||||
hasQuotaSync,
|
||||
};
|
||||
}
|
||||
|
||||
function buildAlibabaCategoryFilterContext(
|
||||
connections: readonly AlibabaConnectionLike[],
|
||||
connectionId: string,
|
||||
fields: {
|
||||
capableKey: string;
|
||||
noFreeTierKey: string;
|
||||
drainedKey: string;
|
||||
}
|
||||
): Record<string, unknown> {
|
||||
const freeConnections = connections.filter(
|
||||
(connection) => getAlibabaBillingMode(connection.providerSpecificData) === "free"
|
||||
);
|
||||
const target = freeConnections.find((connection) => connection.id === connectionId);
|
||||
const targetPsd = asRecord(target?.providerSpecificData);
|
||||
const eligibility = resolveAlibabaFreeTierEligibilityLists(freeConnections, fields);
|
||||
|
||||
const merged: Record<string, unknown> = {
|
||||
alibabaBillingMode: "free",
|
||||
[fields.capableKey]: eligibility.capable,
|
||||
[fields.noFreeTierKey]: eligibility.noFreeTier,
|
||||
[fields.drainedKey]: normalizeModelIdList(targetPsd[fields.drainedKey]),
|
||||
};
|
||||
|
||||
if (eligibility.hasQuotaSync) {
|
||||
merged.alibabaFreeTierQuotaLastSyncAt =
|
||||
eligibility.quotaSyncAt || getAlibabaFreeTierQuotaLastSyncAt(targetPsd) || "provider-merged";
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function buildAlibabaFreeVisionFilterContext(
|
||||
connections: readonly AlibabaConnectionLike[],
|
||||
connectionId: string
|
||||
): Record<string, unknown> {
|
||||
return buildAlibabaCategoryFilterContext(connections, connectionId, {
|
||||
capableKey: "alibabaFreeTierVisionCapableModels",
|
||||
noFreeTierKey: "alibabaNoFreeTierVisionModels",
|
||||
drainedKey: "alibabaFreeTierVisionDrainedModels",
|
||||
});
|
||||
}
|
||||
|
||||
export function buildAlibabaFreeMultimodalFilterContext(
|
||||
connections: readonly AlibabaConnectionLike[],
|
||||
connectionId: string
|
||||
): Record<string, unknown> {
|
||||
return buildAlibabaCategoryFilterContext(connections, connectionId, {
|
||||
capableKey: "alibabaFreeTierMultimodalCapableModels",
|
||||
noFreeTierKey: "alibabaNoFreeTierMultimodalModels",
|
||||
drainedKey: "alibabaFreeTierMultimodalDrainedModels",
|
||||
});
|
||||
}
|
||||
|
||||
export function buildAlibabaFreeAudioFilterContext(
|
||||
connections: readonly AlibabaConnectionLike[],
|
||||
connectionId: string
|
||||
): Record<string, unknown> {
|
||||
return buildAlibabaCategoryFilterContext(connections, connectionId, {
|
||||
capableKey: "alibabaFreeTierAudioCapableModels",
|
||||
noFreeTierKey: "alibabaNoFreeTierAudioModels",
|
||||
drainedKey: "alibabaFreeTierAudioDrainedModels",
|
||||
});
|
||||
}
|
||||
|
||||
const ALIBABA_TEXT_ELIGIBILITY_FIELDS: AlibabaFreeTierEligibilityFields = {
|
||||
capableKey: "alibabaFreeTierCapableModels",
|
||||
noFreeTierKey: "alibabaNoFreeTierModels",
|
||||
drainedKey: "alibabaFreeDrainedModels",
|
||||
};
|
||||
|
||||
export function buildAlibabaFreeTierTextFilterContext(
|
||||
connections: readonly AlibabaConnectionLike[],
|
||||
connectionId: string
|
||||
): Record<string, unknown> {
|
||||
const freeConnections = connections.filter(
|
||||
(connection) => getAlibabaBillingMode(connection.providerSpecificData) === "free"
|
||||
);
|
||||
const target = freeConnections.find((connection) => connection.id === connectionId);
|
||||
const targetPsd = asRecord(target?.providerSpecificData);
|
||||
const eligibility = resolveAlibabaFreeTierEligibilityLists(
|
||||
freeConnections,
|
||||
ALIBABA_TEXT_ELIGIBILITY_FIELDS
|
||||
);
|
||||
|
||||
const useBuiltinFallback =
|
||||
!eligibility.hasQuotaSync || !isAlibabaLiveQuotaSyncAt(eligibility.quotaSyncAt ?? null);
|
||||
|
||||
const merged: Record<string, unknown> = {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeTierCapableModels: useBuiltinFallback
|
||||
? unionModelIdLists([eligibility.capable, getAlibabaBuiltinFreeTierTextCapableModels()])
|
||||
: eligibility.capable,
|
||||
alibabaNoFreeTierModels: useBuiltinFallback
|
||||
? unionModelIdLists([eligibility.noFreeTier, getAlibabaBuiltinNoFreeTierTextModels()])
|
||||
: eligibility.noFreeTier,
|
||||
alibabaFreeDrainedModels: normalizeModelIdList(targetPsd.alibabaFreeDrainedModels),
|
||||
};
|
||||
|
||||
const syncAt =
|
||||
eligibility.quotaSyncAt ||
|
||||
getAlibabaFreeTierQuotaLastSyncAt(targetPsd) ||
|
||||
(useBuiltinFallback ? "builtin-allowlist" : null);
|
||||
if (syncAt) {
|
||||
merged.alibabaFreeTierQuotaLastSyncAt = syncAt;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function getAlibabaFreeTierVisionCapableModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(asRecord(providerSpecificData).alibabaFreeTierVisionCapableModels);
|
||||
}
|
||||
|
||||
export function getAlibabaFreeTierVisionDrainedModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(asRecord(providerSpecificData).alibabaFreeTierVisionDrainedModels);
|
||||
}
|
||||
|
||||
export function getAlibabaNoFreeTierVisionModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(asRecord(providerSpecificData).alibabaNoFreeTierVisionModels);
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierVisionCapableModel(
|
||||
modelId: string,
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): boolean {
|
||||
const noFreeTier = new Set(getAlibabaNoFreeTierVisionModels(providerSpecificData));
|
||||
if (noFreeTier.has(modelId)) return false;
|
||||
|
||||
const drained = new Set(getAlibabaFreeTierVisionDrainedModels(providerSpecificData));
|
||||
if (drained.has(modelId)) return false;
|
||||
|
||||
const capable = new Set(getAlibabaFreeTierVisionCapableModels(providerSpecificData));
|
||||
if (capable.has(modelId)) return true;
|
||||
|
||||
if (getAlibabaFreeTierQuotaLastSyncAt(providerSpecificData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isDashscopeVisionModelId(modelId);
|
||||
}
|
||||
|
||||
export function filterAlibabaFreeVisionEligibleModels(
|
||||
modelIds: readonly string[],
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return filterAlibabaFreeCategoryEligibleModels(
|
||||
modelIds,
|
||||
providerSpecificData,
|
||||
isDashscopeVisionModelId,
|
||||
getAlibabaFreeTierVisionCapableModels,
|
||||
getAlibabaFreeTierVisionDrainedModels,
|
||||
getAlibabaNoFreeTierVisionModels
|
||||
);
|
||||
}
|
||||
|
||||
function getAlibabaFreeTierMultimodalCapableModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(
|
||||
asRecord(providerSpecificData).alibabaFreeTierMultimodalCapableModels
|
||||
);
|
||||
}
|
||||
|
||||
function getAlibabaFreeTierMultimodalDrainedModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(
|
||||
asRecord(providerSpecificData).alibabaFreeTierMultimodalDrainedModels
|
||||
);
|
||||
}
|
||||
|
||||
function getAlibabaNoFreeTierMultimodalModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(asRecord(providerSpecificData).alibabaNoFreeTierMultimodalModels);
|
||||
}
|
||||
|
||||
function getAlibabaFreeTierAudioCapableModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(asRecord(providerSpecificData).alibabaFreeTierAudioCapableModels);
|
||||
}
|
||||
|
||||
function getAlibabaFreeTierAudioDrainedModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(asRecord(providerSpecificData).alibabaFreeTierAudioDrainedModels);
|
||||
}
|
||||
|
||||
function getAlibabaNoFreeTierAudioModels(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return normalizeModelIdList(asRecord(providerSpecificData).alibabaNoFreeTierAudioModels);
|
||||
}
|
||||
|
||||
function filterAlibabaFreeCategoryEligibleModels(
|
||||
modelIds: readonly string[],
|
||||
providerSpecificData: Record<string, unknown> | null | undefined,
|
||||
modelTypeCheck: (modelId: string) => boolean,
|
||||
getCapable: (psd: Record<string, unknown> | null | undefined) => string[],
|
||||
getDrained: (psd: Record<string, unknown> | null | undefined) => string[],
|
||||
getNoFreeTier: (psd: Record<string, unknown> | null | undefined) => string[]
|
||||
): string[] {
|
||||
const drained = new Set(getDrained(providerSpecificData));
|
||||
return modelIds.filter((id) => {
|
||||
if (!modelTypeCheck(id)) return false;
|
||||
if (drained.has(id)) return false;
|
||||
return isAlibabaFreeCategoryCapableModel(
|
||||
id,
|
||||
providerSpecificData,
|
||||
modelTypeCheck,
|
||||
getCapable,
|
||||
getDrained,
|
||||
getNoFreeTier
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function isAlibabaFreeCategoryCapableModel(
|
||||
modelId: string,
|
||||
providerSpecificData: Record<string, unknown> | null | undefined,
|
||||
modelTypeCheck: (modelId: string) => boolean,
|
||||
getCapable: (psd: Record<string, unknown> | null | undefined) => string[],
|
||||
getDrained: (psd: Record<string, unknown> | null | undefined) => string[],
|
||||
getNoFreeTier: (psd: Record<string, unknown> | null | undefined) => string[]
|
||||
): boolean {
|
||||
const noFreeTier = new Set(getNoFreeTier(providerSpecificData));
|
||||
if (noFreeTier.has(modelId)) return false;
|
||||
|
||||
const drained = new Set(getDrained(providerSpecificData));
|
||||
if (drained.has(modelId)) return false;
|
||||
|
||||
const capable = new Set(getCapable(providerSpecificData));
|
||||
if (capable.has(modelId)) return true;
|
||||
|
||||
if (getAlibabaFreeTierQuotaLastSyncAt(providerSpecificData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return modelTypeCheck(modelId);
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierMultimodalCapableModel(
|
||||
modelId: string,
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): boolean {
|
||||
return isAlibabaFreeCategoryCapableModel(
|
||||
modelId,
|
||||
providerSpecificData,
|
||||
isDashscopeMultimodalModelId,
|
||||
getAlibabaFreeTierMultimodalCapableModels,
|
||||
getAlibabaFreeTierMultimodalDrainedModels,
|
||||
getAlibabaNoFreeTierMultimodalModels
|
||||
);
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierAudioCapableModel(
|
||||
modelId: string,
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): boolean {
|
||||
return isAlibabaFreeCategoryCapableModel(
|
||||
modelId,
|
||||
providerSpecificData,
|
||||
isDashscopeAudioModelId,
|
||||
getAlibabaFreeTierAudioCapableModels,
|
||||
getAlibabaFreeTierAudioDrainedModels,
|
||||
getAlibabaNoFreeTierAudioModels
|
||||
);
|
||||
}
|
||||
|
||||
export function filterAlibabaFreeMultimodalEligibleModels(
|
||||
modelIds: readonly string[],
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return filterAlibabaFreeCategoryEligibleModels(
|
||||
modelIds,
|
||||
providerSpecificData,
|
||||
isDashscopeMultimodalModelId,
|
||||
getAlibabaFreeTierMultimodalCapableModels,
|
||||
getAlibabaFreeTierMultimodalDrainedModels,
|
||||
getAlibabaNoFreeTierMultimodalModels
|
||||
);
|
||||
}
|
||||
|
||||
export function filterAlibabaFreeAudioEligibleModels(
|
||||
modelIds: readonly string[],
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string[] {
|
||||
return filterAlibabaFreeCategoryEligibleModels(
|
||||
modelIds,
|
||||
providerSpecificData,
|
||||
isDashscopeAudioModelId,
|
||||
getAlibabaFreeTierAudioCapableModels,
|
||||
getAlibabaFreeTierAudioDrainedModels,
|
||||
getAlibabaNoFreeTierAudioModels
|
||||
);
|
||||
}
|
||||
519
open-sse/services/alibabaFreeTierQuotaFetcher.ts
Normal file
519
open-sse/services/alibabaFreeTierQuotaFetcher.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
/**
|
||||
* @file alibabaFreeTierQuotaFetcher.ts
|
||||
* @description Fetch Alibaba Model Studio free-tier quota from the Bailian console API.
|
||||
*
|
||||
* DashScope inference keys cannot list free-tier eligibility. The console exposes
|
||||
* `zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuotaAsyn` with per-model
|
||||
* `freeTierOnly`, `quotaStatus`, and `quotaTotal` fields.
|
||||
*
|
||||
* Auth: browser session cookie (`login_aliyunid_ticket` or full Cookie header) stored
|
||||
* on the connection as `providerSpecificData.alibabaConsoleCookie`.
|
||||
*
|
||||
* Parsing/classification/eligibility-filtering logic lives in
|
||||
* `alibabaFreeTierQuotaClassify.ts` and shared types/primitives in
|
||||
* `alibabaFreeTierQuotaTypes.ts` (split out to stay under the file-size cap); this file
|
||||
* re-exports their public API so existing imports of this module keep working
|
||||
* unchanged, and owns the HTTP/console-fetch flow itself.
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-25] [Composer] - Merge built-in text free-tier allowlist into filter context
|
||||
* - [2026-07-25] [Composer] - Propagate shared free-tier eligibility across all Alibaba free connections
|
||||
* - [2026-07-25] [Composer] - Add multimodal and audio free-quota classification and console fetch paths
|
||||
* - [2026-07-25] [Composer] - Add vision/media free-quota classification for alibabafreevision
|
||||
* - [2026-07-25] [Composer] - Add console free-tier quota fetcher for Alibaba Model Studio
|
||||
* - [2026-07-25] [Composer] - Use shared toNumberOrNull instead of local coercion helper
|
||||
* - [2026-08-05] - Split classification/eligibility logic into alibabaFreeTierQuotaClassify.ts
|
||||
* and alibabaFreeTierQuotaTypes.ts to stay under the file-size cap
|
||||
*/
|
||||
|
||||
import { getAlibabaBillingMode, isAlibabaModelStudioProvider } from "./alibabaFreeTier.ts";
|
||||
import {
|
||||
asRecord,
|
||||
getAlibabaFreeTierQuotaLastSyncAt,
|
||||
normalizeModelIdList,
|
||||
toTrimmedString,
|
||||
type AlibabaFreeTierQuotaEntry,
|
||||
type AlibabaFreeTierQuotaSnapshot,
|
||||
} from "./alibabaFreeTierQuotaTypes.ts";
|
||||
import {
|
||||
applyAlibabaSharedFreeTierEligibility,
|
||||
classifyAlibabaAudioFreeTierQuotaEntries,
|
||||
classifyAlibabaFreeTierQuotaEntries,
|
||||
classifyAlibabaMultimodalFreeTierQuotaEntries,
|
||||
classifyAlibabaVisionFreeTierQuotaEntries,
|
||||
extractAlibabaSharedFreeTierEligibility,
|
||||
parseAlibabaFreeTierQuotaEntries,
|
||||
} from "./alibabaFreeTierQuotaClassify.ts";
|
||||
|
||||
// Re-export the shared types + the classification/eligibility public API so existing
|
||||
// imports of this module (`from "./alibabaFreeTierQuotaFetcher.ts"`) keep working.
|
||||
export type {
|
||||
AlibabaFreeTierQuotaEntry,
|
||||
AlibabaFreeTierQuotaClassification,
|
||||
AlibabaFreeTierQuotaSnapshot,
|
||||
} from "./alibabaFreeTierQuotaTypes.ts";
|
||||
export { getAlibabaFreeTierQuotaLastSyncAt, isAlibabaLiveQuotaSyncAt } from "./alibabaFreeTierQuotaTypes.ts";
|
||||
export {
|
||||
isAlibabaQuotaValidityExpired,
|
||||
parseAlibabaFreeTierQuotaEntries,
|
||||
classifyAlibabaFreeTierQuotaEntry,
|
||||
classifyAlibabaVisionFreeTierQuotaEntry,
|
||||
classifyAlibabaVisionFreeTierQuotaEntries,
|
||||
classifyAlibabaMultimodalFreeTierQuotaEntries,
|
||||
classifyAlibabaAudioFreeTierQuotaEntries,
|
||||
classifyAlibabaFreeTierQuotaEntries,
|
||||
extractAlibabaSharedFreeTierEligibility,
|
||||
applyAlibabaSharedFreeTierEligibility,
|
||||
pickCanonicalAlibabaFreeTierConnection,
|
||||
buildAlibabaFreeVisionFilterContext,
|
||||
buildAlibabaFreeMultimodalFilterContext,
|
||||
buildAlibabaFreeAudioFilterContext,
|
||||
buildAlibabaFreeTierTextFilterContext,
|
||||
getAlibabaFreeTierVisionCapableModels,
|
||||
getAlibabaFreeTierVisionDrainedModels,
|
||||
getAlibabaNoFreeTierVisionModels,
|
||||
isAlibabaFreeTierVisionCapableModel,
|
||||
filterAlibabaFreeVisionEligibleModels,
|
||||
isAlibabaFreeTierMultimodalCapableModel,
|
||||
isAlibabaFreeTierAudioCapableModel,
|
||||
filterAlibabaFreeMultimodalEligibleModels,
|
||||
filterAlibabaFreeAudioEligibleModels,
|
||||
type AlibabaConnectionLike,
|
||||
type AlibabaFreeTierEligibilityFields,
|
||||
} from "./alibabaFreeTierQuotaClassify.ts";
|
||||
|
||||
const FREE_TIER_QUOTA_API = "zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuotaAsyn";
|
||||
const FREE_TIER_QUOTA_START_API = "zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuota";
|
||||
const DEFAULT_TEXT_FE_PATH = "/costing-balance/free-quota";
|
||||
const DEFAULT_VISION_FE_PATH =
|
||||
process.env.ALIBABA_FREE_TIER_VISION_FE_PATH?.trim() || "/costing-balance/free-quota-image-video";
|
||||
const DEFAULT_MULTIMODAL_FE_PATH =
|
||||
process.env.ALIBABA_FREE_TIER_MULTIMODAL_FE_PATH?.trim() ||
|
||||
"/costing-balance/free-quota-multimodal";
|
||||
const DEFAULT_AUDIO_FE_PATH =
|
||||
process.env.ALIBABA_FREE_TIER_AUDIO_FE_PATH?.trim() || "/costing-balance/free-quota-audio";
|
||||
|
||||
const CONSOLE_GATEWAYS = {
|
||||
"global-sg": {
|
||||
host: "https://bailian-singapore-cs.alibabacloud.com",
|
||||
region: "ap-southeast-1",
|
||||
action: "IntlBroadScopeAspnGateway",
|
||||
product: "sfm_bailian",
|
||||
},
|
||||
"china-beijing": {
|
||||
host: "https://bailian.console.aliyun.com",
|
||||
region: "cn-beijing",
|
||||
action: "BroadScopeAspnGateway",
|
||||
product: "sfm_bailian",
|
||||
},
|
||||
} as const;
|
||||
|
||||
type AlibabaProviderRegion = keyof typeof CONSOLE_GATEWAYS;
|
||||
|
||||
function resolveAlibabaConsoleRegion(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): AlibabaProviderRegion {
|
||||
const region = toTrimmedString(asRecord(providerSpecificData).region);
|
||||
return region === "china-beijing" ? "china-beijing" : "global-sg";
|
||||
}
|
||||
|
||||
export function normalizeAlibabaConsoleCookie(raw: unknown): string | null {
|
||||
const value = toTrimmedString(raw);
|
||||
if (!value) return null;
|
||||
|
||||
if (/login_aliyunid_ticket=/i.test(value) || value.includes(";")) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `login_aliyunid_ticket=${value}`;
|
||||
}
|
||||
|
||||
export function getAlibabaConsoleCookie(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string | null {
|
||||
const psd = asRecord(providerSpecificData);
|
||||
return (
|
||||
normalizeAlibabaConsoleCookie(psd.alibabaConsoleCookie) ||
|
||||
normalizeAlibabaConsoleCookie(psd.cookie) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function getAlibabaConsoleSecToken(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string | null {
|
||||
return toTrimmedString(asRecord(providerSpecificData).alibabaConsoleSecToken);
|
||||
}
|
||||
|
||||
export function hasAlibabaConsoleFreeTierAuth(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): boolean {
|
||||
return getAlibabaConsoleCookie(providerSpecificData) !== null;
|
||||
}
|
||||
|
||||
export function mergeAlibabaFreeTierQuotaClassification(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined,
|
||||
snapshot: AlibabaFreeTierQuotaSnapshot
|
||||
): Record<string, unknown> {
|
||||
const base = asRecord(providerSpecificData);
|
||||
|
||||
const coalesceList = (snapshotList: readonly string[], existingKey: string): string[] =>
|
||||
snapshotList.length > 0 ? [...snapshotList] : normalizeModelIdList(base[existingKey]);
|
||||
|
||||
return {
|
||||
...base,
|
||||
alibabaFreeTierCapableModels: coalesceList(
|
||||
snapshot.text.capableModels,
|
||||
"alibabaFreeTierCapableModels"
|
||||
),
|
||||
alibabaNoFreeTierModels: coalesceList(
|
||||
snapshot.text.noFreeTierModels,
|
||||
"alibabaNoFreeTierModels"
|
||||
),
|
||||
alibabaFreeDrainedModels: coalesceList(snapshot.text.drainedModels, "alibabaFreeDrainedModels"),
|
||||
alibabaFreeTierVisionCapableModels: coalesceList(
|
||||
snapshot.vision.capableModels,
|
||||
"alibabaFreeTierVisionCapableModels"
|
||||
),
|
||||
alibabaNoFreeTierVisionModels: coalesceList(
|
||||
snapshot.vision.noFreeTierModels,
|
||||
"alibabaNoFreeTierVisionModels"
|
||||
),
|
||||
alibabaFreeTierVisionDrainedModels: coalesceList(
|
||||
snapshot.vision.drainedModels,
|
||||
"alibabaFreeTierVisionDrainedModels"
|
||||
),
|
||||
alibabaFreeTierMultimodalCapableModels: coalesceList(
|
||||
snapshot.multimodal.capableModels,
|
||||
"alibabaFreeTierMultimodalCapableModels"
|
||||
),
|
||||
alibabaNoFreeTierMultimodalModels: coalesceList(
|
||||
snapshot.multimodal.noFreeTierModels,
|
||||
"alibabaNoFreeTierMultimodalModels"
|
||||
),
|
||||
alibabaFreeTierMultimodalDrainedModels: coalesceList(
|
||||
snapshot.multimodal.drainedModels,
|
||||
"alibabaFreeTierMultimodalDrainedModels"
|
||||
),
|
||||
alibabaFreeTierAudioCapableModels: coalesceList(
|
||||
snapshot.audio.capableModels,
|
||||
"alibabaFreeTierAudioCapableModels"
|
||||
),
|
||||
alibabaNoFreeTierAudioModels: coalesceList(
|
||||
snapshot.audio.noFreeTierModels,
|
||||
"alibabaNoFreeTierAudioModels"
|
||||
),
|
||||
alibabaFreeTierAudioDrainedModels: coalesceList(
|
||||
snapshot.audio.drainedModels,
|
||||
"alibabaFreeTierAudioDrainedModels"
|
||||
),
|
||||
alibabaFreeTierQuotaEntries: snapshot.entries,
|
||||
alibabaFreeTierVisionQuotaEntries: snapshot.vision.entries,
|
||||
alibabaFreeTierMultimodalQuotaEntries: snapshot.multimodal.entries,
|
||||
alibabaFreeTierAudioQuotaEntries: snapshot.audio.entries,
|
||||
alibabaFreeTierQuotaLastSyncAt: new Date().toISOString(),
|
||||
alibabaFreeTierDiscoverySource: "console-quota-api",
|
||||
};
|
||||
}
|
||||
|
||||
export async function propagateAlibabaFreeTierEligibilityToSiblings(
|
||||
provider: string,
|
||||
sourceConnectionId: string,
|
||||
mergedPsd: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
const shared = extractAlibabaSharedFreeTierEligibility(mergedPsd);
|
||||
if (!shared.alibabaFreeTierQuotaLastSyncAt) return;
|
||||
|
||||
const { getProviderConnections, updateProviderConnection } =
|
||||
await import("../../src/lib/db/providers.ts");
|
||||
const connections = await getProviderConnections({ provider });
|
||||
|
||||
for (const connection of connections) {
|
||||
if (connection.id === sourceConnectionId) continue;
|
||||
const psd = connection.providerSpecificData as Record<string, unknown> | null | undefined;
|
||||
if (getAlibabaBillingMode(psd) !== "free") continue;
|
||||
|
||||
const updated = applyAlibabaSharedFreeTierEligibility(
|
||||
asRecord(connection.providerSpecificData),
|
||||
shared
|
||||
);
|
||||
await updateProviderConnection(connection.id as string, { providerSpecificData: updated });
|
||||
}
|
||||
}
|
||||
|
||||
function buildGatewayUrl(region: AlibabaProviderRegion, api: string): string {
|
||||
const gateway = CONSOLE_GATEWAYS[region];
|
||||
const params = new URLSearchParams({
|
||||
action: gateway.action,
|
||||
product: gateway.product,
|
||||
api,
|
||||
_v: "undefined",
|
||||
});
|
||||
return `${gateway.host}/data/api.json?${params.toString()}`;
|
||||
}
|
||||
|
||||
function buildCornerstoneParam(
|
||||
region: AlibabaProviderRegion,
|
||||
fePath: string = DEFAULT_TEXT_FE_PATH
|
||||
): Record<string, unknown> {
|
||||
const gateway = CONSOLE_GATEWAYS[region];
|
||||
const normalizedPath = fePath.startsWith("/") ? fePath : `/${fePath}`;
|
||||
return {
|
||||
feTraceId: crypto.randomUUID(),
|
||||
feURL: `https://modelstudio.console.alibabacloud.com/${gateway.region}?tab=costing-balance#${normalizedPath}`,
|
||||
protocol: "V2",
|
||||
console: "ONE_CONSOLE",
|
||||
productCode: "p_efm",
|
||||
switchAgent: 416572,
|
||||
switchUserType: 3,
|
||||
domain: "modelstudio.console.alibabacloud.com",
|
||||
consoleSite: "MODELSTUDIO_ALBABACLOUD",
|
||||
userNickName: "",
|
||||
userPrincipalName: "",
|
||||
xsp_lang: "en-US",
|
||||
};
|
||||
}
|
||||
|
||||
function buildRequestBody(
|
||||
region: AlibabaProviderRegion,
|
||||
api: string,
|
||||
taskId?: string | null,
|
||||
fePath: string = DEFAULT_TEXT_FE_PATH
|
||||
): URLSearchParams {
|
||||
const gateway = CONSOLE_GATEWAYS[region];
|
||||
const request: Record<string, unknown> = {};
|
||||
if (taskId) {
|
||||
request.queryFreeTierQuotaRequest = { taskId };
|
||||
} else {
|
||||
request.queryFreeTierQuotaRequest = {};
|
||||
}
|
||||
request.cornerstoneParam = buildCornerstoneParam(region, fePath);
|
||||
|
||||
const body = new URLSearchParams({
|
||||
params: JSON.stringify({
|
||||
Api: api,
|
||||
V: "1.0",
|
||||
Data: request,
|
||||
}),
|
||||
region: gateway.region,
|
||||
});
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
async function postConsoleFreeTierQuota(
|
||||
region: AlibabaProviderRegion,
|
||||
api: string,
|
||||
cookie: string,
|
||||
secToken: string | null,
|
||||
taskId?: string | null,
|
||||
fePath: string = DEFAULT_TEXT_FE_PATH
|
||||
): Promise<unknown> {
|
||||
const body = buildRequestBody(region, api, taskId, fePath);
|
||||
if (secToken) {
|
||||
body.set("sec_token", secToken);
|
||||
}
|
||||
|
||||
const response = await fetch(buildGatewayUrl(region, api), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "*/*",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: cookie,
|
||||
Origin: "https://modelstudio.console.alibabacloud.com",
|
||||
Referer: "https://modelstudio.console.alibabacloud.com/",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
|
||||
},
|
||||
body: body.toString(),
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function extractTaskId(payload: unknown): string | null {
|
||||
const root = asRecord(payload);
|
||||
const dataV2 = asRecord(asRecord(root.data).DataV2 ?? root.DataV2);
|
||||
const inner = asRecord(dataV2.data);
|
||||
const payloadData = asRecord(inner.data ?? inner);
|
||||
return toTrimmedString(payloadData.taskId);
|
||||
}
|
||||
|
||||
function hasQuotaPayload(payload: unknown): boolean {
|
||||
return parseAlibabaFreeTierQuotaEntries(payload).length > 0;
|
||||
}
|
||||
|
||||
async function delay(ms: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function fetchAlibabaFreeTierQuotaEntriesForPath(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined,
|
||||
fePath: string
|
||||
): Promise<AlibabaFreeTierQuotaEntry[] | null> {
|
||||
const cookie = getAlibabaConsoleCookie(providerSpecificData);
|
||||
if (!cookie) return null;
|
||||
|
||||
const region = resolveAlibabaConsoleRegion(providerSpecificData);
|
||||
const secToken = getAlibabaConsoleSecToken(providerSpecificData);
|
||||
|
||||
let payload = await postConsoleFreeTierQuota(
|
||||
region,
|
||||
FREE_TIER_QUOTA_START_API,
|
||||
cookie,
|
||||
secToken,
|
||||
null,
|
||||
fePath
|
||||
);
|
||||
|
||||
if (!hasQuotaPayload(payload)) {
|
||||
payload = await postConsoleFreeTierQuota(
|
||||
region,
|
||||
FREE_TIER_QUOTA_API,
|
||||
cookie,
|
||||
secToken,
|
||||
null,
|
||||
fePath
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasQuotaPayload(payload)) {
|
||||
const taskId = extractTaskId(payload);
|
||||
if (!taskId) return null;
|
||||
|
||||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||
if (attempt > 0) {
|
||||
await delay(400);
|
||||
}
|
||||
payload = await postConsoleFreeTierQuota(
|
||||
region,
|
||||
FREE_TIER_QUOTA_API,
|
||||
cookie,
|
||||
secToken,
|
||||
taskId,
|
||||
fePath
|
||||
);
|
||||
if (hasQuotaPayload(payload)) break;
|
||||
}
|
||||
}
|
||||
|
||||
const entries = parseAlibabaFreeTierQuotaEntries(payload);
|
||||
return entries.length > 0 ? entries : null;
|
||||
}
|
||||
|
||||
export async function fetchAlibabaFreeTierQuotaEntries(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): Promise<AlibabaFreeTierQuotaEntry[] | null> {
|
||||
return fetchAlibabaFreeTierQuotaEntriesForPath(providerSpecificData, DEFAULT_TEXT_FE_PATH);
|
||||
}
|
||||
|
||||
export async function fetchAlibabaFreeTierVisionQuotaEntries(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): Promise<AlibabaFreeTierQuotaEntry[] | null> {
|
||||
return fetchAlibabaFreeTierQuotaEntriesForPath(providerSpecificData, DEFAULT_VISION_FE_PATH);
|
||||
}
|
||||
|
||||
export async function fetchAlibabaFreeTierMultimodalQuotaEntries(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): Promise<AlibabaFreeTierQuotaEntry[] | null> {
|
||||
return fetchAlibabaFreeTierQuotaEntriesForPath(providerSpecificData, DEFAULT_MULTIMODAL_FE_PATH);
|
||||
}
|
||||
|
||||
export async function fetchAlibabaFreeTierAudioQuotaEntries(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): Promise<AlibabaFreeTierQuotaEntry[] | null> {
|
||||
return fetchAlibabaFreeTierQuotaEntriesForPath(providerSpecificData, DEFAULT_AUDIO_FE_PATH);
|
||||
}
|
||||
|
||||
function mergeUniqueQuotaEntries(
|
||||
...entryGroups: Array<readonly AlibabaFreeTierQuotaEntry[]>
|
||||
): AlibabaFreeTierQuotaEntry[] {
|
||||
const merged: AlibabaFreeTierQuotaEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const group of entryGroups) {
|
||||
for (const entry of group) {
|
||||
if (seen.has(entry.model)) continue;
|
||||
seen.add(entry.model);
|
||||
merged.push(entry);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function buildAlibabaFreeTierQuotaSnapshot(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): Promise<AlibabaFreeTierQuotaSnapshot | null> {
|
||||
const textEntries = await fetchAlibabaFreeTierQuotaEntries(providerSpecificData);
|
||||
if (!textEntries) return null;
|
||||
|
||||
const visionEntries =
|
||||
(await fetchAlibabaFreeTierVisionQuotaEntries(providerSpecificData)) || textEntries;
|
||||
const multimodalEntries =
|
||||
(await fetchAlibabaFreeTierMultimodalQuotaEntries(providerSpecificData)) || textEntries;
|
||||
const audioEntries =
|
||||
(await fetchAlibabaFreeTierAudioQuotaEntries(providerSpecificData)) || textEntries;
|
||||
|
||||
const text = classifyAlibabaFreeTierQuotaEntries(textEntries, { textOnly: true });
|
||||
const vision = classifyAlibabaVisionFreeTierQuotaEntries(visionEntries);
|
||||
const multimodal = classifyAlibabaMultimodalFreeTierQuotaEntries(multimodalEntries);
|
||||
const audio = classifyAlibabaAudioFreeTierQuotaEntries(audioEntries);
|
||||
|
||||
return {
|
||||
text,
|
||||
vision,
|
||||
multimodal,
|
||||
audio,
|
||||
entries: mergeUniqueQuotaEntries(textEntries, visionEntries, multimodalEntries, audioEntries),
|
||||
};
|
||||
}
|
||||
|
||||
export async function refreshAlibabaFreeTierQuotaClassification(
|
||||
provider: string,
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
if (
|
||||
!isAlibabaModelStudioProvider(provider) ||
|
||||
getAlibabaBillingMode(providerSpecificData) !== "free"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!hasAlibabaConsoleFreeTierAuth(providerSpecificData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot = await buildAlibabaFreeTierQuotaSnapshot(providerSpecificData);
|
||||
if (!snapshot) return null;
|
||||
|
||||
return mergeAlibabaFreeTierQuotaClassification(providerSpecificData, snapshot);
|
||||
}
|
||||
|
||||
type QuotaConnection = {
|
||||
id: string;
|
||||
providerSpecificData?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export function scheduleAlibabaFreeTierQuotaRefresh(
|
||||
provider: string,
|
||||
connection: QuotaConnection
|
||||
): void {
|
||||
if (!hasAlibabaConsoleFreeTierAuth(connection.providerSpecificData)) return;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const merged = await refreshAlibabaFreeTierQuotaClassification(
|
||||
provider,
|
||||
connection.providerSpecificData
|
||||
);
|
||||
if (!merged) return;
|
||||
const { updateProviderConnection } = await import("../../src/lib/db/providers.ts");
|
||||
await updateProviderConnection(connection.id, { providerSpecificData: merged });
|
||||
await propagateAlibabaFreeTierEligibilityToSiblings(provider, connection.id, merged);
|
||||
} catch (error) {
|
||||
console.warn("[alibaba-free-tier] console quota refresh failed", {
|
||||
connectionId: connection.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
58
open-sse/services/alibabaFreeTierQuotaTypes.ts
Normal file
58
open-sse/services/alibabaFreeTierQuotaTypes.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @file alibabaFreeTierQuotaTypes.ts
|
||||
* @description Shared types + small primitive helpers for the Alibaba free-tier quota
|
||||
* fetcher/classifier split (extracted from alibabaFreeTierQuotaFetcher.ts to keep that
|
||||
* file under the file-size cap; behavior is unchanged).
|
||||
*/
|
||||
|
||||
export type AlibabaFreeTierQuotaEntry = {
|
||||
model: string;
|
||||
freeTierOnly: boolean;
|
||||
quotaStatus: string;
|
||||
quotaTotal?: number;
|
||||
quotaInitTotal?: number;
|
||||
quotaTotalPercentage?: number;
|
||||
quotaValidityPeriod?: number;
|
||||
};
|
||||
|
||||
export type AlibabaFreeTierQuotaClassification = {
|
||||
capableModels: string[];
|
||||
noFreeTierModels: string[];
|
||||
drainedModels: string[];
|
||||
entries: AlibabaFreeTierQuotaEntry[];
|
||||
};
|
||||
|
||||
export type AlibabaFreeTierQuotaSnapshot = {
|
||||
text: AlibabaFreeTierQuotaClassification;
|
||||
vision: AlibabaFreeTierQuotaClassification;
|
||||
multimodal: AlibabaFreeTierQuotaClassification;
|
||||
audio: AlibabaFreeTierQuotaClassification;
|
||||
entries: AlibabaFreeTierQuotaEntry[];
|
||||
};
|
||||
|
||||
export function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
export function toTrimmedString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
export function normalizeModelIdList(raw: unknown): string[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
||||
}
|
||||
|
||||
export function getAlibabaFreeTierQuotaLastSyncAt(
|
||||
providerSpecificData: Record<string, unknown> | null | undefined
|
||||
): string | null {
|
||||
return toTrimmedString(asRecord(providerSpecificData).alibabaFreeTierQuotaLastSyncAt);
|
||||
}
|
||||
|
||||
/** True when the connection has a live console/API quota snapshot (not builtin fallback). */
|
||||
export function isAlibabaLiveQuotaSyncAt(syncAt: string | null | undefined): boolean {
|
||||
if (!syncAt || syncAt === "builtin-allowlist") return false;
|
||||
return Number.isFinite(Date.parse(syncAt));
|
||||
}
|
||||
@@ -32,6 +32,26 @@
|
||||
import { wildcardMatch } from "../wildcardRouter.ts";
|
||||
import { getProviderModels } from "../../config/providerModels.ts";
|
||||
import { getActiveSyncedCatalog } from "../../../src/lib/db/models/activeSyncedCatalog.ts";
|
||||
import { filterAlibabaFreeTierModels, isAlibabaModelStudioProvider } from "../alibabaFreeTier.ts";
|
||||
import {
|
||||
filterAlibabaFreeEligibleModels,
|
||||
buildAlibabaFreeTierFilterContext,
|
||||
} from "../alibabaFreeTierDiscovery.ts";
|
||||
import {
|
||||
buildAlibabaFreeAudioFilterContext,
|
||||
buildAlibabaFreeMultimodalFilterContext,
|
||||
buildAlibabaFreeVisionFilterContext,
|
||||
filterAlibabaFreeAudioEligibleModels,
|
||||
filterAlibabaFreeMultimodalEligibleModels,
|
||||
filterAlibabaFreeVisionEligibleModels,
|
||||
} from "../alibabaFreeTierQuotaFetcher.ts";
|
||||
import type { AlibabaConnectionLike } from "../alibabaFreeTierQuotaFetcher.ts";
|
||||
import {
|
||||
isAlibabaFreeTierAudioComboName,
|
||||
isAlibabaFreeTierMultimodalComboName,
|
||||
isAlibabaFreeTierTextComboName,
|
||||
isAlibabaFreeTierVisionComboName,
|
||||
} from "../dashscopeTextModels.ts";
|
||||
import type { ComboLike } from "./types.ts";
|
||||
|
||||
/** Sentinel pattern used for "all models of a provider". */
|
||||
@@ -130,6 +150,61 @@ async function collectProviderModelIds(providerId: string): Promise<string[]> {
|
||||
return getProviderModels(providerId).map((model) => model.id);
|
||||
}
|
||||
|
||||
async function filterAlibabaFreeDrainedModelIds(
|
||||
providerId: string,
|
||||
modelIds: string[],
|
||||
connectionId: string | null,
|
||||
comboName: string
|
||||
): Promise<string[]> {
|
||||
if (!isAlibabaModelStudioProvider(providerId) || !connectionId) {
|
||||
return modelIds;
|
||||
}
|
||||
try {
|
||||
const { getProviderConnections } = await import("../../../src/lib/db/providers.ts");
|
||||
const connections = await getProviderConnections({ provider: providerId });
|
||||
const connection = connections.find((entry) => entry.id === connectionId);
|
||||
if (!connection) return modelIds;
|
||||
|
||||
if (isAlibabaFreeTierVisionComboName(comboName)) {
|
||||
return filterAlibabaFreeVisionEligibleModels(
|
||||
modelIds,
|
||||
buildAlibabaFreeVisionFilterContext(
|
||||
connections as unknown as readonly AlibabaConnectionLike[],
|
||||
connectionId
|
||||
)
|
||||
);
|
||||
}
|
||||
if (isAlibabaFreeTierMultimodalComboName(comboName)) {
|
||||
return filterAlibabaFreeMultimodalEligibleModels(
|
||||
modelIds,
|
||||
buildAlibabaFreeMultimodalFilterContext(
|
||||
connections as unknown as readonly AlibabaConnectionLike[],
|
||||
connectionId
|
||||
)
|
||||
);
|
||||
}
|
||||
if (isAlibabaFreeTierAudioComboName(comboName)) {
|
||||
return filterAlibabaFreeAudioEligibleModels(
|
||||
modelIds,
|
||||
buildAlibabaFreeAudioFilterContext(
|
||||
connections as unknown as readonly AlibabaConnectionLike[],
|
||||
connectionId
|
||||
)
|
||||
);
|
||||
}
|
||||
return filterAlibabaFreeEligibleModels(
|
||||
modelIds,
|
||||
buildAlibabaFreeTierFilterContext(
|
||||
connections as unknown as readonly AlibabaConnectionLike[],
|
||||
connectionId
|
||||
),
|
||||
{ strictAllowlist: isAlibabaFreeTierTextComboName(comboName) }
|
||||
);
|
||||
} catch {
|
||||
return modelIds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a single provider-wildcard spec into concrete model entry objects
|
||||
* that `normalizeComboStep` can process as normal model steps.
|
||||
@@ -141,9 +216,16 @@ async function expandWildcardSpec(
|
||||
spec: ProviderWildcardSpec,
|
||||
comboName: string
|
||||
): Promise<unknown[] | null> {
|
||||
const modelIds = await collectProviderModelIds(spec.providerId);
|
||||
let modelIds = await collectProviderModelIds(spec.providerId);
|
||||
if (modelIds.length === 0) return null;
|
||||
|
||||
modelIds = await filterAlibabaFreeDrainedModelIds(
|
||||
spec.providerId,
|
||||
modelIds,
|
||||
spec.connectionId,
|
||||
comboName
|
||||
);
|
||||
|
||||
const pattern = spec.modelPattern;
|
||||
const matchingIds =
|
||||
pattern === PROVIDER_WILDCARD_SENTINEL
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
hasPerModelQuota,
|
||||
isProviderExhaustedReason,
|
||||
} from "../accountFallback.ts";
|
||||
import {
|
||||
isAlibabaFreeQuotaExhaustedError,
|
||||
isAlibabaModelStudioProvider,
|
||||
} from "../alibabaFreeTier.ts";
|
||||
import { RateLimitReason } from "../../config/constants.ts";
|
||||
import { isProviderCircuitOpenResult, isRequestScopedUpstreamFailure } from "./comboPredicates.ts";
|
||||
import { isCloudflareFingerprintRejection } from "../errorClassifier.ts";
|
||||
@@ -117,6 +121,14 @@ export function applyComboTargetExhaustion(
|
||||
provider &&
|
||||
provider !== "unknown"
|
||||
) {
|
||||
// Alibaba free-tier drain is model-scoped — the connection and sibling models stay eligible.
|
||||
if (
|
||||
result.status === 403 &&
|
||||
isAlibabaModelStudioProvider(provider) &&
|
||||
isAlibabaFreeQuotaExhaustedError(opts.errorText)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
markAuthLevelExhaustion(target, { result, sets, log, tag });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -161,6 +161,12 @@ async function isTargetSelectableForWeighted(
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (target.provider && rawModel && target.connectionId) {
|
||||
const { isAlibabaFreeTierModelRoutable } = await import("../alibabaFreeTier.ts");
|
||||
if (!(await isAlibabaFreeTierModelRoutable(target.provider, target.connectionId, rawModel))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return isModelAvailable ? await isModelAvailable(target.modelStr, target) : true;
|
||||
}
|
||||
|
||||
|
||||
109
open-sse/services/dashscopeTextModels.ts
Normal file
109
open-sse/services/dashscopeTextModels.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @file dashscopeTextModels.ts
|
||||
* @description DashScope / Alibaba Model Studio text and vision model ID heuristics.
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-25] [Composer] - Add alibabafree text combo name detection for strict allowlist routing
|
||||
* - [2026-07-25] [Composer] - Add multimodal and audio model detection for free-tier combos
|
||||
* - [2026-07-25] [Composer] - Add vision/media model detection for alibabafreevision
|
||||
* - [2026-07-25] [Composer] - Extract DashScope text-model filter for open-sse consumers
|
||||
*/
|
||||
|
||||
const DASHSCOPE_TEXT_MODEL_PREFIXES = [
|
||||
"qwen",
|
||||
"qwq-",
|
||||
"deepseek-",
|
||||
"glm-",
|
||||
"kimi-",
|
||||
"minimax-",
|
||||
] as const;
|
||||
|
||||
const DASHSCOPE_VISION_MODEL_PREFIXES = ["wan", "qwen-image", "happyhorse", "z-image"] as const;
|
||||
|
||||
const DASHSCOPE_NON_TEXT_MODEL_TOKEN =
|
||||
/(?:^|[-_.\/])(?:asr|audio|captioner|embedding|image|livetranslate|omni|ocr|realtime|rerank|s2s|speech|tts|video|vl)(?:$|[-_.\/])/i;
|
||||
|
||||
const DASHSCOPE_VISION_MODEL_TOKEN =
|
||||
/(?:^|[-_.\/])(?:i2v|t2v|r2v|vace|kf2v|videoedit|animate|image-edit)(?:$|[-_.\/])/i;
|
||||
|
||||
export function isDashscopeTextModelId(value: unknown): boolean {
|
||||
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 function isDashscopeVisionModelId(value: unknown): value is string {
|
||||
if (typeof value !== "string") return false;
|
||||
const modelId = value.trim().toLowerCase();
|
||||
if (!modelId || isDashscopeTextModelId(modelId)) return false;
|
||||
return (
|
||||
DASHSCOPE_VISION_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix)) ||
|
||||
DASHSCOPE_VISION_MODEL_TOKEN.test(modelId)
|
||||
);
|
||||
}
|
||||
|
||||
const DASHSCOPE_AUDIO_PREFIXES = [
|
||||
"cosyvoice",
|
||||
"fun-asr",
|
||||
"qwen-audio",
|
||||
"qwen-voice",
|
||||
"voice-enrollment",
|
||||
] as const;
|
||||
|
||||
const DASHSCOPE_AUDIO_MODEL_TOKEN =
|
||||
/(?:^|[-_.\/])(?:asr|tts|livetranslate|captioner|speech|voice-design|voice-enrollment)(?:$|[-_.\/])/i;
|
||||
|
||||
const DASHSCOPE_MULTIMODAL_MODEL_TOKEN = /(?:^|[-_.\/])omni(?:$|[-_.\/])/i;
|
||||
|
||||
export function isDashscopeAudioModelId(value: unknown): value is string {
|
||||
if (typeof value !== "string") return false;
|
||||
const modelId = value.trim().toLowerCase();
|
||||
if (!modelId) return false;
|
||||
return (
|
||||
DASHSCOPE_AUDIO_PREFIXES.some((prefix) => modelId.startsWith(prefix)) ||
|
||||
DASHSCOPE_AUDIO_MODEL_TOKEN.test(modelId)
|
||||
);
|
||||
}
|
||||
|
||||
export function isDashscopeMultimodalModelId(value: unknown): value is string {
|
||||
if (typeof value !== "string") return false;
|
||||
const modelId = value.trim().toLowerCase();
|
||||
if (!modelId || isDashscopeAudioModelId(modelId) || isDashscopeVisionModelId(modelId)) {
|
||||
return false;
|
||||
}
|
||||
return DASHSCOPE_MULTIMODAL_MODEL_TOKEN.test(modelId);
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierTextComboName(comboName: string | null | undefined): boolean {
|
||||
if (!comboName) return false;
|
||||
const normalized = comboName.trim().toLowerCase();
|
||||
if (
|
||||
isAlibabaFreeTierVisionComboName(normalized) ||
|
||||
isAlibabaFreeTierMultimodalComboName(normalized) ||
|
||||
isAlibabaFreeTierAudioComboName(normalized)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return normalized === "alibabafree" || normalized.endsWith("alibabafree");
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierVisionComboName(comboName: string | null | undefined): boolean {
|
||||
if (!comboName) return false;
|
||||
const normalized = comboName.trim().toLowerCase();
|
||||
return normalized === "alibabafreevision" || normalized.endsWith("freevision");
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierMultimodalComboName(
|
||||
comboName: string | null | undefined
|
||||
): boolean {
|
||||
if (!comboName) return false;
|
||||
const normalized = comboName.trim().toLowerCase();
|
||||
return normalized === "alibabafreemultimodal" || normalized.endsWith("freemultimodal");
|
||||
}
|
||||
|
||||
export function isAlibabaFreeTierAudioComboName(comboName: string | null | undefined): boolean {
|
||||
if (!comboName) return false;
|
||||
const normalized = comboName.trim().toLowerCase();
|
||||
return normalized === "alibabafreeaudio" || normalized.endsWith("freeaudio");
|
||||
}
|
||||
@@ -251,7 +251,8 @@
|
||||
"release:contributors": "node scripts/release/gen-contributors.mjs",
|
||||
"release:uncovered": "node scripts/release/list-uncovered-commits.mjs",
|
||||
"test:coverage:runner": "node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial",
|
||||
"test:unit:serial": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 \"tests/unit/serial/**/*.test.ts\""
|
||||
"test:unit:serial": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 \"tests/unit/serial/**/*.test.ts\"",
|
||||
"alibaba:sync-allowlist": "node --import tsx/esm scripts/ops/sync-alibaba-allowlist.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.1073.0",
|
||||
|
||||
427
scripts/ops/alibabafreeaudio-quota.sample.json
Normal file
427
scripts/ops/alibabafreeaudio-quota.sample.json
Normal file
@@ -0,0 +1,427 @@
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"DataV2": {
|
||||
"data": {
|
||||
"data": {
|
||||
"freeTierQuotas": [
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-asr-flash-2025-09-08",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-omni-30b-a3b-captioner",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-vd-2026-01-26",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-vc-realtime-2026-01-15",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "cosyvoice-v3-flash",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-flash-realtime",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-voice-enrollment",
|
||||
"quotaTotal": 1000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "fun-asr-2025-08-25",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "fun-asr-mtl",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "fun-asr-realtime-2025-11-07",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-flash-2025-09-18",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-flash",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-flash-realtime-2025-09-18",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-asr-flash-realtime-2026-02-10",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-livetranslate-flash-realtime-2025-09-22",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-vc-2026-01-22",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1791820800000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-audio-3.0-tts-flash",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-instruct-flash-2026-01-26",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "fun-asr-2025-11-07",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-flash-2025-11-27",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-vc-realtime-2025-11-27",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-asr-flash-realtime-2025-10-27",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-asr-flash-realtime",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-vd-realtime-2025-12-16",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "cosyvoice-v3-plus",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-instruct-flash-realtime-2026-01-22",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-asr-flash-2026-02-10",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-instruct-flash",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-instruct-flash-realtime",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "fun-asr-mtl-2025-08-25",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1791820800000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-audio-3.0-tts-plus",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-vd-realtime-2026-01-15",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-livetranslate-flash",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "fun-asr-realtime",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-asr-flash-filetrans",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-asr-flash-filetrans-2025-11-17",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-livetranslate-flash-realtime",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-tts-flash-realtime-2025-11-27",
|
||||
"quotaTotal": 10000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-livetranslate-flash-realtime-2026-05-19",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "fun-asr",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-livetranslate-flash-realtime",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-livetranslate-flash-2025-12-01",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-asr-flash",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 36000,
|
||||
"quotaValidityPeriod": 1789488000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "fun-asr-flash-2026-06-15",
|
||||
"quotaTotal": 36000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 40,
|
||||
"model": "qwen-voice-design",
|
||||
"quotaTotal": 4,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 0,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 0,
|
||||
"model": "voice-enrollment",
|
||||
"quotaTotal": 0,
|
||||
"quotaStatus": "VALID"
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
201
scripts/ops/alibabafreemultimodal-quota.sample.json
Normal file
201
scripts/ops/alibabafreemultimodal-quota.sample.json
Normal file
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"DataV2": {
|
||||
"data": {
|
||||
"data": {
|
||||
"freeTierQuotas": [
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-omni-plus-2026-03-15",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-omni-flash-realtime-2025-09-15",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-omni-flash-realtime",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-omni-turbo-realtime-2025-05-08",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-omni-flash-realtime-2026-03-15",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-omni-plus",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-omni-flash",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-omni-flash-2025-12-01",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-omni-turbo-realtime",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-omni-turbo",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen2.5-omni-7b",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-omni-plus-realtime",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-omni-turbo-2025-03-26",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-omni-flash",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-omni-flash-realtime-2025-12-01",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-omni-plus-realtime-2026-03-15",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-omni-flash-realtime",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3.5-omni-flash-2026-03-15",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 1000000,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": true,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen3-omni-flash-2025-09-15",
|
||||
"quotaTotal": 1000000,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 0,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 0,
|
||||
"model": "qwen-omni-turbo-realtime-latest",
|
||||
"quotaTotal": 0,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 0,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 0,
|
||||
"model": "qwen-omni-turbo-latest",
|
||||
"quotaTotal": 0,
|
||||
"quotaStatus": "VALID"
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
573
scripts/ops/alibabafreevision-quota.sample.json
Normal file
573
scripts/ops/alibabafreevision-quota.sample.json
Normal file
@@ -0,0 +1,573 @@
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"DataV2": {
|
||||
"ret": ["SUCCESS::接口调用成功"],
|
||||
"data": {
|
||||
"data": {
|
||||
"freeTierQuotas": [
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.1-vace-plus",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-videoedit",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 200,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.1-kf2v-plus",
|
||||
"quotaTotal": 200,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-edit-plus",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10,
|
||||
"quotaValidityPeriod": 1789920000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "happyhorse-1.1-i2v",
|
||||
"quotaTotal": 10,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-edit-plus-2025-10-30",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-i2v",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.6-image",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.6-t2v",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-edit",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.2-t2v-plus",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-2.0",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-t2v",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "z-image-turbo",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-max-2025-12-30",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-r2v",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.2-animate-move",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-edit-max-2026-01-16",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.6-i2v",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.5-t2v-preview",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 200,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.1-t2v-plus",
|
||||
"quotaTotal": 200,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "happyhorse-1.0-t2v",
|
||||
"quotaTotal": 10,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "happyhorse-1.0-r2v",
|
||||
"quotaTotal": 10,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.5-t2i-preview",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1790092800000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-2.0-pro-2026-06-22",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 200,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.1-i2v-turbo",
|
||||
"quotaTotal": 200,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 200,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.1-t2v-turbo",
|
||||
"quotaTotal": 200,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 200,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.1-t2i-turbo",
|
||||
"quotaTotal": 200,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "happyhorse-1.0-video-edit",
|
||||
"quotaTotal": 10,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10,
|
||||
"quotaValidityPeriod": 1789920000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "happyhorse-1.1-r2v",
|
||||
"quotaTotal": 10,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-i2v-2026-04-25",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10,
|
||||
"quotaValidityPeriod": 1789920000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "happyhorse-1.1-t2v",
|
||||
"quotaTotal": 10,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.5-i2v-preview",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.2-t2i-flash",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.2-i2v-plus",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.2-t2i-plus",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.2-i2v-flash",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1790697600000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-t2v-2026-06-12",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1790697600000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-r2v-2026-06-12",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-2.0-2026-03-03",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-edit-max",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-max",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-2.0-pro-2026-03-03",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.5-i2i-preview",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-plus",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-2.0-pro",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-plus-2026-01-09",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 200,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.1-i2v-plus",
|
||||
"quotaTotal": 200,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-image-pro",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.6-r2v",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 200,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.1-t2i-plus",
|
||||
"quotaTotal": 200,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.2-animate-mix",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.6-t2i",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-2.0-pro-2026-04-22",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.6-i2v-flash",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-t2v-2026-04-25",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 100,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "qwen-image-edit-plus-2025-12-15",
|
||||
"quotaTotal": 100,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.6-r2v-flash",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 10,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "happyhorse-1.0-i2v",
|
||||
"quotaTotal": 10,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 50,
|
||||
"quotaValidityPeriod": 1786896000000,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 100,
|
||||
"model": "wan2.7-image",
|
||||
"quotaTotal": 50,
|
||||
"quotaStatus": "VALID"
|
||||
},
|
||||
{
|
||||
"quotaInitTotal": 0,
|
||||
"freeTierOnly": false,
|
||||
"quotaTotalPercentage": 0,
|
||||
"model": "qwen-image-3.0-pro",
|
||||
"quotaTotal": 0,
|
||||
"quotaStatus": "VALID"
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
101
scripts/ops/sync-alibaba-allowlist.mjs
Normal file
101
scripts/ops/sync-alibaba-allowlist.mjs
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @file sync-alibaba-allowlist.mjs
|
||||
* @description Build config/alibaba-free-tier-allowlist.json from Bailian console quota JSON exports.
|
||||
*
|
||||
* Usage:
|
||||
* node --import tsx/esm scripts/ops/sync-alibaba-allowlist.mjs path/to/quota.json [...]
|
||||
* node --import tsx/esm scripts/ops/sync-alibaba-allowlist.mjs --from-samples
|
||||
*
|
||||
* Writes:
|
||||
* - config/alibaba-free-tier-allowlist.json (repo baseline)
|
||||
* - ~/.omniroute/alibaba-free-tier-allowlist.json when DATA_DIR unset
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
classifyAlibabaFreeTierQuotaEntries,
|
||||
parseAlibabaFreeTierQuotaEntries,
|
||||
} from "../../open-sse/services/alibabaFreeTierQuotaFetcher.ts";
|
||||
import { isDashscopeTextModelId } from "../../open-sse/services/dashscopeTextModels.ts";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, "../..");
|
||||
|
||||
const SAMPLE_FILES = [
|
||||
"scripts/ops/alibabafreeaudio-quota.sample.json",
|
||||
"scripts/ops/alibabafreemultimodal-quota.sample.json",
|
||||
"scripts/ops/alibabafreevision-quota.sample.json",
|
||||
].map((relativePath) => path.join(repoRoot, relativePath));
|
||||
|
||||
function readJsonFile(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function collectInputs(argv) {
|
||||
if (argv.includes("--from-samples")) {
|
||||
return SAMPLE_FILES.filter((filePath) => fs.existsSync(filePath));
|
||||
}
|
||||
return argv.filter((arg) => !arg.startsWith("-"));
|
||||
}
|
||||
|
||||
function classifyTextEntries(allEntries) {
|
||||
const capable = new Set();
|
||||
const noFreeTier = new Set();
|
||||
|
||||
for (const entry of allEntries) {
|
||||
if (!isDashscopeTextModelId(entry.model)) continue;
|
||||
const classified = classifyAlibabaFreeTierQuotaEntries([entry], { textOnly: true });
|
||||
for (const modelId of classified.capableModels) capable.add(modelId);
|
||||
for (const modelId of classified.noFreeTierModels) noFreeTier.add(modelId);
|
||||
}
|
||||
|
||||
return {
|
||||
capable: [...capable].sort(),
|
||||
noFreeTier: [...noFreeTier].sort(),
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const inputs = collectInputs(process.argv.slice(2));
|
||||
if (inputs.length === 0) {
|
||||
console.error("Usage: sync-alibaba-allowlist.mjs <quota.json> [...] | --from-samples");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const allEntries = [];
|
||||
for (const inputPath of inputs) {
|
||||
const payload = readJsonFile(inputPath);
|
||||
allEntries.push(...parseAlibabaFreeTierQuotaEntries(payload));
|
||||
}
|
||||
|
||||
const { capable, noFreeTier } = classifyTextEntries(allEntries);
|
||||
if (capable.length === 0) {
|
||||
console.error("No text free-tier models found in input payloads.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const asOf = new Date().toISOString().slice(0, 10);
|
||||
const validUntil = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
||||
const pack = { asOf, validUntil, capable, noFreeTier };
|
||||
const serialized = `${JSON.stringify(pack, null, 2)}\n`;
|
||||
|
||||
const configPath = path.join(repoRoot, "config", "alibaba-free-tier-allowlist.json");
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, serialized);
|
||||
|
||||
const dataDir = process.env.DATA_DIR?.trim() || path.join(os.homedir(), ".omniroute");
|
||||
const runtimePath = path.join(dataDir, "alibaba-free-tier-allowlist.json");
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(runtimePath, serialized);
|
||||
|
||||
console.log(`Wrote ${capable.length} capable + ${noFreeTier.length} blocked models`);
|
||||
console.log(` config: ${configPath}`);
|
||||
console.log(` runtime: ${runtimePath}`);
|
||||
console.log(` validUntil: ${validUntil}`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
44
tests/unit/alibaba-free-tier-allowlist.test.ts
Normal file
44
tests/unit/alibaba-free-tier-allowlist.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file alibaba-free-tier-allowlist.test.ts
|
||||
* @description Tests for built-in Alibaba free-tier text model allowlists.
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-25] [Composer] - Add built-in allowlist regression tests
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
ALIBABA_FREE_TIER_TEXT_CAPABLE_MODELS,
|
||||
ALIBABA_NO_FREE_TIER_TEXT_MODELS,
|
||||
isAlibabaBuiltinFreeTierTextModel,
|
||||
isAlibabaBuiltinNoFreeTierTextModel,
|
||||
isAlibabaFreeTierAllowlistPackValid,
|
||||
loadAlibabaFreeTierAllowlistPack,
|
||||
resetAlibabaFreeTierAllowlistCache,
|
||||
} from "../../open-sse/services/alibabaFreeTierAllowlist.ts";
|
||||
|
||||
test("built-in allowlist includes operator free models and excludes paid blocklist", () => {
|
||||
assert.ok(ALIBABA_FREE_TIER_TEXT_CAPABLE_MODELS.includes("qwen3.6-plus"));
|
||||
assert.ok(ALIBABA_FREE_TIER_TEXT_CAPABLE_MODELS.includes("glm-5.2"));
|
||||
assert.ok(ALIBABA_NO_FREE_TIER_TEXT_MODELS.includes("qwen3.7-max"));
|
||||
assert.ok(ALIBABA_NO_FREE_TIER_TEXT_MODELS.includes("glm-5.2-fast-preview"));
|
||||
assert.equal(isAlibabaBuiltinFreeTierTextModel("qwen3.6-plus"), true);
|
||||
assert.equal(isAlibabaBuiltinNoFreeTierTextModel("kimi-k2.7-code"), true);
|
||||
assert.equal(isAlibabaBuiltinFreeTierTextModel("qwen3.7-max"), false);
|
||||
});
|
||||
|
||||
test("allowlist JSON pack overrides embedded lists when valid", () => {
|
||||
const previousPath = process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH;
|
||||
const packPath = `${process.cwd()}/config/alibaba-free-tier-allowlist.json`;
|
||||
process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH = packPath;
|
||||
resetAlibabaFreeTierAllowlistCache();
|
||||
|
||||
const pack = loadAlibabaFreeTierAllowlistPack();
|
||||
assert.ok(pack);
|
||||
assert.ok(isAlibabaFreeTierAllowlistPackValid(pack!));
|
||||
assert.ok(pack!.capable.includes("qwen3.6-plus"));
|
||||
|
||||
if (previousPath) process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH = previousPath;
|
||||
else delete process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH;
|
||||
resetAlibabaFreeTierAllowlistCache();
|
||||
});
|
||||
180
tests/unit/alibaba-free-tier-discovery.test.ts
Normal file
180
tests/unit/alibaba-free-tier-discovery.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @file alibaba-free-tier-discovery.test.ts
|
||||
* @description Tests for probe-based Alibaba free-tier model classification.
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-25] [Composer] - Add Alibaba free-tier probe classification tests
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
buildAlibabaFreeTierFilterContext,
|
||||
classifyAlibabaFreeTierProbe,
|
||||
filterAlibabaFreeEligibleModels,
|
||||
isAlibabaFreeTierCapableModel,
|
||||
isAlibabaNativeFreeTierModelFamily,
|
||||
isAlibabaThirdPartyPaidModelFamily,
|
||||
mergeAlibabaFreeTierProbeResults,
|
||||
} from "../../open-sse/services/alibabaFreeTierDiscovery.ts";
|
||||
|
||||
const FREE_TIER_ONLY_BODY = JSON.stringify({
|
||||
error: {
|
||||
code: "AllocationQuota.FreeTierOnly",
|
||||
message:
|
||||
'The free quota has been exhausted. To continue accessing the model on a paid basis, please disable the "use free tier only" mode in the management console.',
|
||||
},
|
||||
});
|
||||
|
||||
test("classifyAlibabaFreeTierProbe: FreeTierOnly means capable but drained", () => {
|
||||
const result = classifyAlibabaFreeTierProbe("qwen3.6-plus", 403, FREE_TIER_ONLY_BODY);
|
||||
assert.equal(result.verdict, "capable_drained");
|
||||
});
|
||||
|
||||
test("classifyAlibabaFreeTierProbe: native 200 means capable with remaining quota", () => {
|
||||
const result = classifyAlibabaFreeTierProbe("qwen3-coder-plus", 200, "{}");
|
||||
assert.equal(result.verdict, "capable_available");
|
||||
});
|
||||
|
||||
test("classifyAlibabaFreeTierProbe: kimi 200 is paid-only third-party family", () => {
|
||||
const result = classifyAlibabaFreeTierProbe("kimi-k2.7-code", 200, "{}");
|
||||
assert.equal(result.verdict, "not_capable");
|
||||
assert.equal(isAlibabaThirdPartyPaidModelFamily("kimi-k2.7-code"), true);
|
||||
assert.equal(isAlibabaNativeFreeTierModelFamily("kimi-k2.7-code"), false);
|
||||
});
|
||||
|
||||
test("mergeAlibabaFreeTierProbeResults persists capable and no-free-tier sets", () => {
|
||||
const merged = mergeAlibabaFreeTierProbeResults(
|
||||
{ alibabaBillingMode: "free" },
|
||||
[
|
||||
{ modelId: "qwen3.6-plus", verdict: "capable_drained", status: 403 },
|
||||
{ modelId: "kimi-k2.7-code", verdict: "not_capable", status: 200 },
|
||||
{ modelId: "qwen3-coder-plus", verdict: "capable_available", status: 200 },
|
||||
],
|
||||
"free"
|
||||
);
|
||||
|
||||
assert.ok(merged.alibabaFreeTierCapableModels?.includes("qwen3.6-plus"));
|
||||
assert.ok(merged.alibabaFreeTierCapableModels?.includes("qwen3-coder-plus"));
|
||||
assert.ok(merged.alibabaNoFreeTierModels?.includes("kimi-k2.7-code"));
|
||||
assert.ok(merged.alibabaFreeDrainedModels?.includes("qwen3.6-plus"));
|
||||
});
|
||||
|
||||
test("filterAlibabaFreeEligibleModels excludes drained and paid-only families", () => {
|
||||
const filtered = filterAlibabaFreeEligibleModels(
|
||||
["qwen3.6-plus", "qwen3-coder-plus", "kimi-k2.7-code", "glm-5.2"],
|
||||
{
|
||||
alibabaFreeDrainedModels: ["qwen3.6-plus"],
|
||||
alibabaNoFreeTierModels: ["kimi-k2.7-code"],
|
||||
}
|
||||
);
|
||||
assert.deepEqual(filtered, ["qwen3-coder-plus", "glm-5.2"]);
|
||||
});
|
||||
|
||||
test("isAlibabaFreeTierCapableModel defaults native families to capable before probe", () => {
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3-coder-next", {}), true);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("kimi-k2.7-code", {}), false);
|
||||
assert.equal(
|
||||
isAlibabaFreeTierCapableModel("kimi-k2.7-code", {
|
||||
alibabaNoFreeTierModels: ["kimi-k2.7-code"],
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("isAlibabaFreeTierCapableModel does not guess when console quota sync exists", () => {
|
||||
const psd = {
|
||||
alibabaFreeTierCapableModels: ["qwen3.6-plus"],
|
||||
alibabaNoFreeTierModels: ["qwen3.7-plus"],
|
||||
alibabaFreeTierQuotaLastSyncAt: "2026-07-25T00:00:00.000Z",
|
||||
};
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.6-plus", psd), true);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.7-plus", psd), false);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3-coder-next", psd), true);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("totally-unknown-model", psd), false);
|
||||
});
|
||||
|
||||
test("buildAlibabaFreeTierFilterContext blocks paid models on unsynced sibling keys", () => {
|
||||
const merged = buildAlibabaFreeTierFilterContext(
|
||||
[
|
||||
{
|
||||
id: "main",
|
||||
providerSpecificData: {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeTierQuotaLastSyncAt: "2026-07-25T00:00:00.000Z",
|
||||
alibabaFreeTierCapableModels: ["qwen3.6-plus"],
|
||||
alibabaNoFreeTierModels: ["glm-5.2-fast-preview"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "main-2",
|
||||
providerSpecificData: {
|
||||
alibabaBillingMode: "free",
|
||||
},
|
||||
},
|
||||
],
|
||||
"main-2"
|
||||
);
|
||||
|
||||
assert.equal(isAlibabaFreeTierCapableModel("glm-5.2-fast-preview", merged), false);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.6-plus", merged), true);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.7-max", merged), false);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("totally-unknown-model", merged), false);
|
||||
});
|
||||
|
||||
test("buildAlibabaFreeTierFilterContext prefers sibling with populated free-model lists", () => {
|
||||
const merged = buildAlibabaFreeTierFilterContext(
|
||||
[
|
||||
{
|
||||
id: "main",
|
||||
providerSpecificData: {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeTierQuotaLastSyncAt: "2026-07-25T12:00:00.000Z",
|
||||
alibabaFreeTierCapableModels: [],
|
||||
alibabaNoFreeTierModels: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "main-2",
|
||||
providerSpecificData: {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeTierQuotaLastSyncAt: "2026-07-25T06:00:00.000Z",
|
||||
alibabaFreeTierCapableModels: ["qwen3.6-plus"],
|
||||
alibabaNoFreeTierModels: ["glm-5.2-fast-preview", "qwen3.7-max"],
|
||||
},
|
||||
},
|
||||
],
|
||||
"main-2"
|
||||
);
|
||||
|
||||
assert.equal(isAlibabaFreeTierCapableModel("glm-5.2-fast-preview", merged), false);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.6-plus", merged), true);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.7-max", merged), false);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3-coder-next", merged), true);
|
||||
});
|
||||
|
||||
test("strictAllowlist blocks optimistic qwen/glm guessing for alibabafree combos", () => {
|
||||
const psd = {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeDrainedModels: [],
|
||||
alibabaNoFreeTierModels: [],
|
||||
alibabaFreeTierCapableModels: [],
|
||||
};
|
||||
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen-not-in-builtin-list", psd), true);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.7-max", psd), false);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.7-max", psd, { strictAllowlist: true }), false);
|
||||
assert.equal(
|
||||
isAlibabaFreeTierCapableModel(
|
||||
"qwen3.6-plus",
|
||||
{ ...psd, alibabaFreeTierCapableModels: ["qwen3.6-plus"] },
|
||||
{ strictAllowlist: true }
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.deepEqual(
|
||||
filterAlibabaFreeEligibleModels(["qwen3.7-max", "qwen3.6-plus", "glm-5.2-fast-preview"], psd, {
|
||||
strictAllowlist: true,
|
||||
}),
|
||||
["qwen3.6-plus"]
|
||||
);
|
||||
});
|
||||
113
tests/unit/alibaba-free-tier-exhaustion.test.ts
Normal file
113
tests/unit/alibaba-free-tier-exhaustion.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @file alibaba-free-tier-exhaustion.test.ts
|
||||
* @description Regression tests for Alibaba Model Studio free-tier drain handling.
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-24] [Composer] - Add free-tier exhaustion detection and combo exhaustion tests
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
ALIBABA_FREE_DRAINED_LOCK_MS,
|
||||
filterAlibabaFreeTierModels,
|
||||
getAlibabaBillingMode,
|
||||
getAlibabaFreeDrainedModels,
|
||||
isAlibabaFreeQuotaExhaustedError,
|
||||
isAlibabaModelFreeDrained,
|
||||
isAlibabaModelStudioProvider,
|
||||
mergeAlibabaFreeDrainedModels,
|
||||
} from "../../open-sse/services/alibabaFreeTier.ts";
|
||||
import {
|
||||
applyComboTargetExhaustion,
|
||||
type ComboExhaustionSets,
|
||||
} from "../../open-sse/services/combo/targetExhaustion.ts";
|
||||
import {
|
||||
clearAllModelLockouts,
|
||||
isModelLocked,
|
||||
lockModel,
|
||||
} from "../../open-sse/services/accountFallback.ts";
|
||||
|
||||
const ALIBABA_FREE_QUOTA_ERROR =
|
||||
"The free quota has been exhausted. To continue accessing the model on a paid basis, please complete your payment information (or disable the use free tier only mode).";
|
||||
|
||||
test("detects Alibaba free-quota exhaustion phrasing", () => {
|
||||
assert.equal(isAlibabaFreeQuotaExhaustedError(ALIBABA_FREE_QUOTA_ERROR), true);
|
||||
assert.equal(isAlibabaFreeQuotaExhaustedError("free tier of the model has been exhausted"), true);
|
||||
assert.equal(isAlibabaFreeQuotaExhaustedError("Forbidden"), false);
|
||||
});
|
||||
|
||||
test("defaults Alibaba billing mode to paid and supports free override", () => {
|
||||
assert.equal(getAlibabaBillingMode({}), "paid");
|
||||
assert.equal(getAlibabaBillingMode({ alibabaBillingMode: "free" }), "free");
|
||||
});
|
||||
|
||||
test("mergeAlibabaFreeDrainedModels persists unique model ids", () => {
|
||||
const first = mergeAlibabaFreeDrainedModels({}, "qwen3.7-max-preview");
|
||||
const second = mergeAlibabaFreeDrainedModels(first, "qwen3.6-plus");
|
||||
const third = mergeAlibabaFreeDrainedModels(second, "qwen3.7-max-preview");
|
||||
|
||||
assert.deepEqual(getAlibabaFreeDrainedModels(third), ["qwen3.7-max-preview", "qwen3.6-plus"]);
|
||||
assert.equal(isAlibabaModelFreeDrained("alibaba", third, "qwen3.7-max-preview"), true);
|
||||
assert.equal(isAlibabaModelFreeDrained("openai", third, "qwen3.7-max-preview"), false);
|
||||
});
|
||||
|
||||
test("isAlibabaModelStudioProvider recognizes alibaba family ids", () => {
|
||||
assert.equal(isAlibabaModelStudioProvider("alibaba"), true);
|
||||
assert.equal(isAlibabaModelStudioProvider("alibaba-cn"), true);
|
||||
assert.equal(isAlibabaModelStudioProvider("ali"), true);
|
||||
assert.equal(isAlibabaModelStudioProvider("bailian-coding-plan"), false);
|
||||
});
|
||||
|
||||
test("alibaba free-quota 403 does not exhaust the whole connection in combo routing", () => {
|
||||
const s: ComboExhaustionSets = {
|
||||
exhaustedProviders: new Set<string>(),
|
||||
exhaustedConnections: new Set<string>(),
|
||||
transientRateLimitedProviders: new Set<string>(),
|
||||
};
|
||||
const log = { info() {}, warn() {}, error() {}, debug() {} };
|
||||
const target = {
|
||||
kind: "model",
|
||||
executionKey: "ek",
|
||||
modelStr: "alibaba/qwen3.7-max-preview",
|
||||
provider: "alibaba",
|
||||
providerId: null,
|
||||
connectionId: "conn-1",
|
||||
} as Parameters<typeof applyComboTargetExhaustion>[0];
|
||||
|
||||
const exhausted = applyComboTargetExhaustion(target, {
|
||||
result: { status: 403 },
|
||||
fallbackResult: {},
|
||||
errorText: ALIBABA_FREE_QUOTA_ERROR,
|
||||
rawModel: "qwen3.7-max-preview",
|
||||
isTokenLimitBreach: false,
|
||||
allAccountsRateLimited: false,
|
||||
sets: s,
|
||||
log,
|
||||
tag: "COMBO",
|
||||
exhaustedLogLevel: "info",
|
||||
});
|
||||
|
||||
assert.equal(exhausted, false);
|
||||
assert.equal(s.exhaustedConnections.size, 0);
|
||||
assert.equal(s.exhaustedProviders.size, 0);
|
||||
});
|
||||
|
||||
test("filterAlibabaFreeTierModels removes only persisted drained models", () => {
|
||||
const filtered = filterAlibabaFreeTierModels(["qwen3.7-max-preview", "qwen3.6-plus"], {
|
||||
alibabaFreeDrainedModels: ["qwen3.7-max-preview"],
|
||||
});
|
||||
assert.deepEqual(filtered, ["qwen3.6-plus"]);
|
||||
});
|
||||
|
||||
test("permanent free-tier lockout uses a multi-year cooldown window", () => {
|
||||
clearAllModelLockouts();
|
||||
lockModel(
|
||||
"alibaba",
|
||||
"conn-1",
|
||||
"qwen3.7-max-preview",
|
||||
"free_quota_exhausted",
|
||||
ALIBABA_FREE_DRAINED_LOCK_MS
|
||||
);
|
||||
assert.equal(isModelLocked("alibaba", "conn-1", "qwen3.7-max-preview"), true);
|
||||
clearAllModelLockouts();
|
||||
});
|
||||
468
tests/unit/alibaba-free-tier-quota-fetcher.test.ts
Normal file
468
tests/unit/alibaba-free-tier-quota-fetcher.test.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* @file alibaba-free-tier-quota-fetcher.test.ts
|
||||
* @description Tests for Alibaba console free-tier quota API parsing and classification.
|
||||
*
|
||||
* @changes
|
||||
* - [2026-07-25] [Composer] - Add multimodal and audio quota classification tests
|
||||
* - [2026-07-25] [Composer] - Add console free-tier quota fetcher tests
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
classifyAlibabaAudioFreeTierQuotaEntries,
|
||||
classifyAlibabaFreeTierQuotaEntries,
|
||||
classifyAlibabaFreeTierQuotaEntry,
|
||||
classifyAlibabaMultimodalFreeTierQuotaEntries,
|
||||
classifyAlibabaVisionFreeTierQuotaEntries,
|
||||
isAlibabaQuotaValidityExpired,
|
||||
mergeAlibabaFreeTierQuotaClassification,
|
||||
normalizeAlibabaConsoleCookie,
|
||||
parseAlibabaFreeTierQuotaEntries,
|
||||
applyAlibabaSharedFreeTierEligibility,
|
||||
buildAlibabaFreeTierTextFilterContext,
|
||||
extractAlibabaSharedFreeTierEligibility,
|
||||
isAlibabaFreeTierAudioCapableModel,
|
||||
isAlibabaFreeTierMultimodalCapableModel,
|
||||
isAlibabaFreeTierVisionCapableModel,
|
||||
} from "../../open-sse/services/alibabaFreeTierQuotaFetcher.ts";
|
||||
import { isAlibabaFreeTierCapableModel } from "../../open-sse/services/alibabaFreeTierDiscovery.ts";
|
||||
import {
|
||||
isDashscopeAudioModelId,
|
||||
isDashscopeMultimodalModelId,
|
||||
isDashscopeVisionModelId,
|
||||
} from "../../open-sse/services/dashscopeTextModels.ts";
|
||||
|
||||
const SAMPLE_CONSOLE_RESPONSE = {
|
||||
code: "200",
|
||||
data: {
|
||||
DataV2: {
|
||||
data: {
|
||||
data: {
|
||||
freeTierQuotas: [
|
||||
{
|
||||
quotaInitTotal: 1000000,
|
||||
quotaValidityPeriod: 1786896000000,
|
||||
freeTierOnly: true,
|
||||
quotaTotalPercentage: 99.98,
|
||||
model: "qwen3.6-plus",
|
||||
quotaTotal: 999795,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
freeTierOnly: false,
|
||||
model: "qwen3.7-plus",
|
||||
quotaStatus: "UNKNOWN",
|
||||
},
|
||||
{
|
||||
freeTierOnly: false,
|
||||
model: "kimi-k2.7-code",
|
||||
quotaStatus: "UNKNOWN",
|
||||
},
|
||||
{
|
||||
freeTierOnly: true,
|
||||
model: "glm-5.2",
|
||||
quotaStatus: "UNKNOWN",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 0,
|
||||
freeTierOnly: false,
|
||||
quotaTotalPercentage: 0,
|
||||
model: "glm-5.2-fast-preview",
|
||||
quotaTotal: 0,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 1000000,
|
||||
freeTierOnly: true,
|
||||
quotaTotalPercentage: 100,
|
||||
model: "qwen3-vl-plus",
|
||||
quotaTotal: 1000000,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
],
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const SAMPLE_VISION_CONSOLE_RESPONSE = {
|
||||
code: "200",
|
||||
data: {
|
||||
DataV2: {
|
||||
data: {
|
||||
data: {
|
||||
freeTierQuotas: [
|
||||
{
|
||||
quotaInitTotal: 50,
|
||||
freeTierOnly: false,
|
||||
quotaTotalPercentage: 100,
|
||||
model: "wan2.7-t2v",
|
||||
quotaTotal: 50,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 100,
|
||||
freeTierOnly: false,
|
||||
quotaTotalPercentage: 100,
|
||||
model: "qwen-image-2.0-pro",
|
||||
quotaTotal: 100,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 0,
|
||||
freeTierOnly: false,
|
||||
quotaTotalPercentage: 0,
|
||||
model: "qwen-image-3.0-pro",
|
||||
quotaTotal: 0,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 1000000,
|
||||
freeTierOnly: true,
|
||||
quotaTotalPercentage: 100,
|
||||
model: "qwen3.6-plus",
|
||||
quotaTotal: 999795,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
],
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const SAMPLE_MULTIMODAL_CONSOLE_RESPONSE = {
|
||||
code: "200",
|
||||
data: {
|
||||
DataV2: {
|
||||
data: {
|
||||
data: {
|
||||
freeTierQuotas: [
|
||||
{
|
||||
quotaInitTotal: 1000000,
|
||||
freeTierOnly: true,
|
||||
model: "qwen3.5-omni-plus",
|
||||
quotaTotal: 1000000,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 0,
|
||||
freeTierOnly: false,
|
||||
model: "qwen-omni-turbo-latest",
|
||||
quotaTotal: 0,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 36000,
|
||||
freeTierOnly: true,
|
||||
model: "qwen3-asr-flash",
|
||||
quotaTotal: 36000,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
],
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const SAMPLE_AUDIO_CONSOLE_RESPONSE = {
|
||||
code: "200",
|
||||
data: {
|
||||
DataV2: {
|
||||
data: {
|
||||
data: {
|
||||
freeTierQuotas: [
|
||||
{
|
||||
quotaInitTotal: 36000,
|
||||
freeTierOnly: true,
|
||||
model: "qwen3-asr-flash",
|
||||
quotaTotal: 36000,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 10000,
|
||||
freeTierOnly: true,
|
||||
model: "qwen3-tts-flash",
|
||||
quotaTotal: 10000,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 0,
|
||||
freeTierOnly: false,
|
||||
model: "voice-enrollment",
|
||||
quotaTotal: 0,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
{
|
||||
quotaInitTotal: 1000000,
|
||||
freeTierOnly: true,
|
||||
model: "qwen3.5-omni-plus",
|
||||
quotaTotal: 1000000,
|
||||
quotaStatus: "VALID",
|
||||
},
|
||||
],
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("parseAlibabaFreeTierQuotaEntries reads console payload", () => {
|
||||
const entries = parseAlibabaFreeTierQuotaEntries(SAMPLE_CONSOLE_RESPONSE);
|
||||
assert.equal(entries.length, 6);
|
||||
assert.equal(entries[0].model, "qwen3.6-plus");
|
||||
assert.equal(entries[0].freeTierOnly, true);
|
||||
});
|
||||
|
||||
test("classifyAlibabaFreeTierQuotaEntry maps console flags", () => {
|
||||
assert.equal(
|
||||
classifyAlibabaFreeTierQuotaEntry({
|
||||
model: "qwen3.6-plus",
|
||||
freeTierOnly: true,
|
||||
quotaStatus: "VALID",
|
||||
quotaTotal: 100,
|
||||
}),
|
||||
"available"
|
||||
);
|
||||
assert.equal(
|
||||
classifyAlibabaFreeTierQuotaEntry({
|
||||
model: "glm-5.2",
|
||||
freeTierOnly: true,
|
||||
quotaStatus: "UNKNOWN",
|
||||
}),
|
||||
"capable_unknown"
|
||||
);
|
||||
assert.equal(
|
||||
classifyAlibabaFreeTierQuotaEntry({
|
||||
model: "qwen3.7-plus",
|
||||
freeTierOnly: false,
|
||||
quotaStatus: "UNKNOWN",
|
||||
}),
|
||||
"not_capable"
|
||||
);
|
||||
assert.equal(
|
||||
classifyAlibabaFreeTierQuotaEntry({
|
||||
model: "glm-5.2-fast-preview",
|
||||
freeTierOnly: false,
|
||||
quotaStatus: "VALID",
|
||||
quotaTotal: 0,
|
||||
quotaInitTotal: 0,
|
||||
}),
|
||||
"not_capable"
|
||||
);
|
||||
});
|
||||
|
||||
test("classifyAlibabaFreeTierQuotaEntry treats expired quotaValidityPeriod as not_capable", () => {
|
||||
assert.equal(
|
||||
classifyAlibabaFreeTierQuotaEntry(
|
||||
{
|
||||
model: "qwen3.6-plus",
|
||||
freeTierOnly: true,
|
||||
quotaStatus: "VALID",
|
||||
quotaTotal: 100,
|
||||
quotaValidityPeriod: Date.now() - 60_000,
|
||||
},
|
||||
Date.now()
|
||||
),
|
||||
"not_capable"
|
||||
);
|
||||
assert.equal(
|
||||
isAlibabaQuotaValidityExpired({
|
||||
model: "qwen3.6-plus",
|
||||
freeTierOnly: true,
|
||||
quotaStatus: "VALID",
|
||||
quotaValidityPeriod: Date.now() + 60_000,
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("classifyAlibabaFreeTierQuotaEntries filters text-only models for alibabafree", () => {
|
||||
const classification = classifyAlibabaFreeTierQuotaEntries(
|
||||
parseAlibabaFreeTierQuotaEntries(SAMPLE_CONSOLE_RESPONSE),
|
||||
{ textOnly: true }
|
||||
);
|
||||
|
||||
assert.ok(classification.capableModels.includes("qwen3.6-plus"));
|
||||
assert.ok(classification.capableModels.includes("glm-5.2"));
|
||||
assert.ok(classification.noFreeTierModels.includes("qwen3.7-plus"));
|
||||
assert.ok(classification.noFreeTierModels.includes("kimi-k2.7-code"));
|
||||
assert.ok(classification.noFreeTierModels.includes("glm-5.2-fast-preview"));
|
||||
assert.equal(classification.capableModels.includes("qwen3-vl-plus"), false);
|
||||
});
|
||||
|
||||
test("classifyAlibabaVisionFreeTierQuotaEntries includes promo vision quota models", () => {
|
||||
const classification = classifyAlibabaVisionFreeTierQuotaEntries(
|
||||
parseAlibabaFreeTierQuotaEntries(SAMPLE_VISION_CONSOLE_RESPONSE)
|
||||
);
|
||||
|
||||
assert.ok(classification.capableModels.includes("wan2.7-t2v"));
|
||||
assert.ok(classification.capableModels.includes("qwen-image-2.0-pro"));
|
||||
assert.ok(classification.drainedModels.includes("qwen-image-3.0-pro"));
|
||||
assert.equal(classification.capableModels.includes("qwen-image-3.0-pro"), false);
|
||||
assert.equal(classification.capableModels.includes("qwen3.6-plus"), false);
|
||||
});
|
||||
|
||||
test("isDashscopeVisionModelId recognizes wan and qwen-image families", () => {
|
||||
assert.equal(isDashscopeVisionModelId("wan2.7-t2v"), true);
|
||||
assert.equal(isDashscopeVisionModelId("qwen-image-plus"), true);
|
||||
assert.equal(isDashscopeVisionModelId("qwen3.6-plus"), false);
|
||||
});
|
||||
|
||||
test("classifyAlibabaMultimodalFreeTierQuotaEntries requires freeTierOnly omni models", () => {
|
||||
const classification = classifyAlibabaMultimodalFreeTierQuotaEntries(
|
||||
parseAlibabaFreeTierQuotaEntries(SAMPLE_MULTIMODAL_CONSOLE_RESPONSE)
|
||||
);
|
||||
|
||||
assert.ok(classification.capableModels.includes("qwen3.5-omni-plus"));
|
||||
assert.ok(classification.noFreeTierModels.includes("qwen-omni-turbo-latest"));
|
||||
assert.equal(classification.capableModels.includes("qwen3-asr-flash"), false);
|
||||
});
|
||||
|
||||
test("classifyAlibabaAudioFreeTierQuotaEntries includes asr and tts free-tier models", () => {
|
||||
const classification = classifyAlibabaAudioFreeTierQuotaEntries(
|
||||
parseAlibabaFreeTierQuotaEntries(SAMPLE_AUDIO_CONSOLE_RESPONSE)
|
||||
);
|
||||
|
||||
assert.ok(classification.capableModels.includes("qwen3-asr-flash"));
|
||||
assert.ok(classification.capableModels.includes("qwen3-tts-flash"));
|
||||
assert.ok(classification.noFreeTierModels.includes("voice-enrollment"));
|
||||
assert.equal(classification.capableModels.includes("qwen3.5-omni-plus"), false);
|
||||
});
|
||||
|
||||
test("isDashscopeMultimodalModelId and isDashscopeAudioModelId classify model families", () => {
|
||||
assert.equal(isDashscopeMultimodalModelId("qwen3.5-omni-plus"), true);
|
||||
assert.equal(isDashscopeMultimodalModelId("qwen3-asr-flash"), false);
|
||||
assert.equal(isDashscopeAudioModelId("qwen3-tts-flash"), true);
|
||||
assert.equal(isDashscopeAudioModelId("cosyvoice-v3-flash"), true);
|
||||
assert.equal(isDashscopeAudioModelId("qwen3.5-omni-plus"), false);
|
||||
});
|
||||
|
||||
test("mergeAlibabaFreeTierQuotaClassification makes discovery authoritative", () => {
|
||||
const text = classifyAlibabaFreeTierQuotaEntries(
|
||||
parseAlibabaFreeTierQuotaEntries(SAMPLE_CONSOLE_RESPONSE),
|
||||
{ textOnly: true }
|
||||
);
|
||||
const vision = classifyAlibabaVisionFreeTierQuotaEntries(
|
||||
parseAlibabaFreeTierQuotaEntries(SAMPLE_VISION_CONSOLE_RESPONSE)
|
||||
);
|
||||
const multimodal = classifyAlibabaMultimodalFreeTierQuotaEntries(
|
||||
parseAlibabaFreeTierQuotaEntries(SAMPLE_MULTIMODAL_CONSOLE_RESPONSE)
|
||||
);
|
||||
const audio = classifyAlibabaAudioFreeTierQuotaEntries(
|
||||
parseAlibabaFreeTierQuotaEntries(SAMPLE_AUDIO_CONSOLE_RESPONSE)
|
||||
);
|
||||
const merged = mergeAlibabaFreeTierQuotaClassification(
|
||||
{ alibabaBillingMode: "free" },
|
||||
{
|
||||
text,
|
||||
vision,
|
||||
multimodal,
|
||||
audio,
|
||||
entries: [...text.entries, ...vision.entries, ...multimodal.entries, ...audio.entries],
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(merged.alibabaFreeTierDiscoverySource, "console-quota-api");
|
||||
assert.equal(typeof merged.alibabaFreeTierQuotaLastSyncAt, "string");
|
||||
assert.ok(merged.alibabaFreeTierVisionCapableModels?.includes("wan2.7-t2v"));
|
||||
assert.equal(merged.alibabaFreeTierVisionCapableModels?.includes("qwen-image-3.0-pro"), false);
|
||||
assert.ok(merged.alibabaFreeTierMultimodalCapableModels?.includes("qwen3.5-omni-plus"));
|
||||
assert.ok(merged.alibabaFreeTierAudioCapableModels?.includes("qwen3-tts-flash"));
|
||||
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.6-plus", merged), true);
|
||||
assert.equal(isAlibabaFreeTierCapableModel("qwen3.7-plus", merged), false);
|
||||
assert.equal(isAlibabaFreeTierVisionCapableModel("wan2.7-t2v", merged), true);
|
||||
assert.equal(isAlibabaFreeTierVisionCapableModel("qwen-image-3.0-pro", merged), false);
|
||||
assert.equal(isAlibabaFreeTierMultimodalCapableModel("qwen3.5-omni-plus", merged), true);
|
||||
assert.equal(isAlibabaFreeTierAudioCapableModel("qwen3-tts-flash", merged), true);
|
||||
});
|
||||
|
||||
test("mergeAlibabaFreeTierQuotaClassification preserves text lists on partial category snapshot", () => {
|
||||
const merged = mergeAlibabaFreeTierQuotaClassification(
|
||||
{
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeTierCapableModels: ["qwen3.6-plus"],
|
||||
alibabaNoFreeTierModels: ["qwen3.7-max"],
|
||||
},
|
||||
{
|
||||
text: { capableModels: [], noFreeTierModels: [], drainedModels: [], entries: [] },
|
||||
vision: {
|
||||
capableModels: ["wan2.7-t2v"],
|
||||
noFreeTierModels: [],
|
||||
drainedModels: [],
|
||||
entries: [],
|
||||
},
|
||||
multimodal: { capableModels: [], noFreeTierModels: [], drainedModels: [], entries: [] },
|
||||
audio: { capableModels: [], noFreeTierModels: [], drainedModels: [], entries: [] },
|
||||
entries: [],
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(merged.alibabaFreeTierCapableModels, ["qwen3.6-plus"]);
|
||||
assert.deepEqual(merged.alibabaNoFreeTierModels, ["qwen3.7-max"]);
|
||||
assert.deepEqual(merged.alibabaFreeTierVisionCapableModels, ["wan2.7-t2v"]);
|
||||
});
|
||||
|
||||
test("buildAlibabaFreeTierTextFilterContext uses canonical sibling free-model lists", () => {
|
||||
const context = buildAlibabaFreeTierTextFilterContext(
|
||||
[
|
||||
{
|
||||
id: "main",
|
||||
providerSpecificData: {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeTierQuotaLastSyncAt: "2026-07-25T12:00:00.000Z",
|
||||
alibabaFreeTierCapableModels: [],
|
||||
alibabaNoFreeTierModels: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "main-2",
|
||||
providerSpecificData: {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeTierQuotaLastSyncAt: "2026-07-25T06:00:00.000Z",
|
||||
alibabaFreeTierCapableModels: ["qwen3.6-plus"],
|
||||
alibabaNoFreeTierModels: ["qwen3.7-max", "glm-5.2-fast-preview"],
|
||||
},
|
||||
},
|
||||
],
|
||||
"main"
|
||||
);
|
||||
|
||||
assert.ok((context.alibabaFreeTierCapableModels as string[]).includes("qwen3.6-plus"));
|
||||
assert.equal((context.alibabaFreeTierCapableModels as string[]).includes("glm-5.2"), false);
|
||||
assert.ok((context.alibabaNoFreeTierModels as string[]).includes("qwen3.7-max"));
|
||||
assert.ok((context.alibabaNoFreeTierModels as string[]).includes("glm-5.2-fast-preview"));
|
||||
});
|
||||
|
||||
test("shared free-tier eligibility copies lists but preserves per-connection drained models", () => {
|
||||
const shared = extractAlibabaSharedFreeTierEligibility({
|
||||
alibabaFreeTierCapableModels: ["qwen3.6-plus"],
|
||||
alibabaNoFreeTierModels: ["qwen3.7-max"],
|
||||
alibabaFreeTierQuotaLastSyncAt: "2026-07-25T00:00:00.000Z",
|
||||
});
|
||||
const applied = applyAlibabaSharedFreeTierEligibility(
|
||||
{
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeDrainedModels: ["qwen3.6-plus"],
|
||||
},
|
||||
shared
|
||||
);
|
||||
|
||||
assert.deepEqual(applied.alibabaFreeTierCapableModels, ["qwen3.6-plus"]);
|
||||
assert.deepEqual(applied.alibabaFreeDrainedModels, ["qwen3.6-plus"]);
|
||||
});
|
||||
|
||||
test("normalizeAlibabaConsoleCookie accepts ticket-only paste", () => {
|
||||
assert.equal(normalizeAlibabaConsoleCookie("abc123ticket"), "login_aliyunid_ticket=abc123ticket");
|
||||
assert.equal(
|
||||
normalizeAlibabaConsoleCookie("login_aliyunid_ticket=abc123ticket; cna=xyz"),
|
||||
"login_aliyunid_ticket=abc123ticket; cna=xyz"
|
||||
);
|
||||
});
|
||||
@@ -32,10 +32,13 @@ const {
|
||||
|
||||
const { replaceSyncedAvailableModelsForConnection, getSyncedAvailableModels } =
|
||||
await import("../../src/lib/db/models.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
core.getDbInstance(); // initialise DB + run migrations
|
||||
|
||||
const { normalizeComboRecord } = await import("../../src/lib/combos/steps.ts");
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function makeCombo(models: unknown[], name = "test-combo") {
|
||||
@@ -406,3 +409,98 @@ test("#8926: inactive synced catalog does not override static wildcard fallback"
|
||||
"an inactive connection must not make its synced catalog authoritative"
|
||||
);
|
||||
});
|
||||
test("expandProviderWildcardsInCombo: skips Alibaba free-tier drained models for pinned connections", async () => {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "alibaba",
|
||||
authType: "apikey",
|
||||
name: "alibaba-free-wildcard",
|
||||
apiKey: "test-dashscope-key",
|
||||
providerSpecificData: {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeDrainedModels: ["qwen3.7-max-preview"],
|
||||
},
|
||||
});
|
||||
await seedSyncedModels("alibaba", connection.id, [
|
||||
"qwen3.7-max-preview",
|
||||
"qwen3.6-plus",
|
||||
"glm-5.2",
|
||||
]);
|
||||
|
||||
const combo = makeCombo([
|
||||
{
|
||||
kind: "provider-wildcard",
|
||||
providerId: "alibaba",
|
||||
modelPattern: "*",
|
||||
connectionId: connection.id,
|
||||
},
|
||||
]);
|
||||
const result = await expandProviderWildcardsInCombo(combo);
|
||||
const models = result.models.map((entry) => (entry as { model: string }).model);
|
||||
|
||||
assert.equal(models.includes("alibaba/qwen3.7-max-preview"), false);
|
||||
assert.ok(models.includes("alibaba/qwen3.6-plus"));
|
||||
assert.ok(models.includes("alibaba/glm-5.2"));
|
||||
assert.equal(models.includes("alibaba/kimi-k2.7-code"), false);
|
||||
});
|
||||
|
||||
test("expandProviderWildcardsInCombo: alibabafree combo uses strict console allowlist only", async () => {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "alibaba",
|
||||
authType: "apikey",
|
||||
name: "alibaba-free-strict",
|
||||
apiKey: "test-dashscope-key-strict-allowlist",
|
||||
providerSpecificData: {
|
||||
alibabaBillingMode: "free",
|
||||
alibabaFreeTierCapableModels: ["qwen3.6-plus"],
|
||||
alibabaNoFreeTierModels: ["glm-5.2-fast-preview", "qwen3.7-max"],
|
||||
},
|
||||
});
|
||||
await seedSyncedModels("alibaba", connection.id, [
|
||||
"qwen3.6-plus",
|
||||
"qwen3.7-max",
|
||||
"glm-5.2-fast-preview",
|
||||
"glm-5.2",
|
||||
]);
|
||||
|
||||
const combo = makeCombo(
|
||||
[
|
||||
{
|
||||
kind: "provider-wildcard",
|
||||
providerId: "alibaba",
|
||||
modelPattern: "*",
|
||||
connectionId: connection.id,
|
||||
},
|
||||
],
|
||||
"alibabafree"
|
||||
);
|
||||
const result = await expandProviderWildcardsInCombo(combo);
|
||||
const models = result.models.map((entry) => (entry as { model: string }).model);
|
||||
|
||||
assert.ok(models.includes("alibaba/qwen3.6-plus"));
|
||||
assert.ok(models.includes("alibaba/glm-5.2"));
|
||||
assert.equal(models.includes("alibaba/qwen3.7-max"), false);
|
||||
assert.equal(models.includes("alibaba/glm-5.2-fast-preview"), false);
|
||||
});
|
||||
|
||||
test("normalizeComboRecord: preserves provider-wildcard steps for DB round-trip", () => {
|
||||
const normalized = normalizeComboRecord({
|
||||
name: "alibabafree",
|
||||
models: [
|
||||
{
|
||||
kind: "provider-wildcard",
|
||||
providerId: "alibaba",
|
||||
modelPattern: "*",
|
||||
connectionId: "conn-main",
|
||||
label: "main",
|
||||
},
|
||||
"alibaba/qwen*",
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(normalized.models.length, 2);
|
||||
assert.equal(normalized.models[0].kind, "provider-wildcard");
|
||||
assert.equal((normalized.models[0] as { providerId: string }).providerId, "alibaba");
|
||||
assert.equal((normalized.models[0] as { connectionId?: string }).connectionId, "conn-main");
|
||||
assert.equal(normalized.models[1].kind, "provider-wildcard");
|
||||
assert.equal((normalized.models[1] as { modelPattern: string }).modelPattern, "qwen*");
|
||||
});
|
||||
|
||||
@@ -176,3 +176,50 @@ test("legacy Alibaba China connections also sync only Beijing text models", asyn
|
||||
expectedModelIds: ALIBABA_MODEL_STUDIO_MODEL_IDS,
|
||||
});
|
||||
});
|
||||
|
||||
test("Alibaba free billing mode syncs all live text models, not only the curated catalog", async () => {
|
||||
await resetStorage();
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "alibaba",
|
||||
authType: "apikey",
|
||||
name: "alibaba-free-live",
|
||||
apiKey: "test-dashscope-key",
|
||||
providerSpecificData: { region: "global-sg", alibabaBillingMode: "free" },
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({ object: "list", data: MIXED_DASHSCOPE_MODELS }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const response = await modelsRoute.GET(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.ok(
|
||||
body.models.length > ALIBABA_MODEL_STUDIO_MODEL_IDS.length,
|
||||
"free billing mode should expose more than the curated catalog"
|
||||
);
|
||||
assert.ok(
|
||||
body.models.some((model: { id: string }) => model.id === "qwen3-coder-next"),
|
||||
"expected upstream-only free-tier model to be included"
|
||||
);
|
||||
assert.equal(
|
||||
body.models.some((model: { id: string }) => model.id === "qwen-image-2.0-pro"),
|
||||
false,
|
||||
"non-text models must still be filtered out"
|
||||
);
|
||||
assert.equal(
|
||||
body.models.some((model: { id: string }) => model.id === "kimi-k2.7-code"),
|
||||
false,
|
||||
"third-party paid-only models must be excluded from free billing discovery"
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
86
tests/unit/forced-connection-fallback.test.ts
Normal file
86
tests/unit/forced-connection-fallback.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { resolveForcedConnectionForCredentialPool } from "../../src/sse/services/sessionAffinityPin.ts";
|
||||
|
||||
const conn = (id: string, rateLimitedUntil: string | null = null) => ({
|
||||
id,
|
||||
rateLimitedUntil,
|
||||
});
|
||||
|
||||
test("resolveForcedConnectionForCredentialPool drops forced id when excluded after 429 fallback", () => {
|
||||
const excluded = new Set(["dead-account"]);
|
||||
assert.equal(
|
||||
resolveForcedConnectionForCredentialPool({
|
||||
forcedConnectionId: "dead-account",
|
||||
excludedConnectionIds: excluded,
|
||||
connections: [conn("dead-account"), conn("healthy-account")],
|
||||
allowRateLimitedConnections: false,
|
||||
bypassQuotaPolicy: false,
|
||||
isQuotaExhausted: () => false,
|
||||
isQuotaPolicyBlocked: () => false,
|
||||
}),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveForcedConnectionForCredentialPool keeps forced id when eligible", () => {
|
||||
assert.equal(
|
||||
resolveForcedConnectionForCredentialPool({
|
||||
forcedConnectionId: "healthy-account",
|
||||
excludedConnectionIds: new Set(),
|
||||
connections: [conn("healthy-account"), conn("other-account")],
|
||||
allowRateLimitedConnections: false,
|
||||
bypassQuotaPolicy: false,
|
||||
isQuotaExhausted: () => false,
|
||||
isQuotaPolicyBlocked: () => false,
|
||||
}),
|
||||
"healthy-account"
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveForcedConnectionForCredentialPool drops forced id on cooldown", () => {
|
||||
const future = new Date(Date.now() + 60_000).toISOString();
|
||||
assert.equal(
|
||||
resolveForcedConnectionForCredentialPool({
|
||||
forcedConnectionId: "cooling-account",
|
||||
excludedConnectionIds: new Set(),
|
||||
connections: [conn("cooling-account", future)],
|
||||
allowRateLimitedConnections: false,
|
||||
bypassQuotaPolicy: false,
|
||||
isQuotaExhausted: () => false,
|
||||
isQuotaPolicyBlocked: () => false,
|
||||
}),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveForcedConnectionForCredentialPool drops forced id when quota exhausted", () => {
|
||||
assert.equal(
|
||||
resolveForcedConnectionForCredentialPool({
|
||||
forcedConnectionId: "exhausted-account",
|
||||
excludedConnectionIds: new Set(),
|
||||
connections: [conn("exhausted-account")],
|
||||
allowRateLimitedConnections: false,
|
||||
bypassQuotaPolicy: false,
|
||||
isQuotaExhausted: (id) => id === "exhausted-account",
|
||||
isQuotaPolicyBlocked: () => false,
|
||||
}),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveForcedConnectionForCredentialPool with empty connections only checks exclusion", () => {
|
||||
assert.equal(
|
||||
resolveForcedConnectionForCredentialPool({
|
||||
forcedConnectionId: "pinned-account",
|
||||
excludedConnectionIds: new Set(),
|
||||
connections: [],
|
||||
allowRateLimitedConnections: false,
|
||||
bypassQuotaPolicy: false,
|
||||
isQuotaExhausted: () => true,
|
||||
isQuotaPolicyBlocked: () => true,
|
||||
}),
|
||||
"pinned-account"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user