diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index e3c4333104..6ebe96d371 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -364,7 +364,7 @@ promotionnels de Cline ne sont accessibles que dans l'IDE ou la CLI Cline, pas p | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | | [BigModel Coding Plan — Responses (liste statique)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | -| Qwen Cloud | Forfait à jetons (par défaut) : `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Facturation à l'usage : `https://dashscope.aliyuncs.com/compatible-mode/v1` · ou personnalisé | +| Qwen Cloud | Forfait à jetons (par défaut) : `https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` · Facturation à l'usage : `https://dashscope.aliyuncs.com/compatible-mode/v1` · ou personnalisé | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | | Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` | diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 79b628aa78..e0a7cbd473 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -739,7 +739,7 @@ region-pinned EU routes, is at [opper.ai/models](https://opper.ai/models). Opper | Z.AI (GLM Coding) | `https://api.z.ai` — Responses at `/api/v1/responses` by default; Chat Completions at `/api/coding/paas/v4/chat/completions` per model through `modelAdapters` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | | BigModel Coding Plan (Responses, static roster) | `https://open.bigmodel.cn/api/v1` | -| Qwen Cloud | Token plan (default): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · or Custom | +| Qwen Cloud | Token plan (default): `https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · or Custom | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | | Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` | @@ -754,6 +754,26 @@ region-pinned EU routes, is at [opper.ai/models](https://opper.ai/models). Opper | Cloudflare AI Gateway | `https://gateway.ai.cloudflare.com/v1/{account-id}/{gateway}/anthropic` | | …and more | opencode zen, Vercel AI Gateway, Venice, NanoGPT, Synthetic, Qianfan, Alibaba, Parallel, ZenMux, LiteLLM | +**Alibaba Token Plan** provides Beijing Personal Edition and International Team Edition +presets. Both regions offer both subscription tiers. The presets keep static chat-model +lists because upstream discovery also includes non-chat models. Existing selections and +explicit International endpoint overrides are preserved. + +Both presets include `deepseek-v4.1-flash-1017` with text/image input, text output, +1,000,000 context and `low`/`high`/`max` reasoning (default `high`). Its 393,216 output +budget follows the existing published-budget normalization, not a measured boundary; +the lab lists 384,000. Strict schemas reject and `json_object` is unverified for this +exact ID, so Chat requests omit `response_format`. This is best-effort text, not +guaranteed JSON. The bare model's verified JSON downgrade and existing Responses +pins are unchanged. Beijing also includes now-listed `deepseek-v4-pro-0813`; +International omits retired `deepseek-v3.2` and bare `deepseek-v4-flash` but retains +`deepseek-v4-flash-0731`. The canonical Beijing host is +`https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1`; International uses the +Qwen Cloud Token Plan host above. Historical saved hosts remain recognized. +See the [1017 model card](https://www.qianwenai.com/models/deepseek-v4.1-flash-1017), +[Chat reference](https://docs.qwencloud.com/api-reference/chat/openai-chat) and +[Personal overview](https://help.aliyun.com/en/model-studio/token-plan-personal-overview). + **OpenGateway** is an OpenAI-compatible gateway operated by Sionic AI at `https://apis.opengateway.ai/v1`. Its public catalog contains about 80 active models (as verified on 2026-10-02); the preset automatically refreshes the live model list from diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 6e06678477..1c85022ea9 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -260,7 +260,7 @@ Cline IDE/CLI のみで API からは使えません。`minimax/minimax-m2.5` | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | | [BigModel Coding Plan — Responses (静的モデル一覧)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | -| Qwen Cloud | トークンプラン(デフォルト): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 従量課金: `https://dashscope.aliyuncs.com/compatible-mode/v1` · またはカスタム | +| Qwen Cloud | トークンプラン(デフォルト): `https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` · 従量課金: `https://dashscope.aliyuncs.com/compatible-mode/v1` · またはカスタム | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | | Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` | diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index ce40892e76..a413e23721 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -265,7 +265,7 @@ Cline IDE/CLI에서만 제공되며 API로는 사용할 수 없습니다. `minim | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | | [BigModel Coding Plan — Responses (정적 모델 목록)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | -| Qwen Cloud | Token plan(기본): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 종량제: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 또는 사용자 지정 | +| Qwen Cloud | Token plan(기본): `https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` · 종량제: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 또는 사용자 지정 | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | | Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` | diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 6d3a795542..30962e743b 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -275,7 +275,7 @@ opencodex поставляется с 104 встроенными пресета | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | | [BigModel Coding Plan — Responses (статический список)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | -| Qwen Cloud | Token plan (по умолчанию): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · или Custom | +| Qwen Cloud | Token plan (по умолчанию): `https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · или Custom | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | | Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` | diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index 317a6b6b50..388be9afe1 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -402,7 +402,7 @@ yalnızca Cline IDE/CLI içinde mevcuttur; `minimax/minimax-m2.5` belgelenmiş A | Z.AI (GLM Kodlama) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | | [BigModel Coding Plan — Responses (statik model listesi)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | -| Qwen Cloud | Token planı (varsayılan): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Kullandıkça öde: `https://dashscope.aliyuncs.com/compatible-mode/v1` · veya Özel | +| Qwen Cloud | Token planı (varsayılan): `https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` · Kullandıkça öde: `https://dashscope.aliyuncs.com/compatible-mode/v1` · veya Özel | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | | Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` | diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index ef88d8c183..2f12506a3c 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -254,7 +254,7 @@ Cline IDE/CLI 中提供,不能通过 API 使用;`minimax/minimax-m2.5` 是 | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | 智谱 AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | | [BigModel Coding Plan — Responses (静态模型列表)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | -| Qwen Cloud | Token plan(默认): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 按量付费: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 或自定义 | +| Qwen Cloud | Token plan(默认): `https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` · 按量付费: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 或自定义 | | 腾讯云 Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | | 火山方舟 · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` | diff --git a/docs-site/src/content/docs/zh-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md index 9be3a8c2aa..dea1e1d782 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -316,7 +316,7 @@ IDE/CLI,不透過 API;`minimax/minimax-m2.5` 是文件列出的 API 免費 | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | | [BigModel Coding Plan — Responses (靜態模型清單)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | -| Qwen Cloud | Token plan(預設):`https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · pay as you go:`https://dashscope.aliyuncs.com/compatible-mode/v1` · 或 Custom | +| Qwen Cloud | Token plan(預設):`https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` · pay as you go:`https://dashscope.aliyuncs.com/compatible-mode/v1` · 或 Custom | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | | Volcengine Ark · Coding Plan · Agent Plan | `https://ark.cn-beijing.volces.com/api/v3` · `https://ark.cn-beijing.volces.com/api/coding/v3` · `https://ark.cn-beijing.volces.com/api/plan/v3` | diff --git a/src/providers/alibaba-region-migration.ts b/src/providers/alibaba-region-migration.ts index 2f359059c4..9f942d7a32 100644 --- a/src/providers/alibaba-region-migration.ts +++ b/src/providers/alibaba-region-migration.ts @@ -1,4 +1,3 @@ -import { ALIBABA_INTL_BASE_URL_CHOICES } from "./base-url-choices"; import { providerConfigSeed } from "./derive"; import { rewriteProviderReferences } from "./provider-id-rewrite"; import { PROVIDER_REGISTRY } from "./registry"; @@ -16,7 +15,7 @@ const INTL_ID = "alibaba-token-plan-intl"; * while advertising the six-model Beijing Personal Edition catalog. * * `baseUrl` is carried deliberately: the migration only fires for a URL that - * `ALIBABA_INTL_BASE_URL_CHOICES` recognises, so it is nonblank and + * the International registry entry recognises, so it is nonblank and * placeholder-free and satisfies the override guard in the router. * `liveModels` is carried because it is directly user-editable, so resetting it * to the seed's `false` would silently undo a deliberate choice. @@ -36,9 +35,11 @@ const normalize = (url: string): string => url.trim().replace(/\/+$/, "").toLowe /** True when the saved URL is one the international entry actually serves. */ function isInternationalEndpoint(baseUrl: string): boolean { - return ALIBABA_INTL_BASE_URL_CHOICES.some( - choice => choice.baseUrl && normalize(choice.baseUrl) === normalize(baseUrl), - ); + const entry = PROVIDER_REGISTRY.find(row => row.id === INTL_ID); + if (!entry) return false; + const candidates = [entry.baseUrl, ...(entry.baseUrlChoices ?? []).map(choice => choice.baseUrl), + ...(entry.destinationAliases ?? []).filter(alias => alias.adapter === entry.adapter).map(alias => alias.baseUrl)]; + return candidates.some(url => url && normalize(url) === normalize(baseUrl)); } /** diff --git a/src/providers/base-url-choices.ts b/src/providers/base-url-choices.ts index 43a533059c..f1a808422a 100644 --- a/src/providers/base-url-choices.ts +++ b/src/providers/base-url-choices.ts @@ -3,7 +3,7 @@ * Token plan is the GUI default; pay-as-you-go and custom are selectable. */ export const QWEN_CLOUD_TOKEN_PLAN_BASE_URL = - "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"; + "https://token-plan.maas.qwencloudapi.com/compatible-mode/v1"; export const QWEN_CLOUD_PAYG_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"; @@ -23,13 +23,13 @@ export const QWEN_CLOUD_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [ /** * Alibaba Token Plan International (ap-southeast-1) endpoint presets. - * Same product as the Beijing Token Plan but for international accounts. + * Separate regional account and billing destination; both regions offer both tiers. * Note: ALIBABA_INTL_TOKEN_PLAN_BASE_URL intentionally duplicates * QWEN_CLOUD_TOKEN_PLAN_BASE_URL — same host, different product branding - * and model lineup. Kept as a separate constant for clarity. + * and preset curation. Kept as a separate constant for clarity. */ export const ALIBABA_INTL_TOKEN_PLAN_BASE_URL = - "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"; + "https://token-plan.maas.qwencloudapi.com/compatible-mode/v1"; export const ALIBABA_INTL_PAYG_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"; diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 5e01ebb876..bbb67c2701 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -3,6 +3,7 @@ import { COST_VENDOR_PRIORITY, getModelMetadata, getModelMetadataCaseInsensitive import { cloneFastWire } from "./fastwire"; import { isAzureModelMetadataDestination, publishedAzureModelMetadata, type AzureModelMetadata } from "./azure-model-metadata"; import { resolveModelPolicy } from "./resolved-model-policy"; +import { mapFill, nestedMapFill, stableUnion } from "./resolved-model-policy-merge"; import { PROVIDER_REGISTRY, registryEntryForProviderDestination, @@ -470,13 +471,22 @@ function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistry * was skipped and the reasoning ladder was advertised without summary support — exactly the * inconsistency that makes Codex drop the inbound reasoning object. * - * Deliberately narrow: reasoning-summary, effort, and replay-compatibility metadata only, via + * Deliberately narrow: reasoning-summary, effort, structured-output, and replay-compatibility metadata only, via * `registryEntryForProviderDestination`, which matches fixed key destinations and refuses * templated or overridable base URLs. A custom row keeps its own identity for everything else. */ function enrichReasoningMetadataByDestination(prov: OcxProviderConfig): void { const destination = registryEntryForProviderDestination(prov); applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries); + if (destination?.modelDefaultReasoningEfforts) { + prov.modelDefaultReasoningEfforts = mapFill(destination.modelDefaultReasoningEfforts, prov.modelDefaultReasoningEfforts)[0]; + } + if (destination?.modelReasoningEffortMap) { + prov.modelReasoningEffortMap = nestedMapFill(destination.modelReasoningEffortMap, prov.modelReasoningEffortMap)[0]; + } + if (destination?.noStructuredOutputModels?.length) { + prov.noStructuredOutputModels = stableUnion(destination.noStructuredOutputModels, prov.noStructuredOutputModels)[0]; + } if (prov.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined) { prov.dropResponsesReasoningItems = destination.dropResponsesReasoningItems; } diff --git a/src/providers/model-rename-migration.ts b/src/providers/model-rename-migration.ts index 37a778766a..9ad05d3a72 100644 --- a/src/providers/model-rename-migration.ts +++ b/src/providers/model-rename-migration.ts @@ -256,7 +256,10 @@ function providerStillMatchesRegistry(name: string, prov: OcxProviderConfig): bo if (!entry) return false; if (!prov.baseUrl || !entry.baseUrl) return true; const choices = entry.baseUrlChoices?.map(choice => choice.baseUrl) ?? []; - const known = [entry.baseUrl, ...choices] + // Destination aliases preserve saved hosts without duplicating registry metadata. + const aliases = entry.destinationAliases?.filter(alias => alias.adapter === prov.adapter) + .map(alias => alias.baseUrl) ?? []; + const known = [entry.baseUrl, ...choices, ...aliases] .filter((url): url is string => typeof url === "string") .map(normalizedProviderEndpoint); return known.includes(normalizedProviderEndpoint(prov.baseUrl)); diff --git a/src/providers/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index 1fd4039425..96aeac789c 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -837,7 +837,8 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ { id: "alibaba-token-plan", label: "Alibaba Token Plan (Beijing)", - baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", + baseUrl: "https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1", + destinationAliases: [{ baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", adapter: "openai-chat" }], adapter: "openai-chat", authKind: "key", dashboardUrl: "https://bailian.console.aliyun.com/cn-beijing?tab=plan", @@ -887,19 +888,23 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ "deepseek-v4-pro-0813": deepseekThinkingEffortsFor("deepseek-v4-pro-0813"), "deepseek-v4-flash-0731": deepseekThinkingEffortsFor("deepseek-v4-flash-0731"), "deepseek-v4.1-flash": deepseekThinkingEffortsFor("deepseek-v4.1-flash"), + "deepseek-v4.1-flash-1017": deepseekThinkingEffortsFor("deepseek-v4.1-flash-1017"), }, modelReasoningEffortMap: { "deepseek-v4-pro": deepseekReasoningMapFor("deepseek-v4-pro"), "deepseek-v4-pro-0813": deepseekReasoningMapFor("deepseek-v4-pro-0813"), "deepseek-v4-flash-0731": deepseekReasoningMapFor("deepseek-v4-flash-0731"), "deepseek-v4.1-flash": deepseekReasoningMapFor("deepseek-v4.1-flash"), + "deepseek-v4.1-flash-1017": deepseekReasoningMapFor("deepseek-v4.1-flash-1017"), }, // Probed 260915 on the plan gateway: json_object returns valid JSON, strict // json_schema is rejected 400 ("This response_format type is unavailable now") // in both thinking modes, so requests downgrade to json_object rather than // sending a schema the gateway refuses. noJsonSchemaModels: ["deepseek-v4.1-flash"], - modelDefaultReasoningEfforts: Object.fromEntries(QWEN38_FAMILY.map(id => [id, "xhigh"])), + // 261020 strict schema rejected; json_object unverified. Omit rather than invent a fallback. + noStructuredOutputModels: ["deepseek-v4.1-flash-1017"], + modelDefaultReasoningEfforts: { ...Object.fromEntries(QWEN38_FAMILY.map(id => [id, "xhigh"])), "deepseek-v4.1-flash-1017": "high" }, directReasoningEffortModels: QWEN38_FAMILY, thinkingBudgetModels: ALIBABA_TOKEN_PLAN_QWEN_MODELS.filter(id => !QWEN38_FAMILY.includes(id)), preserveReasoningContentModels: ALIBABA_TOKEN_PLAN_PRESERVE_REASONING, @@ -915,6 +920,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ id: "alibaba-token-plan-intl", label: "Alibaba Token Plan (International)", baseUrl: ALIBABA_INTL_TOKEN_PLAN_BASE_URL, + destinationAliases: [{ baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", adapter: "openai-chat" }], adapter: "openai-chat", authKind: "key", allowBaseUrlOverride: true, @@ -935,25 +941,27 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ "glm-5.3": ZAI_GLM_53_REASONING_EFFORTS, "deepseek-v4-pro": deepseekThinkingEffortsFor("deepseek-v4-pro"), "deepseek-v4-pro-0813": deepseekThinkingEffortsFor("deepseek-v4-pro-0813"), - "deepseek-v4-flash": deepseekThinkingEffortsFor("deepseek-v4-flash"), "deepseek-v4-flash-0731": deepseekThinkingEffortsFor("deepseek-v4-flash-0731"), "deepseek-v4.1-flash": deepseekThinkingEffortsFor("deepseek-v4.1-flash"), + "deepseek-v4.1-flash-1017": deepseekThinkingEffortsFor("deepseek-v4.1-flash-1017"), }, modelReasoningEffortMap: { "deepseek-v4-pro": deepseekReasoningMapFor("deepseek-v4-pro"), "deepseek-v4-pro-0813": deepseekReasoningMapFor("deepseek-v4-pro-0813"), - "deepseek-v4-flash": deepseekReasoningMapFor("deepseek-v4-flash"), "deepseek-v4-flash-0731": deepseekReasoningMapFor("deepseek-v4-flash-0731"), "deepseek-v4.1-flash": deepseekReasoningMapFor("deepseek-v4.1-flash"), + "deepseek-v4.1-flash-1017": deepseekReasoningMapFor("deepseek-v4.1-flash-1017"), }, // Same 260915 json_schema rejection probe as the Beijing entry. noJsonSchemaModels: ["deepseek-v4.1-flash"], + // 261020 strict schema rejected; json_object unverified. Omit rather than invent a fallback. + noStructuredOutputModels: ["deepseek-v4.1-flash-1017"], directReasoningEffortModels: QWEN38_FAMILY, thinkingBudgetModels: ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.filter(id => !QWEN38_FAMILY.includes(id)), preserveReasoningContentModels: ALIBABA_TOKEN_PLAN_PRESERVE_REASONING, noVisionModels: ALIBABA_TOKEN_PLAN_NO_VISION, - noReasoningModels: ["kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", "deepseek-v3.2", "glm-5.1", "glm-5", "MiniMax-M2.5"], - modelDefaultReasoningEfforts: Object.fromEntries(QWEN38_FAMILY.map(id => [id, "xhigh"])), + noReasoningModels: ["kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", "glm-5.1", "glm-5", "MiniMax-M2.5"], + modelDefaultReasoningEfforts: { ...Object.fromEntries(QWEN38_FAMILY.map(id => [id, "xhigh"])), "deepseek-v4.1-flash-1017": "high" }, promptCacheKey: true, }, // NEEDS_HUMAN 2026-07-10: kept for config compatibility, but this is a dashboard URL, diff --git a/src/providers/registry/model-ids.ts b/src/providers/registry/model-ids.ts index 8fb7603d81..b5d4b59e68 100644 --- a/src/providers/registry/model-ids.ts +++ b/src/providers/registry/model-ids.ts @@ -114,6 +114,7 @@ export const REGISTRY_FIELD_MODEL_ID_ROLES = { noStopModels: NONE, noPenaltyModels: NONE, noJsonSchemaModels: NONE, + noStructuredOutputModels: NONE, parallelToolCalls: NONE, promptCacheKey: NONE, chatServiceTier: NONE, diff --git a/src/providers/registry/model-seeds.ts b/src/providers/registry/model-seeds.ts index 836f8751ab..674b37fe60 100644 --- a/src/providers/registry/model-seeds.ts +++ b/src/providers/registry/model-seeds.ts @@ -573,22 +573,22 @@ export const deepseekReasoningMapFor = (modelId: string): Record // text-only input, strict json_schema accepted. glm-5.3-flash REMAINS OUT: // still never served by the Token Plan gateway (docs.z.ai VLM id, not plan // entitlement). -// The Beijing preset keeps the Personal Edition subset; non-chat ids (audio/image/ -// video families) stay out: they answer only on async endpoints openai-chat cannot -// reach. deepseek-v4-pro-0813 is callable but NOT listed by /models, which is the -// reason liveModels must stay false for this provider. deepseek-v4.1-flash is the -// 260910 DeepSeek rename row: listed on /models on both tiers and regions from 260915, -// hybrid thinking, vision via user message and tool result, json_object but not -// json_schema (see noJsonSchemaModels on the entries). -// Beijing serves the Personal Edition, so this is the Personal-tier roster probed -// 260909 (a strict subset of Team). deepseek-v4-pro-0813 stays out of the Beijing -// entry: its callability is only proven on Team keys, and no Personal key has been -// shown to reach it. The Beijing entry also shares the intl maps, so it carries a -// few orphan keys (kimi/glm-5/MiniMax rows); harmless, and one map beats two -// drifting ones. +// Both regions support Personal and Team subscriptions. Keep the existing Beijing +// Personal and International Team model rosters. Discovery stays disabled because +// the upstream list includes non-chat models. +// 261020: 0813 is now listed on Global Personal/Team and CN Team, and the CN Personal +// overview documents entitlement. Add it to the existing Personal curation without +// claiming new Personal inference. Retired 261011: deepseek-v3.2 and bare v4-flash. +// 1017 evidence: https://www.qianwenai.com/models/deepseek-v4.1-flash-1017 and +// https://docs.qwencloud.com/api-reference/chat/openai-chat (checked 2026-10-20). +// Global Personal probes: completion, thinking-off, low effort, tools, user/tool images +// and Responses succeed; strict json_schema rejects 400. json_object and replay are +// untested on this exact id. Published 1M context and 393K output use 393216 here, +// consistent with the existing plan budget convention, not a measured boundary; +// the lab's 384000 precision differs. Do not inherit the bare model's JSON fallback. export const ALIBABA_TOKEN_PLAN_MODELS = [ "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash", - "deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3", + "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v4.1-flash-1017", "glm-5.2", "glm-5.3", ]; export const ALIBABA_TOKEN_PLAN_QWEN_MODELS = [ "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash", @@ -604,12 +604,13 @@ export const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record = { "deepseek-v4-flash-0731": ["text"], // Vision probed on the plan gateway 260915 (user message and tool result, both 200). "deepseek-v4.1-flash": ["text", "image"], + "deepseek-v4.1-flash-1017": ["text", "image"], "glm-5.2": ["text"], "glm-5.3": ["text"], }; // 260721 Alibaba Token Plan International (ap-southeast-1 / Singapore, hardened 260721). -// Multi-vendor lineup distinct from Beijing — includes DeepSeek V4 flash, Kimi K2.7, MiniMax. +// International Team model roster; tier and region are independent. // Evidence: https://www.alibabacloud.com/help/en/model-studio/token-plan-overview // https://qwencloud.com/pricing/token-plan (qwen3.8 metadata) // The Team Edition roster (Singapore), verified identical to the CN Team set on 260909. @@ -617,7 +618,7 @@ export const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record = { // listed on /models on both regions) after being dropped as "retired" upstream. export const ALIBABA_INTL_TOKEN_PLAN_MODELS = [ "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash", - "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v3.2", + "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v4.1-flash-1017", "kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", "glm-5.2", "glm-5.3", "glm-5.1", "glm-5", "MiniMax-M2.5", @@ -702,8 +703,6 @@ export const VOLCENGINE_PLAN_TEXT_ONLY_MODELS = [ export const ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES: Record = { ...ALIBABA_TOKEN_PLAN_INPUT_MODALITIES, "qwen3.6-plus": ["text", "image"], - "deepseek-v4-flash": ["text"], - "deepseek-v3.2": ["text"], "kimi-k2.7-code": ["text", "image"], "kimi-k2.6": ["text", "image"], "kimi-k2.5": ["text", "image"], @@ -712,14 +711,14 @@ export const ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES: Record "MiniMax-M2.5": ["text"], }; -// Shared Token Plan metadata (260909 gateway probes; output ceilings are max_tokens -// boundary probes: accept at N, reject at N+1). +// Shared Token Plan metadata: legacy rows use 260909 max_tokens boundary probes. +// The 1017 row uses published card normalization, not a boundary probe (see above). export const QWEN38_FAMILY = ["qwen3.8-max", "qwen3.8-flash"]; export const ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS: Record = { "qwen3.8-max": 1_000_000, "qwen3.8-flash": 1_000_000, "qwen3.7-max": 1_000_000, "qwen3.7-plus": 1_000_000, "qwen3.6-plus": 1_000_000, "qwen3.6-flash": 1_000_000, - "deepseek-v4-pro": 1_000_000, "deepseek-v4-pro-0813": 1_000_000, "deepseek-v4-flash": 1_000_000, - "deepseek-v4-flash-0731": 1_000_000, "deepseek-v4.1-flash": 1_000_000, "deepseek-v3.2": 131_072, + "deepseek-v4-pro": 1_000_000, "deepseek-v4-pro-0813": 1_000_000, + "deepseek-v4-flash-0731": 1_000_000, "deepseek-v4.1-flash": 1_000_000, "deepseek-v4.1-flash-1017": 1_000_000, "kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 262_144, "glm-5.2": 1_000_000, "glm-5.3": 1_000_000, "glm-5.1": 202_752, "glm-5": 202_752, "MiniMax-M2.5": 196_608, @@ -727,20 +726,20 @@ export const ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS: Record = { export const ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS: Record = { "qwen3.8-max": 131_072, "qwen3.8-flash": 131_072, "qwen3.7-max": 131_072, "qwen3.7-plus": 131_072, "qwen3.6-plus": 65_536, "qwen3.6-flash": 65_536, - "deepseek-v4-pro": 393_216, "deepseek-v4-pro-0813": 393_216, "deepseek-v4-flash": 393_216, - "deepseek-v4-flash-0731": 393_216, "deepseek-v4.1-flash": 393_216, "deepseek-v3.2": 65_536, + "deepseek-v4-pro": 393_216, "deepseek-v4-pro-0813": 393_216, + "deepseek-v4-flash-0731": 393_216, "deepseek-v4.1-flash": 393_216, "deepseek-v4.1-flash-1017": 393_216, "kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 98_304, "glm-5.2": 131_072, "glm-5.3": 131_072, "glm-5.1": 128_000, "glm-5": 16_384, "MiniMax-M2.5": 32_768, }; export const ALIBABA_TOKEN_PLAN_NO_VISION = [ - "qwen3.7-max", "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", - "deepseek-v4-flash-0731", "deepseek-v3.2", "glm-5.2", "glm-5.3", "glm-5.1", "glm-5", "MiniMax-M2.5", + "qwen3.7-max", "deepseek-v4-pro", "deepseek-v4-pro-0813", + "deepseek-v4-flash-0731", "glm-5.2", "glm-5.3", "glm-5.1", "glm-5", "MiniMax-M2.5", ]; export const ALIBABA_TOKEN_PLAN_PRESERVE_REASONING = [ "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash", - "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731", - "deepseek-v4.1-flash", "glm-5.2", "glm-5.3", + "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash-0731", + "deepseek-v4.1-flash", "deepseek-v4.1-flash-1017", "glm-5.2", "glm-5.3", ]; // 260717 Kimi K3: the subscription endpoint uses one upstream id (`k3`) for both diff --git a/src/providers/registry/types.ts b/src/providers/registry/types.ts index f5939c9860..8eadae4586 100644 --- a/src/providers/registry/types.ts +++ b/src/providers/registry/types.ts @@ -334,6 +334,8 @@ export interface ProviderRegistryEntry { * `directReasoningEffortModels` above is registry-owned. */ noJsonSchemaModels?: string[]; + /** Exact models with no established structured-output wire format. Registry-owned at routing. */ + noStructuredOutputModels?: string[]; /** Opt this provider into parallel tool calls (see OcxProviderConfig.parallelToolCalls). */ parallelToolCalls?: boolean; /** Opt this provider into forwarding prompt_cache_key (OpenAI-specific; strict backends reject it). */ diff --git a/src/providers/resolved-model-policy.ts b/src/providers/resolved-model-policy.ts index e2cf956e9d..f18edc1e7c 100644 --- a/src/providers/resolved-model-policy.ts +++ b/src/providers/resolved-model-policy.ts @@ -31,7 +31,7 @@ export type StaticProviderPolicyField = | "modelMaxOutputTokens" | "reasoningEfforts" | "modelReasoningEfforts" | "modelReasoningEffortsAuthoritative" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat" | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" - | "noTopPModels" | "noStopModels" | "noPenaltyModels" | "noJsonSchemaModels" | "parallelToolCalls" + | "noTopPModels" | "noStopModels" | "noPenaltyModels" | "noJsonSchemaModels" | "noStructuredOutputModels" | "parallelToolCalls" | "promptCacheKey" | "chatServiceTier" | "openaiChatEofTolerance" | "statelessResponses" | "requiresAdjacentResponsesToolResults" | "requiresPairedResponsesToolResults" | "annotateEmptyToolOutputs" | "fastWire" | "supportsServiceTier" | "modelSupportsServiceTier" | "supportsOpenAiWebSearchToolFields" @@ -231,7 +231,7 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode for (const key of [ "noVisionModels", "noReasoningModels", "noTemperatureModels", "noTopPModels", "noStopModels", - "noPenaltyModels", "noJsonSchemaModels", "autoToolChoiceOnlyModels", + "noPenaltyModels", "noJsonSchemaModels", "noStructuredOutputModels", "autoToolChoiceOnlyModels", "preserveReasoningContentModels", "requiresReasoningPlaceholderModels", "reasoningSplitModels", "reasoningDetailsModels", "thinkingToggleModels", "thinkingBudgetModels", ] as const) putUnion(key, entry?.[key]); diff --git a/src/providers/routed-config.ts b/src/providers/routed-config.ts index df6f20cd0c..7189623ad8 100644 --- a/src/providers/routed-config.ts +++ b/src/providers/routed-config.ts @@ -21,6 +21,7 @@ import { providerSupportsLiveModelDiscovery, } from "./static-model-discovery"; import { resolveModelPolicy } from "./resolved-model-policy"; +import { mapFill, nestedMapFill, stableUnion } from "./resolved-model-policy-merge"; /** Same endpoint modulo surrounding space and trailing slashes — matches `matchBaseUrlChoice`. */ function isSameEndpoint(a: string, b: string): boolean { @@ -124,6 +125,15 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey), + ...(destination?.modelDefaultReasoningEfforts ? { + modelDefaultReasoningEfforts: mapFill(destination.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts)[0], + } : {}), + ...(destination?.modelReasoningEffortMap ? { + modelReasoningEffortMap: nestedMapFill(destination.modelReasoningEffortMap, provider.modelReasoningEffortMap)[0], + } : {}), + ...(destination?.noStructuredOutputModels?.length ? { + noStructuredOutputModels: stableUnion(destination.noStructuredOutputModels, provider.noStructuredOutputModels)[0], + } : {}), ...(provider.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined ? { dropResponsesReasoningItems: destination.dropResponsesReasoningItems } : {}), @@ -172,6 +182,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider const noStopModels = staticPolicy.noStopModels; const noPenaltyModels = staticPolicy.noPenaltyModels; const noJsonSchemaModels = staticPolicy.noJsonSchemaModels; + const noStructuredOutputModels = staticPolicy.noStructuredOutputModels; const autoToolChoiceOnlyModels = staticPolicy.autoToolChoiceOnlyModels; const preserveReasoningContentModels = staticPolicy.preserveReasoningContentModels; const requiresReasoningPlaceholderModels = staticPolicy.requiresReasoningPlaceholderModels; @@ -325,6 +336,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider ...(noStopModels ? { noStopModels } : {}), ...(noPenaltyModels ? { noPenaltyModels } : {}), ...(noJsonSchemaModels ? { noJsonSchemaModels } : {}), + ...(noStructuredOutputModels ? { noStructuredOutputModels } : {}), ...(autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels } : {}), ...(preserveReasoningContentModels ? { preserveReasoningContentModels } : {}), ...(requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels } : {}), diff --git a/src/providers/stale-model-roster-migration.ts b/src/providers/stale-model-roster-migration.ts index bdb6a07cbd..69c031de27 100644 --- a/src/providers/stale-model-roster-migration.ts +++ b/src/providers/stale-model-roster-migration.ts @@ -17,6 +17,9 @@ */ import { PROVIDER_REGISTRY } from "./registry"; import { + ALIBABA_TOKEN_PLAN_MODELS, + ALIBABA_INTL_TOKEN_PLAN_MODELS, + ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS, MINIMAX_M31_DEFAULT_REASONING_EFFORT, MINIMAX_M31_FLASH_PREVIEW, MINIMAX_MODELS, @@ -42,17 +45,39 @@ export interface StaleModelRosterProjection { warnings: string[]; } +const ALIBABA_TOKEN_PLAN_MODELS_BEFORE_1017 = [ + "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash", + "deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3", +]; +const ALIBABA_INTL_TOKEN_PLAN_MODELS_BEFORE_1017 = [ + "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash", + "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v3.2", + "kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", + "glm-5.2", "glm-5.3", "glm-5.1", "glm-5", + "MiniMax-M2.5", +]; + /** * MiniMax-M3.1-Flash-Preview (2026-09-30). MiniMax's /v1/models roster does not list the * preview, so live discovery cannot add it; the catalog keeps it only when it is configured. */ -export const STALE_MODEL_ROSTERS: readonly StaleModelRoster[] = ["minimax", "minimax-cn"].map(provider => ({ - provider, - from: MINIMAX_MODELS_BEFORE_M31, - to: MINIMAX_MODELS, - contextWindows: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_MODEL_CONTEXT_WINDOWS[MINIMAX_M31_FLASH_PREVIEW]! }, - defaultReasoningEfforts: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT }, -})); +export const STALE_MODEL_ROSTERS: readonly StaleModelRoster[] = [ + ...["minimax", "minimax-cn"].map(provider => ({ + provider, + from: MINIMAX_MODELS_BEFORE_M31, + to: MINIMAX_MODELS, + contextWindows: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_MODEL_CONTEXT_WINDOWS[MINIMAX_M31_FLASH_PREVIEW]! }, + defaultReasoningEfforts: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT }, + })), + ...[ + { provider: "alibaba-token-plan", from: ALIBABA_TOKEN_PLAN_MODELS_BEFORE_1017, to: ALIBABA_TOKEN_PLAN_MODELS }, + { provider: "alibaba-token-plan-intl", from: ALIBABA_INTL_TOKEN_PLAN_MODELS_BEFORE_1017, to: ALIBABA_INTL_TOKEN_PLAN_MODELS }, + ].map(entry => ({ + ...entry, + contextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS, + defaultReasoningEfforts: { "deepseek-v4.1-flash-1017": "high" }, + })), +]; function sameList(value: unknown, expected: readonly string[]): boolean { return Array.isArray(value) diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index 7eb489e8e4..1179d7fe41 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -589,3 +589,12 @@ remains authoritative. No raw body is retained in that field, serialized into cl Presence of both `detail` and `error` across the original root and its own non-array `response` record is ambiguous, regardless of values; null counts and deeper quoted objects do not. This is checked before selecting an error carrier. For complete bounded HTTP errors, the text wrapper selects root `detail`/`error` when present; otherwise it selects the own `response` record for the same exact `detail` or `error.message` match. It never searches another carrier after a nonmatch or descends further. Policy-fallback and direct callers share this wrapper; SSE message parsing disables nested HTTP selection. The same bounded HTTP parser retains allowlisted dedicated `code` fields from root and response before display truncation; diagnostic `type` is never promoted, including when code is absent or malformed. Policy/direct classification inspects full text only without authoritative carrier metadata; SSE never borrows hard codes from quoted message JSON. Committed output, cancellation, hard policy refusals and resend limits remain stronger than model fallback. + + +Token Plan dated-model policy (2026-10-20): both curated presets include exact +`deepseek-v4.1-flash-1017` metadata without changing live discovery or Responses pins. +The registry may seed `noStructuredOutputModels` at route resolution, unioned with +operator opt-outs, just as it seeds `noJsonSchemaModels`. The full opt-out wins on +both translated and native Chat paths. For 1017 this deliberately omits JSON requests: +strict schemas are rejected and a `json_object` fallback has not been verified. +The bare model's independently verified downgrade remains separate. Token Plan startup refreshes only exact previous registry rosters for both presets, adding dated models and removing retired International IDs. Edited or reordered lists, saved model defaults and existing per-model window and effort overrides remain untouched. Historical Token Plan hosts are registry `destinationAliases`; destination-based reasoning and structured-output enrichment, saved-host model renames and region repair share that metadata without rewriting saved URLs. International custom-name enrichment remains excluded by its existing override guard. diff --git a/tests/gui/alibaba-intl-token-plan.test.ts b/tests/gui/alibaba-intl-token-plan.test.ts index f645235804..3f432afc50 100644 --- a/tests/gui/alibaba-intl-token-plan.test.ts +++ b/tests/gui/alibaba-intl-token-plan.test.ts @@ -35,8 +35,7 @@ describe("alibaba-token-plan-intl registry entry", () => { // 260909 gateway re-probe (both regions, both tiers): deepseek-v4-pro is still // listed and callable on Token Plan, so the retirement drop is restored here. expect(entry!.models).toContain("deepseek-v4-pro"); - // Callable snapshots the gateway omits (or lists late) from /models, which is - // also why liveModels must stay false for this provider. + // Curated chat snapshots; liveModels stays false because discovery includes media. expect(entry!.models).toContain("qwen3.8-flash"); expect(entry!.models).toContain("deepseek-v4-pro-0813"); expect(entry!.models).toContain("deepseek-v4-flash-0731"); @@ -53,7 +52,7 @@ describe("alibaba-token-plan-intl registry entry", () => { expect(entry!.preserveReasoningContentModels).toContain("glm-5.3"); // GLM-5.3-flash remains a phantom: still never served by the Token Plan gateway. expect(entry!.models).not.toContain("glm-5.3-flash"); - expect(entry!.models!.length).toBe(20); + expect(entry!.models!.length).toBe(19); }); test("MiniMax case-insensitive normalization is set", () => { @@ -71,7 +70,7 @@ describe("alibaba-token-plan-intl registry entry", () => { test("every international chat model has an explicit context window", () => { const entry = PROVIDER_REGISTRY.find(e => e.id === "alibaba-token-plan-intl"); - expect(entry!.modelContextWindows?.["deepseek-v3.2"]).toBe(131_072); + expect(entry!.modelContextWindows?.["deepseek-v3.2"]).toBeUndefined(); expect(entry!.modelContextWindows?.["glm-5"]).toBe(202_752); expect(entry!.modelContextWindows?.["MiniMax-M2.5"]).toBe(196_608); for (const model of entry!.models ?? []) { @@ -153,7 +152,7 @@ describe("alibaba-token-plan-intl registry entry", () => { expect(entry!.noReasoningModels).toContain("kimi-k2.7-code"); expect(entry!.noReasoningModels).toContain("kimi-k2.6"); expect(entry!.noReasoningModels).toContain("kimi-k2.5"); - expect(entry!.noReasoningModels).toContain("deepseek-v3.2"); + expect(entry!.noReasoningModels).not.toContain("deepseek-v3.2"); expect(entry!.noReasoningModels).toContain("glm-5.1"); expect(entry!.noReasoningModels).toContain("glm-5"); expect(entry!.noReasoningModels).toContain("MiniMax-M2.5"); @@ -181,7 +180,7 @@ describe("alibaba-token-plan-intl registry entry", () => { const cnModels = PROVIDER_REGISTRY.find(e => e.id === "alibaba-token-plan")!.models; expect(cnModels).toEqual([ "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash", - "deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3", + "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v4.1-flash-1017", "glm-5.2", "glm-5.3", ]); // The 260910 DeepSeek rename row is wired: vision-capable, effort ladder, and the // json_schema downgrade the plan gateway needs (probed 260915). diff --git a/tests/providers/alibaba-region-migration.test.ts b/tests/providers/alibaba-region-migration.test.ts index 0406fea22b..b549c2b79f 100644 --- a/tests/providers/alibaba-region-migration.test.ts +++ b/tests/providers/alibaba-region-migration.test.ts @@ -5,6 +5,9 @@ import { join } from "node:path"; import { loadConfig, saveConfig } from "../../src/config"; import { projectAlibabaRegionMigration } from "../../src/providers/alibaba-region-migration"; import { routeModel } from "../../src/router"; +import { enrichProviderFromRegistry } from "../../src/providers/derive"; +import { registryEntryForProviderDestination } from "../../src/providers/registry"; +import type { OcxProviderConfig } from "../../src/types"; import type { OcxConfig } from "../../src/types"; import { removeTreeWithRetry } from "../helpers/remove-tree"; @@ -47,7 +50,7 @@ test("moves a Beijing entry holding an international endpoint", () => { // so a Team-Edition-only model is present and routes. expect(moved.models).toContain("kimi-k2.7-code"); expect(routeModel(projection.config, "alibaba-token-plan-intl/kimi-k2.7-code").provider.baseUrl) - .toContain("ap-southeast-1"); + .toBe(INTL_URL); }); test("the migrated config survives a reload", () => { @@ -177,3 +180,29 @@ test("carries liveModels, modelCosts, and a user-authored note, but not the Beij expect(moved.note).toBe("my own note"); expect(moved.models).toContain("kimi-k2.7-code"); }); + + +test("canonical international host is recognized while preserving the saved destination", () => { + const config = migratableConfig(); + const canonical = "https://token-plan.maas.qwencloudapi.com/compatible-mode/v1"; + config.providers["alibaba-token-plan"]!.baseUrl = canonical; + const projection = projectAlibabaRegionMigration(config); + expect(projection.changed).toBe(true); + expect(projection.config.providers["alibaba-token-plan-intl"]?.baseUrl).toBe(canonical); + expect(config.providers["alibaba-token-plan"]?.baseUrl).toBe(canonical); +}); + + +test("legacy Token Plan destinations retain reasoning metadata under custom names", () => { + for (const [id, baseUrl] of [ + ["alibaba-token-plan", "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"], + ]) { + const provider: OcxProviderConfig = { adapter: "openai-chat", baseUrl }; + expect(registryEntryForProviderDestination(provider)?.id).toBe(id); + enrichProviderFromRegistry("custom-plan", provider); + expect(provider.modelReasoningEfforts?.["deepseek-v4.1-flash-1017"]).toEqual(["low", "high", "max"]); + expect(provider.baseUrl).toBe(baseUrl); + expect(registryEntryForProviderDestination({ ...provider, adapter: "anthropic" })).toBeUndefined(); + expect(registryEntryForProviderDestination({ ...provider, baseUrl: baseUrl.replace(".com/", ".com.example.test/") })).toBeUndefined(); + } +}); diff --git a/tests/providers/alibaba-token-plan-responses-optin.test.ts b/tests/providers/alibaba-token-plan-responses-optin.test.ts index 4c7c936f59..f14d541082 100644 --- a/tests/providers/alibaba-token-plan-responses-optin.test.ts +++ b/tests/providers/alibaba-token-plan-responses-optin.test.ts @@ -17,7 +17,10 @@ * a Responses wire default must carry the Responses-side preservation beside it. */ import { afterEach, describe, expect, test } from "bun:test"; -import { providerConfigSeed } from "../../src/providers/derive"; +import { routeModel } from "../../src/router"; +import { createOpenAIChatAdapter } from "../../src/adapters/openai-chat"; +import { buildOpenAIChatPassthroughRequest } from "../../src/adapters/openai-chat/passthrough"; +import { enrichProviderFromRegistry, providerConfigSeed } from "../../src/providers/derive"; import { getProviderRegistryEntry } from "../../src/providers/registry"; import { resolveWireProtocolOverride } from "../../src/server/adapter-resolve"; import { handleResponses } from "../../src/server/responses/core"; @@ -58,7 +61,7 @@ describe("the documented Responses opt-in resolves and reaches the native wire", const resolved = resolveWireProtocolOverride("alibaba-token-plan", model, provider, "responses"); expect(resolved.adapter).toBe("openai-responses"); expect(resolved.apiKey).toBe("sk-test"); - expect(resolved.baseUrl).toBe("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"); + expect(resolved.baseUrl).toBe("https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1"); }); } @@ -110,20 +113,20 @@ describe("the opt-in survives the handleResponses replay", () => { test("qwen3.8-flash reaches /responses once opted in", async () => { const url = await drive("qwen3.8-flash", { "qwen3.8-flash": "openai-responses" }); - expect(url).toContain("token-plan.cn-beijing.maas.aliyuncs.com"); + expect(url).toContain("token-plan.maas.qianwenaiapi.com"); expect(url).toContain("/responses"); expect(url).not.toContain("chat/completions"); }); test("an unpinned family member reaches /chat/completions without the opt-in", async () => { const url = await drive("qwen3.8-max"); - expect(url).toContain("token-plan.cn-beijing.maas.aliyuncs.com"); + expect(url).toContain("token-plan.maas.qianwenaiapi.com"); expect(url).toContain("chat/completions"); }); test("an unpinned family member reaches /responses once opted in", async () => { const url = await drive("qwen3.8-max", { "qwen3.8-max": "openai-responses" }); - expect(url).toContain("token-plan.cn-beijing.maas.aliyuncs.com"); + expect(url).toContain("token-plan.maas.qianwenaiapi.com"); expect(url).toContain("/responses"); expect(url).not.toContain("chat/completions"); }); @@ -153,3 +156,131 @@ describe("a Responses default flip must carry Responses-side reasoning preservat expect(entry.preserveResponsesReasoningContent).toBe(true); }); }); + + +describe("1017 exact-ID Token Plan adapter policy", () => { + const model = "deepseek-v4.1-flash-1017"; + const schema = { type: "json_schema", name: "answer", schema: { type: "object" }, strict: true }; + for (const id of ["alibaba-token-plan", "alibaba-token-plan-intl"]) { + const entry = getProviderRegistryEntry(id)!; + const route = (overrides: Partial = {}, requestedModel = model) => routeModel({ + port: 10100, defaultProvider: id, + providers: { [id]: { ...providerConfigSeed(entry), apiKey: "sk-test", ...overrides } }, + } as OcxConfig, `${id}/${requestedModel}`); + + test(`${id} routes the documented ladder and high default without a Responses pin`, () => { + const resolved = route(); + expect(resolved.provider.modelReasoningEfforts?.[model]).toEqual(["low", "high", "max"]); + expect(resolved.provider.modelDefaultReasoningEfforts?.[model]).toBe("high"); + expect(resolved.provider.modelInputModalities?.[model]).toEqual(["text", "image"]); + expect(resolved.provider.modelContextWindows?.[model]).toBe(1_000_000); + expect(resolved.provider.modelMaxOutputTokens?.[model]).toBe(393_216); + for (const inbound of INBOUNDS) { + expect(resolveWireProtocolOverride(id, model, resolved.provider, inbound).adapter).toBe("openai-chat"); + } + for (const [effort, expected] of [["low", "low"], ["medium", "high"], ["xhigh", "high"], ["max", "max"]]) { + const request = createOpenAIChatAdapter(resolved.provider).buildRequest({ + modelId: model, context: { messages: [{ role: "user", content: "hi", timestamp: 0 }] }, + options: { reasoning: effort }, stream: false, + }); + const body = JSON.parse(request.body as string); + expect(body.reasoning_effort).toBe(expected); + expect(body.thinking_budget).toBeUndefined(); + } + }); + + test(`${id} omits unknown structured-output formats on both Chat paths`, () => { + const resolved = route({ noStructuredOutputModels: ["operator-model", model] }); + expect(resolved.provider.noStructuredOutputModels).toEqual([model, "operator-model"]); + expect(resolved.provider.noJsonSchemaModels).not.toContain(model); + for (const format of [schema, { type: "json_object" }]) { + const request = createOpenAIChatAdapter(resolved.provider).buildRequest({ + modelId: model, context: { messages: [{ role: "user", content: "hi", timestamp: 0 }] }, + options: { textFormat: format }, stream: false, + }); + expect(JSON.parse(request.body as string).response_format).toBeUndefined(); + const native = buildOpenAIChatPassthroughRequest(resolved.provider, { + messages: [{ role: "user", content: "hi" }], response_format: format, + }, model, false); + expect(JSON.parse(native.body as string).response_format).toBeUndefined(); + } + // Existing bare-ID evidence stays separate: its verified JSON fallback still applies. + const bare = route({}, "deepseek-v4.1-flash"); + const request = createOpenAIChatAdapter(bare.provider).buildRequest({ + modelId: bare.modelId, context: { messages: [{ role: "user", content: "hi", timestamp: 0 }] }, + options: { textFormat: schema }, stream: false, + }); + expect(JSON.parse(request.body as string).response_format).toEqual({ type: "json_object" }); + }); + } +}); + + +describe("custom Beijing destination structured-output policy", () => { + const model = "deepseek-v4.1-flash-1017"; + for (const baseUrl of [ + "https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1", + "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", + ]) { + for (const enriched of [false, true]) { + test(`${baseUrl} enriched=${enriched} preserves destination opt-outs`, () => { + const provider: OcxProviderConfig = { + adapter: "openai-chat", authMode: "key", apiKey: "test-key", baseUrl, + noStructuredOutputModels: ["operator-model"], + }; + if (enriched) enrichProviderFromRegistry("custom-beijing", provider); + const resolved = routeModel({ port: 10100, defaultProvider: "custom-beijing", + providers: { "custom-beijing": provider } } as OcxConfig, `custom-beijing/${model}`); + expect(resolved.provider.modelDefaultReasoningEfforts?.[model]).toBe("high"); + expect(resolved.provider.modelReasoningEffortMap?.[model]?.medium).toBe("high"); + if (enriched) { + expect(provider.modelDefaultReasoningEfforts?.[model]).toBe("high"); + expect(provider.modelReasoningEffortMap?.[model]?.medium).toBe("high"); + } + const effort = createOpenAIChatAdapter(resolved.provider).buildRequest({ + modelId: model, context: { messages: [{ role: "user", content: "hi", timestamp: 0 }] }, + options: { reasoning: "medium" }, stream: false, + }); + expect(JSON.parse(effort.body as string).reasoning_effort).toBe("high"); + expect(resolved.provider.noStructuredOutputModels).toContain("operator-model"); + expect(resolved.provider.noStructuredOutputModels).toContain(model); + for (const format of [{ type: "json_object" }, { type: "json_schema", name: "answer", schema: { type: "object" }, strict: true }]) { + const native = buildOpenAIChatPassthroughRequest(resolved.provider, { + messages: [{ role: "user", content: "hi" }], response_format: format, + }, model, false); + expect(JSON.parse(native.body as string).response_format).toBeUndefined(); + const translated = createOpenAIChatAdapter(resolved.provider).buildRequest({ + modelId: model, context: { messages: [{ role: "user", content: "hi", timestamp: 0 }] }, + options: { textFormat: format }, stream: false, + }); + expect(JSON.parse(translated.body as string).response_format).toBeUndefined(); + } + expect(resolved.provider.noStructuredOutputModels).toEqual([model, "operator-model"]); + const overridden: OcxProviderConfig = { + ...provider, + modelDefaultReasoningEfforts: { [model]: "max" }, + modelReasoningEffortMap: { [model]: { medium: "low" } }, + }; + if (enriched) enrichProviderFromRegistry("custom-beijing", overridden); + const explicit = routeModel({ port: 10100, providers: { "custom-beijing": overridden } } as OcxConfig, `custom-beijing/${model}`); + expect(explicit.provider.modelDefaultReasoningEfforts?.[model]).toBe("max"); + expect(explicit.provider.modelReasoningEffortMap?.[model]?.medium).toBe("low"); + + }); + } + } + test.each([ + { adapter: "openai-chat", baseUrl: "https://example.test/v1" }, + { adapter: "anthropic", baseUrl: "https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1" }, + { adapter: "anthropic", baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1" }, + { adapter: "openai-chat", baseUrl: "https://token-plan.maas.qwencloudapi.com/compatible-mode/v1" }, + { adapter: "openai-chat", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1" }, + ])("nonmatching custom destination keeps operator policy: $adapter $baseUrl", ({ adapter, baseUrl }) => { + const provider: OcxProviderConfig = { adapter, baseUrl, noStructuredOutputModels: ["operator-model"] }; + enrichProviderFromRegistry("custom", provider); + const resolved = routeModel({ port: 10100, providers: { custom: provider } } as OcxConfig, `custom/${model}`); + expect(resolved.provider.noStructuredOutputModels).toEqual(["operator-model"]); + expect(resolved.provider.baseUrl).toBe(baseUrl); + expect(resolved.provider.adapter).toBe(adapter); + }); +}); diff --git a/tests/providers/alibaba-token-plan-wire-defaults.test.ts b/tests/providers/alibaba-token-plan-wire-defaults.test.ts index 90a746937d..cc3be78844 100644 --- a/tests/providers/alibaba-token-plan-wire-defaults.test.ts +++ b/tests/providers/alibaba-token-plan-wire-defaults.test.ts @@ -78,7 +78,7 @@ describe("the Token Plan default is isolated to the registry provider", () => { const resolved = resolveWireProtocolOverride("alibaba-token-plan", "qwen3.8-flash", tokenPlanProvider(), "responses"); expect(resolved.adapter).toBe("openai-responses"); expect(resolved.apiKey).toBe("sk-test"); - expect(resolved.baseUrl).toBe("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"); + expect(resolved.baseUrl).toBe("https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1"); }); }); @@ -133,7 +133,7 @@ describe("the Token Plan wire default survives the handleResponses replay", () = test("qwen3.8-flash reaches the Responses upstream, never /chat/completions", async () => { const url = await drive("qwen3.8-flash", "responses"); - expect(url).toContain("token-plan.cn-beijing.maas.aliyuncs.com"); + expect(url).toContain("token-plan.maas.qianwenaiapi.com"); expect(url).toContain("/responses"); expect(url).not.toContain("chat/completions"); }); @@ -144,7 +144,7 @@ describe("the Token Plan wire default survives the handleResponses replay", () = // with the other two. test("qwen3.7-plus reaches the Responses upstream, never /chat/completions", async () => { const url = await drive("qwen3.7-plus", "responses"); - expect(url).toContain("token-plan.cn-beijing.maas.aliyuncs.com"); + expect(url).toContain("token-plan.maas.qianwenaiapi.com"); expect(url).toContain("/responses"); expect(url).not.toContain("chat/completions"); }); @@ -163,14 +163,14 @@ describe("the Token Plan wire default survives the handleResponses replay", () = test("glm-5.3 reaches the Responses upstream on a responses inbound", async () => { const url = await drive("glm-5.3", "responses"); - expect(url).toContain("token-plan.cn-beijing.maas.aliyuncs.com"); + expect(url).toContain("token-plan.maas.qianwenaiapi.com"); expect(url).toContain("/responses"); expect(url).not.toContain("chat/completions"); }); test("qwen3.8-flash keeps the chat upstream on a chat inbound replay", async () => { const url = await drive("qwen3.8-flash", "chat"); - expect(url).toContain("token-plan.cn-beijing.maas.aliyuncs.com"); + expect(url).toContain("token-plan.maas.qianwenaiapi.com"); expect(url).toContain("chat/completions"); }); diff --git a/tests/providers/model-rename-migration.test.ts b/tests/providers/model-rename-migration.test.ts index bf63bdd037..130a2db904 100644 --- a/tests/providers/model-rename-migration.test.ts +++ b/tests/providers/model-rename-migration.test.ts @@ -637,3 +637,23 @@ describe("registry-seeded metadata does not re-arm the rename (#5066)", () => { expect(changed).toBe(false); }); }); + + +test("Token Plan preview repair accepts canonical and legacy hosts without rewriting destinations", () => { + for (const [id, urls] of [ + ["alibaba-token-plan-intl", [INTL_BASE_URL, "https://token-plan.maas.qwencloudapi.com/compatible-mode/v1"]], + ["alibaba-token-plan", ["https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", + "https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1"]], + ] as const) { + for (const baseUrl of urls) { + const config = staleConfig(); + const row = config.providers["alibaba-token-plan-intl"]!; + config.providers = { [id]: { ...row, baseUrl } }; + const result = projectModelRenames(config, [{ ...RENAME, provider: id }]); + expect(result.changed).toBe(true); + expect(result.config.providers[id]?.models).toContain("qwen3.8-max"); + expect(result.config.providers[id]?.baseUrl).toBe(baseUrl); + expect(result.config).toBe(config); // The migration intentionally updates this config in place. + } + } +}); diff --git a/tests/providers/model-roster-seed-repair.test.ts b/tests/providers/model-roster-seed-repair.test.ts index 996e8ded9d..3dffe4e6d2 100644 --- a/tests/providers/model-roster-seed-repair.test.ts +++ b/tests/providers/model-roster-seed-repair.test.ts @@ -86,7 +86,7 @@ describe("stale model roster migration", () => { for (const entry of STALE_MODEL_ROSTERS) { const registry = PROVIDER_REGISTRY.find(row => row.id === entry.provider); expect(registry?.models).toEqual([...entry.to]); - expect(entry.to.length).toBeGreaterThan(entry.from.length); + expect(entry.to.some(id => !entry.from.includes(id))).toBe(true); } }); }); @@ -105,3 +105,68 @@ describe("MiniMax preview catalog retention", () => { } }); }); + +describe.each([ + { provider: "alibaba-token-plan", before: [ + "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash", + "deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3", +] }, + { provider: "alibaba-token-plan-intl", before: [ + "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash", + "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v3.2", + "kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", + "glm-5.2", "glm-5.3", "glm-5.1", "glm-5", + "MiniMax-M2.5", +] }, +])("Token Plan saved roster refresh: $provider", ({ provider, before }) => { + const entry = STALE_MODEL_ROSTERS.find(row => row.provider === provider)!; + const makeConfig = (models: string[]) => ({ providers: { [entry.provider]: { + adapter: "openai-chat", baseUrl: "https://example.com/v1", models, + defaultModel: "qwen3.8-max", liveModels: false, + modelContextWindows: { "deepseek-v4.1-flash-1017": 123 }, + modelDefaultReasoningEfforts: { "deepseek-v4.1-flash-1017": "low" }, + } } } as unknown as OcxConfig); + test("refreshes only the exact previous seed through startup and is idempotent", () => { + const config = makeConfig([...before]); + const first = projectStartupConfigRepairs(config); + expect(first.changed).toBe(true); + const row = first.config.providers![entry.provider]!; + expect(row.models).toEqual([...entry.to]); + expect(row.models).toContain("deepseek-v4.1-flash-1017"); + expect(row.modelContextWindows!["deepseek-v4.1-flash-1017"]).toBe(123); + expect(row.modelDefaultReasoningEfforts!["deepseek-v4.1-flash-1017"]).toBe("low"); + expect(row.defaultModel).toBe("qwen3.8-max"); + expect(row.liveModels).toBe(false); + expect(row.baseUrl).toBe("https://example.com/v1"); + expect(projectStartupConfigRepairs(structuredClone(first.config)).changed).toBe(false); + }); + test("preserves trimmed, extended, reordered, duplicated and empty operator lists", () => { + for (const models of [before.slice(1), [...before, "custom-model"], [...before].reverse(), [...before, before[0]!], []]) { + const config = makeConfig(models); + const original = structuredClone(config); + expect(projectStaleModelRosters(config).changed).toBe(false); + expect(config).toEqual(original); + } + }); + test("fills existing metadata containers but never creates absent containers", () => { + const config = makeConfig([...before]); + const row = config.providers![entry.provider]!; + row.modelContextWindows = {}; + row.modelDefaultReasoningEfforts = {}; + expect(projectStaleModelRosters(config).changed).toBe(true); + expect(row.modelContextWindows["deepseek-v4.1-flash-1017"]).toBe(1_000_000); + expect(row.modelDefaultReasoningEfforts["deepseek-v4.1-flash-1017"]).toBe("high"); + const absent = makeConfig([...before]); + delete absent.providers![entry.provider]!.modelContextWindows; + delete absent.providers![entry.provider]!.modelDefaultReasoningEfforts; + projectStaleModelRosters(absent); + expect(absent.providers![entry.provider]!.modelContextWindows).toBeUndefined(); + expect(absent.providers![entry.provider]!.modelDefaultReasoningEfforts).toBeUndefined(); + }); + test("preserves a custom adapter", () => { + const config = makeConfig([...before]); + config.providers![entry.provider]!.adapter = "anthropic"; + expect(projectStaleModelRosters(config).changed).toBe(false); + expect(config.providers![entry.provider]!.models).toEqual(before); + }); +}); diff --git a/tests/providers/provider-registry-parity.test.ts b/tests/providers/provider-registry-parity.test.ts index ada4dac958..be9e01eca8 100644 --- a/tests/providers/provider-registry-parity.test.ts +++ b/tests/providers/provider-registry-parity.test.ts @@ -535,12 +535,12 @@ describe("provider registry parity", () => { expect(KEY_LOGIN_PROVIDERS["alibaba-token-plan"]).toMatchObject({ label: "Alibaba Token Plan (Beijing)", adapter: "openai-chat", - baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", + baseUrl: "https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1", defaultModel: "qwen3.8-max", liveModels: false, models: [ "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash", - "deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3", + "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v4.1-flash-1017", "glm-5.2", "glm-5.3", ], modelInputModalities: { "qwen3.8-max": ["text", "image"], @@ -560,8 +560,8 @@ describe("provider registry parity", () => { "deepseek-v4-pro": 393_216, }, noVisionModels: expect.arrayContaining(["qwen3.7-max", "deepseek-v4-pro", "glm-5.2"]), - // Beijing is the Personal Edition roster: the Team-only 0813 snapshot and the - // still-phantom glm-5.3-flash must stay out of this preset's models list. + // Beijing retains Personal curation; 0813 is now listed on Personal too. + // The still-phantom glm-5.3-flash stays out of this preset. // glm-5.3 itself joined the plan on 260917 and is Personal-entitled (probed 260918). preserveReasoningContentModels: expect.arrayContaining(["qwen3.8-max", "qwen3.7-max", "qwen3.7-plus"]), @@ -1822,7 +1822,7 @@ describe("free-provider directory isolation", () => { { provider: "opencode-go", model: "deepseek-v4.1-flash", flash: true }, { provider: "opencode-go", model: "deepseek-v4-flash", flash: true }, { provider: "volcengine-coding-plan", model: "deepseek-v4-flash", flash: true }, - { provider: "alibaba-token-plan-intl", model: "deepseek-v4-flash", flash: true }, + { provider: "alibaba-token-plan-intl", model: "deepseek-v4.1-flash-1017", flash: true }, ]; for (const { provider, model, flash } of cases) { diff --git a/tests/providers/provider-static-model-discovery.test.ts b/tests/providers/provider-static-model-discovery.test.ts index 88006aaf3e..5ae61e8f55 100644 --- a/tests/providers/provider-static-model-discovery.test.ts +++ b/tests/providers/provider-static-model-discovery.test.ts @@ -147,3 +147,31 @@ describe("static provider model discovery policy", () => { }))).toThrow("only supports the canonical Xiaomi MiMo Free endpoint"); }); }); + + +test("Token Plan curation stays static while exact-ID metadata enriches saved discovery", () => { + for (const id of ["alibaba-token-plan", "alibaba-token-plan-intl"]) { + const entry = getProviderRegistryEntry(id)!; + expect(providerConfigSeed(entry).liveModels).toBeFalse(); + expect(entry.models).toContain("deepseek-v4.1-flash-1017"); + expect(entry.models).toContain("deepseek-v4-pro-0813"); + for (const retired of ["deepseek-v3.2", "deepseek-v4-flash"]) { + expect(entry.models).not.toContain(retired); + expect(entry.modelContextWindows?.[retired]).toBeUndefined(); + expect(entry.modelMaxOutputTokens?.[retired]).toBeUndefined(); + expect(entry.modelInputModalities?.[retired]).toBeUndefined(); + expect(entry.preserveReasoningContentModels).not.toContain(retired); + } + const saved = provider({ ...providerConfigSeed(entry), liveModels: true, + models: ["deepseek-v4.1-flash-1017", "custom-selector"], + selectedModels: ["custom-selector"], defaultModel: "custom-selector", + }); + enrichProviderFromRegistry(id, saved); + expect(saved.liveModels).toBeTrue(); + expect(saved.models).toEqual(["deepseek-v4.1-flash-1017", "custom-selector"]); + expect(saved.selectedModels).toEqual(["custom-selector"]); + expect(saved.defaultModel).toBe("custom-selector"); + expect(saved.modelContextWindows?.["deepseek-v4.1-flash-1017"]).toBe(1_000_000); + expect(saved.modelDefaultReasoningEfforts?.["deepseek-v4.1-flash-1017"]).toBe("high"); + } +}); diff --git a/tests/routing/router-discarded-baseurl-warning.test.ts b/tests/routing/router-discarded-baseurl-warning.test.ts index d1413c4aa0..df03ea208a 100644 --- a/tests/routing/router-discarded-baseurl-warning.test.ts +++ b/tests/routing/router-discarded-baseurl-warning.test.ts @@ -150,7 +150,7 @@ test("stays silent for a non-string baseUrl (control: unchanged pre-existing beh * the international Team Edition is a separate provider that honors its configured endpoint. * Locking it here means a change to either registry entry has to face this test. */ -const ALIBABA_BEIJING_BASE_URL = "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"; +const ALIBABA_BEIJING_BASE_URL = "https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1"; const ALIBABA_INTL_BASE_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"; test("alibaba-token-plan is pinned to Beijing and warns about a saved international URL", () => {