fix(providers): fetch live Qwen and Alibaba Token Plan catalogs (#13299)

Merged. Both Token Plan providers had no `modelsUrl` and no discovery config, so Sync Models never even tried a live request. Fetching the public Personal Plan catalog through `safeOutboundFetch` with fixed hosts, no inference keys and no cookies, validating the gateway envelope and reusing the DashScope text-model classifier keeps this narrow and safe; a failed or media-only result falls back without touching the previous catalog.

Validated as a combined board first (this PR merged with the 4 siblings of the JxnLexn wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 77 passing / 0 failing focused node:test cases across the test files the wave touches. The wave's i18n fill (new keys carried to all 66 locales), free-tier doc counts and file-size rebaseline land in one follow-up PR right after the wave, as with #13904.

Thank you.
This commit is contained in:
Jan Leon
2026-09-16 21:50:42 +02:00
committed by GitHub
parent af2002a493
commit bd8a12f304
4 changed files with 205 additions and 0 deletions

View File

@@ -0,0 +1 @@
- **fix(providers):** Fetch Qwen and Alibaba Token Plan model catalogs through their authenticated console gateways, with public-only URL validation and local-catalog fallback when discovery is unavailable. ([#13299](https://github.com/diegosouzapw/OmniRoute/pull/13299)) — thanks @JxnLexn

View File

@@ -27,6 +27,11 @@ import {
import { errorResponse, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts";
import { deriveConfigFromRegistryModelsUrl } from "./discoveryConfig";
import {
buildTokenPlanCatalogRequest,
isTokenPlanCatalogProvider,
parseTokenPlanCatalog,
} from "@/lib/providerModels/tokenPlanModelDiscovery";
import { resolveZedModels } from "@omniroute/open-sse/shared/zedAuth.ts";
import {
fetchGitHubCopilotModels,
@@ -1938,6 +1943,40 @@ export async function GET(
}
const xaiOauthLiveConfig = provider === "xai-oauth" ? getXaiOauthLiveModelsConfig() : undefined;
if (isTokenPlanCatalogProvider(provider)) {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const disabledResponse = maybeReturnAutoFetchDisabled();
if (disabledResponse) return disabledResponse;
const catalogRequest = buildTokenPlanCatalogRequest(
provider,
connection.providerSpecificData
);
let liveModels: Array<{ id: string; name: string }>;
try {
const response = await safeOutboundFetch(catalogRequest.url, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
...catalogRequest.init,
guard: "public-only",
proxyConfig: proxy,
});
if (!response.ok) throw new Error("Token Plan catalog request failed");
liveModels = parseTokenPlanCatalog(await response.json());
} catch {
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "Token Plan live catalog unavailable — using cached catalog",
localWarning: "Token Plan live catalog unavailable — using local catalog",
});
if (fallback) return fallback;
return errorResponse(502, "Token Plan live catalog unavailable. Please try again later.");
}
return buildApiDiscoveryResponse(liveModels, undefined, {
catalogMode: "live_token_plan_catalog",
catalogScope: "product",
});
}
const config =
xaiOauthLiveConfig ??
(provider in PROVIDER_MODELS_CONFIG

View File

@@ -0,0 +1,89 @@
import { z } from "zod";
import { isDashscopeTextModelId } from "../../../open-sse/services/dashscopeTextModels.ts";
import { resolveAlibabaProviderRegion } from "../../shared/constants/alibabaProviderRegions.ts";
// Public model list used by the official Qwen Personal Token Plan pricing page.
// This is a product catalog, not an account entitlement or quota check.
const MODEL_LIST_API = "zeldaEasy.bmp.bmpTokenPlanServcie.modelIdList";
export function isTokenPlanCatalogProvider(provider: string): boolean {
return provider === "qwen-cloud-token-plan" || provider === "bailian-coding-plan";
}
export function buildTokenPlanCatalogRequest(provider: string, providerSpecificData?: unknown) {
if (!isTokenPlanCatalogProvider(provider)) throw new Error("Unsupported Token Plan provider");
const beijing = resolveAlibabaProviderRegion(provider, providerSpecificData) === "china-beijing";
const alibaba = provider === "bailian-coding-plan";
// The regional public Qwen catalog also covers the Alibaba Personal Token Plan.
const host = beijing
? "cs-data.qianwenai.com"
: alibaba
? "bailian-singapore-cs.alibabacloud.com"
: "cs-data.qwencloud.com";
const action = beijing ? "BroadScopeAspnGateway" : "IntlBroadScopeAspnGateway";
const query = new URLSearchParams({ action, product: "sfm_bailian", api: MODEL_LIST_API });
const body = new URLSearchParams({
product: "sfm_bailian",
action,
sec_token: "",
region: beijing ? "cn-beijing" : "ap-southeast-1",
params: JSON.stringify({
Api: MODEL_LIST_API,
Data: {
edition: "PERSONAL",
cornerstoneParam: {
consoleSite: beijing ? "QIANWENAI" : alibaba ? "ALIYUN" : "QWENCLOUD",
domain: beijing
? "www.qianwenai.com"
: alibaba
? "modelstudio.console.alibabacloud.com"
: "www.qwencloud.com",
productCode: "p_efm",
protocol: "V2",
xsp_lang: beijing ? "zh-CN" : "en-US",
},
},
}),
});
return {
url: `https://${host}/data/api.json?${query}`,
init: {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
body: body.toString(),
} satisfies RequestInit,
};
}
const successCode = z.union([z.literal("200"), z.literal(200)]);
const catalogEnvelope = z.object({
code: successCode,
data: z.object({
success: z.literal(true),
DataV2: z.object({
data: z.object({
code: successCode,
success: z.literal(true),
data: z
.array(
z
.string()
.trim()
.min(1)
.max(256)
.regex(/^[a-zA-Z0-9._-]+$/)
)
.max(2000),
}),
}),
}),
});
export function parseTokenPlanCatalog(payload: unknown): Array<{ id: string; name: string }> {
const parsed = catalogEnvelope.safeParse(payload);
if (!parsed.success) throw new Error("Token Plan catalog returned an invalid response");
const ids = [...new Set(parsed.data.data.DataV2.data.data)].filter(isDashscopeTextModelId);
// An unexpected empty/media-only response must not erase a previously usable chat catalog.
if (ids.length === 0) throw new Error("Token Plan catalog returned no chat models");
return ids.map((id) => ({ id, name: id }));
}

View File

@@ -0,0 +1,76 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
buildTokenPlanCatalogRequest,
isTokenPlanCatalogProvider,
parseTokenPlanCatalog,
} from "../../src/lib/providerModels/tokenPlanModelDiscovery.ts";
function envelope(ids: unknown[]) {
return {
code: "200",
data: { success: true, DataV2: { data: { code: "200", success: true, data: ids } } },
};
}
test("Token Plan discovery uses regional public gateways without forwarding credentials", () => {
for (const provider of ["qwen-cloud-token-plan", "bailian-coding-plan"]) {
for (const region of ["global-sg", "china-beijing"]) {
const request = buildTokenPlanCatalogRequest(provider, {
region,
apiKey: "secret-key",
cookie: "secret-cookie",
baseUrl: "http://127.0.0.1/private",
});
const expectedHost =
region === "china-beijing"
? "cs-data.qianwenai.com"
: provider === "bailian-coding-plan"
? "bailian-singapore-cs.alibabacloud.com"
: "cs-data.qwencloud.com";
assert.equal(new URL(request.url).hostname, expectedHost);
assert.equal(request.init.method, "POST");
assert.equal(JSON.stringify(request).includes("secret"), false);
const data = JSON.parse(new URLSearchParams(request.init.body).get("params")!).Data;
assert.equal(data.edition, "PERSONAL");
}
}
assert.equal(isTokenPlanCatalogProvider("qwen-cloud"), false);
assert.equal(isTokenPlanCatalogProvider("alibaba"), false);
assert.throws(() => buildTokenPlanCatalogRequest("alibaba"));
});
test("live IDs survive without a static allowlist while media and duplicates are excluded", () => {
const models = parseTokenPlanCatalog(
envelope([
"qwen3.8-flash",
"deepseek-v4-pro-0813",
"glm-5.2",
"qwen-future-model",
"qwen3.8-flash",
"qwen-image-3.0-pro",
"wan2.7-image",
"happyhorse-1.1-i2v",
"qwen-audio-3.0-asr-flash",
"qwen-audio-3.0-realtime-plus",
"qwen-audio-3.0-tts-plus",
])
);
assert.deepEqual(
models.map(({ id }) => id),
["qwen3.8-flash", "deepseek-v4-pro-0813", "glm-5.2", "qwen-future-model"]
);
});
test("HTTP-success gateway errors and unusable catalogs cannot replace the cached list", () => {
for (const payload of [
{ code: "200", data: { success: false, errorCode: "LoginRequired" } },
envelope([]),
envelope(["qwen-image-3.0-pro"]),
envelope([null]),
envelope(["qwen-valid", "../bad"]),
"<html>login</html>",
]) {
assert.throws(() => parseTokenPlanCatalog(payload));
}
});