diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index e3c43331042..6ebe96d371b 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 79b628aa787..519ad12aae5 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,27 @@ 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`. Both presets keep the hosts their official quick starts +document: Beijing `https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1` and +International `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1`. +The Qwen Cloud preset uses the separate Token Plan host that Qwen Cloud documents above. +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 6e066784772..1c85022ea97 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 ce40892e76e..a413e237217 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 6d3a7955427..30962e743bc 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 317a6b6b50d..388be9afe17 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 ef88d8c1837..2f12506a3cc 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 9be3a8c2aaa..dea1e1d782b 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/base-url-choices.ts b/src/providers/base-url-choices.ts index 43a533059ca..14535df961a 100644 --- a/src/providers/base-url-choices.ts +++ b/src/providers/base-url-choices.ts @@ -1,9 +1,11 @@ /** * Qwen Cloud OpenAI-compatible endpoint presets. * Token plan is the GUI default; pay-as-you-go and custom are selectable. + * Token plan host per https://docs.qwencloud.com/token-plan/personal/token-plan-personal-quickstart + * (checked 2026-10-23); a saved older host stays usable as a custom URL. */ 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,10 +25,11 @@ 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. - * 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. + * Separate regional account and billing destination; both regions offer both tiers. + * Alibaba Cloud's own quick start still documents this host (checked 2026-10-23): + * https://www.alibabacloud.com/help/en/model-studio/token-plan-personal-quick-start + * Qwen Cloud documents a different Token Plan host, so the two constants are + * independent and must not be assumed equal. */ export const ALIBABA_INTL_TOKEN_PLAN_BASE_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"; diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 2aa73259bc2..561b6964b41 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, @@ -474,13 +475,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/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index cb365fcd4ef..348d7f847b5 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -837,6 +837,8 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ { id: "alibaba-token-plan", label: "Alibaba Token Plan (Beijing)", + // Official Personal quick start (2026-10-23) still documents this host: + // https://help.aliyun.com/en/model-studio/token-plan-personal-quick-start baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", adapter: "openai-chat", authKind: "key", @@ -887,19 +889,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, @@ -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 3f07c4463c4..8bf4cf6f12e 100644 --- a/src/providers/registry/model-ids.ts +++ b/src/providers/registry/model-ids.ts @@ -115,6 +115,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 836f8751ab6..674b37fe607 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 e41af4602b0..538a7577a81 100644 --- a/src/providers/registry/types.ts +++ b/src/providers/registry/types.ts @@ -342,6 +342,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 e2cf956e9d4..f18edc1e7c3 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 df6f20cd0cc..7189623ad86 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 bdb6a07cbde..271db0b27c5 100644 --- a/src/providers/stale-model-roster-migration.ts +++ b/src/providers/stale-model-roster-migration.ts @@ -15,8 +15,12 @@ * all-or-nothing, so creating one here would hide the rest of the registry seed. * Same shape and restraint as `stale-context-window-migration`. */ -import { PROVIDER_REGISTRY } from "./registry"; +import { ALIBABA_INTL_TOKEN_PLAN_BASE_URL } from "./base-url-choices"; +import { PROVIDER_REGISTRY, normalizedProviderEndpoint } 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, @@ -34,6 +38,12 @@ export interface StaleModelRoster { /** Per-model seed values for the added ids, written only into containers that exist. */ contextWindows?: Readonly>; defaultReasoningEfforts?: Readonly>; + /** + * For a provider whose saved URL may point elsewhere (pay-as-you-go or Custom), the + * destinations whose roster `to` describes. A row on any other URL keeps its list; a row + * with no saved URL uses the registry default and is refreshed. + */ + destinationBaseUrls?: readonly string[]; } export interface StaleModelRosterProjection { @@ -42,17 +52,44 @@ 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 }, + // International honors pay-as-you-go and Custom URLs, which may still serve the retired ids. + // Beijing needs no guard: the router pins it to the registry Token Plan host. + { + provider: "alibaba-token-plan-intl", from: ALIBABA_INTL_TOKEN_PLAN_MODELS_BEFORE_1017, to: ALIBABA_INTL_TOKEN_PLAN_MODELS, + destinationBaseUrls: [ALIBABA_INTL_TOKEN_PLAN_BASE_URL], + }, + ].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) @@ -65,6 +102,13 @@ function registrySeeds(provider: string, adapter: unknown, roster: readonly stri return entry !== undefined && entry.adapter === adapter && sameList(entry.models, roster); } +function servesRoster(baseUrl: unknown, destinations: readonly string[] | undefined): boolean { + if (!destinations || baseUrl === undefined || baseUrl === "") return true; + if (typeof baseUrl !== "string") return false; + const saved = normalizedProviderEndpoint(baseUrl); + return destinations.some(url => normalizedProviderEndpoint(url) === saved); +} + function fillExisting(container: Record | undefined, values: Readonly> | undefined, ids: readonly string[]): void { if (!container || !values) return; for (const id of ids) { @@ -85,6 +129,7 @@ export function projectStaleModelRosters( const prov = config.providers?.[entry.provider]; if (!prov) continue; if (!registrySeeds(entry.provider, prov.adapter, entry.to)) continue; + if (!servesRoster(prov.baseUrl, entry.destinationBaseUrls)) continue; if (!sameList(prov.models, entry.from)) continue; const added = entry.to.filter(id => !entry.from.includes(id)); prov.models = [...entry.to]; diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index 1e2005d8fef..91b5c161e90 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -589,3 +589,11 @@ 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. International rows refresh only on the Token Plan host or the registry default; pay-as-you-go and Custom destinations keep their lists. Edited or reordered lists, saved model defaults and existing per-model window and effort overrides remain untouched. Destination-based enrichment of custom-named rows fills per-model effort defaults, effort maps and structured-output restrictions from the matching registry destination or alias, and operator values win. Both Token Plan presets keep the hosts their official quick starts document; 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 f645235804b..3f432afc509 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-token-plan-responses-optin.test.ts b/tests/providers/alibaba-token-plan-responses-optin.test.ts index 4c7c936f59c..17d240189f4 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"; @@ -153,3 +156,129 @@ 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.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.cn-beijing.maas.aliyuncs.com/compatible-mode/v1" }, + // Undocumented for Token Plan; must not be treated as the Beijing destination. + { adapter: "openai-chat", baseUrl: "https://token-plan.maas.qianwenaiapi.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/model-roster-seed-repair.test.ts b/tests/providers/model-roster-seed-repair.test.ts index 996e8ded9d6..6af9206d064 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,95 @@ describe("MiniMax preview catalog retention", () => { } }); }); + +describe.each([ + // Beijing is pinned to its registry host by the router, so any saved URL is refreshed. + { provider: "alibaba-token-plan", baseUrl: "https://example.com/v1", 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", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", 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, baseUrl, before }) => { + const entry = STALE_MODEL_ROSTERS.find(row => row.provider === provider)!; + const makeConfig = (models: string[]) => ({ providers: { [entry.provider]: { + adapter: "openai-chat", baseUrl, 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(baseUrl); + 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); + }); +}); + +describe("International roster refresh follows the saved destination", () => { + const entry = STALE_MODEL_ROSTERS.find(row => row.provider === "alibaba-token-plan-intl")!; + const row = (baseUrl: string | undefined, selectedModels?: string[]) => ({ providers: { [entry.provider]: { + adapter: "openai-chat", ...(baseUrl === undefined ? {} : { baseUrl }), models: [...entry.from], + ...(selectedModels ? { selectedModels } : {}), + } } } as unknown as OcxConfig); + + test("leaves pay-as-you-go and Custom destinations, and their allowlists, untouched", () => { + for (const baseUrl of ["https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "https://gateway.example.test/v1"]) { + const config = row(baseUrl, ["deepseek-v3.2", "deepseek-v4-flash"]); + const original = structuredClone(config); + expect(projectStartupConfigRepairs(config).changed).toBe(false); + expect(config).toEqual(original); + } + }); + + test("refreshes the Token Plan host in any spelling and a row using the registry default", () => { + for (const baseUrl of [undefined, "HTTPS://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/"]) { + const config = row(baseUrl); + expect(projectStaleModelRosters(config).changed).toBe(true); + expect(config.providers![entry.provider]!.models).toEqual([...entry.to]); + expect(config.providers![entry.provider]!.baseUrl).toBe(baseUrl); + } + }); +}); diff --git a/tests/providers/provider-registry-parity.test.ts b/tests/providers/provider-registry-parity.test.ts index aea29c79d74..11e6e2747c2 100644 --- a/tests/providers/provider-registry-parity.test.ts +++ b/tests/providers/provider-registry-parity.test.ts @@ -556,7 +556,7 @@ describe("provider registry parity", () => { 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"], @@ -576,8 +576,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"]), @@ -1837,7 +1837,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 88006aaf3ea..5ae61e8f550 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/providers/registry-transport-alias.test.ts b/tests/providers/registry-transport-alias.test.ts index 912206ccc4f..2c5a3d5e795 100644 --- a/tests/providers/registry-transport-alias.test.ts +++ b/tests/providers/registry-transport-alias.test.ts @@ -1,5 +1,7 @@ import { expect, test } from "bun:test"; +import { enrichProviderFromRegistry } from "../../src/providers/derive"; import { providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../src/providers/registry"; +import { deepseekReasoningMapFor } from "../../src/providers/registry/model-seeds"; import { captureRouteStaticPolicy, routedProviderConfig } from "../../src/router"; import type { OcxProviderConfig } from "../../src/types"; @@ -25,3 +27,38 @@ test("the Go inference alias identifies renamed destinations while the preset re expect(routedProviderConfig("opencode-go", provider).baseUrl).toBe("https://opencode.ai/zen/go/v1"); expect(routedProviderConfig("renamed-go", provider).baseUrl).toBe(provider.baseUrl); }); + +test("non-Alibaba destination aliases fill effort defaults and maps while operator values win", () => { + // Z.AI's former Chat address carries the GLM-5.3 default; Ark's Chat alias carries a map. + const zaiUrl = "https://api.z.ai/api/coding/paas/v4"; + const arkUrl = "https://ark.cn-beijing.volces.com/api/coding/v3"; + const registryMap = deepseekReasoningMapFor("deepseek-v4-flash"); + const zai: OcxProviderConfig = { adapter: "openai-chat", baseUrl: zaiUrl }; + const ark: OcxProviderConfig = { adapter: "openai-chat", baseUrl: arkUrl }; + enrichProviderFromRegistry("GLM", zai); + enrichProviderFromRegistry("ark-chat", ark); + expect(zai.modelDefaultReasoningEfforts?.["glm-5.3"]).toBe("max"); + expect(ark.modelReasoningEffortMap?.["deepseek-v4-flash"]).toEqual(registryMap); + expect(routedProviderConfig("GLM", { adapter: "openai-chat", baseUrl: zaiUrl }) + .modelDefaultReasoningEfforts?.["glm-5.3"]).toBe("max"); + expect(routedProviderConfig("ark-chat", { adapter: "openai-chat", baseUrl: arkUrl }) + .modelReasoningEffortMap?.["deepseek-v4-flash"]).toEqual(registryMap); + + const zaiOperator: OcxProviderConfig = { + adapter: "openai-chat", baseUrl: zaiUrl, modelDefaultReasoningEfforts: { "glm-5.3": "low" }, + }; + const arkOperator: OcxProviderConfig = { + adapter: "openai-chat", baseUrl: arkUrl, modelReasoningEffortMap: { "deepseek-v4-flash": { medium: "low" } }, + }; + const zaiRouted = routedProviderConfig("GLM", zaiOperator); + const arkRouted = routedProviderConfig("ark-chat", arkOperator); + enrichProviderFromRegistry("GLM", zaiOperator); + enrichProviderFromRegistry("ark-chat", arkOperator); + for (const row of [zaiOperator, zaiRouted]) { + expect(row.modelDefaultReasoningEfforts?.["glm-5.3"]).toBe("low"); + expect(row.modelDefaultReasoningEfforts?.["glm-5.3-flash"]).toBe("max"); + } + for (const row of [arkOperator, arkRouted]) { + expect(row.modelReasoningEffortMap?.["deepseek-v4-flash"]).toEqual({ ...registryMap, medium: "low" }); + } +});