From f176dff6749972c37b66635277168a4316262a02 Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:45:31 +0800 Subject: [PATCH 1/9] fix(providers): refresh dated Token Plan catalogue and metadata --- .../src/content/docs/fr/guides/providers.md | 2 +- .../src/content/docs/guides/providers.md | 31 ++++++++- .../src/content/docs/ja/guides/providers.md | 2 +- .../src/content/docs/ko/guides/providers.md | 2 +- .../src/content/docs/ru/guides/providers.md | 2 +- .../src/content/docs/tr/guides/providers.md | 2 +- .../content/docs/zh-cn/guides/providers.md | 2 +- .../content/docs/zh-tw/guides/providers.md | 2 +- src/providers/alibaba-region-migration.ts | 3 +- src/providers/base-url-choices.ts | 8 +-- src/providers/registry/entries-extended.ts | 22 +++--- src/providers/registry/model-ids.ts | 1 + src/providers/registry/model-seeds.ts | 55 ++++++++------- src/providers/registry/types.ts | 2 + src/providers/resolved-model-policy.ts | 4 +- src/providers/routed-config.ts | 2 + structure/providers-and-adapters.md | 9 +++ tests/gui/alibaba-intl-token-plan.test.ts | 11 ++- .../alibaba-region-migration.test.ts | 13 +++- ...alibaba-token-plan-responses-optin.test.ts | 69 +++++++++++++++++-- .../alibaba-token-plan-wire-defaults.test.ts | 10 +-- .../provider-registry-parity.test.ts | 10 +-- .../provider-static-model-discovery.test.ts | 28 ++++++++ 23 files changed, 220 insertions(+), 72 deletions(-) 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..77908c7261 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` | @@ -1678,3 +1678,32 @@ An access-denied result does not by itself prove an expired login or an ineligib ## Large inline images on Chat providers Translated OpenAI-compatible Chat requests shrink inline images when their combined base64 data exceeds 3.5 MiB. Older images lose detail first. This is a best-effort image budget, so large text, schemas, or images that cannot be processed may still exceed an upstream request limit. Remote image URLs are not downloaded, and images that cannot be shrunk remain attached. Native Chat passthrough keeps its original image bytes. + + +### Alibaba Token Plan dated catalogue (2026-10-20) + +The Beijing preset curates Personal chat models; the International preset curates Team +chat models. Both regions offer both subscription tiers. Discovery remains disabled by +default because the upstream list also includes non-chat models. Saved model selections +and explicit International endpoint overrides are not rewritten by this catalogue refresh. + +Both presets include `deepseek-v4.1-flash-1017`: text and image input, text output, +1,000,000 context, documented `low`/`high`/`max` efforts (default `high`). The 393,216 +output budget follows the existing plan normalization of the vendor's 393K/384K display; +it is not a measured exact boundary and differs from the lab's 384,000 value. +The existing structured-output opt-out omits `response_format` for this exact ID: +strict schemas were rejected, and `json_object` is unverified. This is best-effort text, +not guaranteed JSON. Validate any structured response in the client. The bare model's +verified JSON downgrade is unchanged. A direct Responses success does not add a new +Responses default pin or prove reasoning replay. + +`deepseek-v4-pro-0813` now joins the existing Personal curation based on current listing +and documented entitlement. The International active catalogue removes retired +`deepseek-v3.2` and bare `deepseek-v4-flash`; `deepseek-v4-flash-0731` remains available. +The canonical Beijing endpoint is +`https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1`; the International endpoint is +`https://token-plan.maas.qwencloudapi.com/compatible-mode/v1`. + +Sources checked 2026-10-20: [model card](https://www.qianwenai.com/models/deepseek-v4.1-flash-1017), +[Chat API reference](https://docs.qwencloud.com/api-reference/chat/openai-chat), and +[Personal overview](https://help.aliyun.com/en/model-studio/token-plan-personal-overview). 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..107be75062 100644 --- a/src/providers/alibaba-region-migration.ts +++ b/src/providers/alibaba-region-migration.ts @@ -36,7 +36,8 @@ 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( + return normalize(baseUrl) === "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1" + || ALIBABA_INTL_BASE_URL_CHOICES.some( choice => choice.baseUrl && normalize(choice.baseUrl) === 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/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index 1fd4039425..ebe726b614 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -837,7 +837,7 @@ 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", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://bailian.console.aliyun.com/cn-beijing?tab=plan", @@ -874,7 +874,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ "qwen3.7-plus": { wire: "openai-responses", inbound: ["responses"] }, "glm-5.3": { wire: "openai-responses", inbound: ["responses"] }, }, - note: "Token Plan Personal Edition · China (Beijing)", + note: "Personal-curated Token Plan preset · China (Beijing); both tiers supported", modelInputModalities: ALIBABA_TOKEN_PLAN_INPUT_MODALITIES, modelContextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS, modelMaxOutputTokens: ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS, @@ -887,19 +887,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, @@ -923,7 +927,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ defaultModel: "qwen3.7-max", models: ALIBABA_INTL_TOKEN_PLAN_MODELS, liveModels: false, - note: "Token Plan Team Edition · Singapore (ap-southeast-1)", + note: "Team-curated Token Plan preset · International; both tiers supported", metadataModelIdNormalize: "case-insensitive", modelInputModalities: ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES, modelContextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS, @@ -935,25 +939,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..9434d07f4e 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. These presets deliberately curate +// a Personal subset for Beijing and a Team subset for international accounts; geography +// does not determine tier. Keep liveModels false: discovery 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. +// Team-curated multi-vendor lineup, not a region-specific entitlement difference. // 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..9ad0b1a4b4 100644 --- a/src/providers/routed-config.ts +++ b/src/providers/routed-config.ts @@ -172,6 +172,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 +326,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/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index 7eb489e8e4..52a0e853d5 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. 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..0050028ee3 100644 --- a/tests/providers/alibaba-region-migration.test.ts +++ b/tests/providers/alibaba-region-migration.test.ts @@ -47,7 +47,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 +177,14 @@ 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); +}); diff --git a/tests/providers/alibaba-token-plan-responses-optin.test.ts b/tests/providers/alibaba-token-plan-responses-optin.test.ts index 4c7c936f59..fa8987beff 100644 --- a/tests/providers/alibaba-token-plan-responses-optin.test.ts +++ b/tests/providers/alibaba-token-plan-responses-optin.test.ts @@ -17,6 +17,9 @@ * a Responses wire default must carry the Responses-side preservation beside it. */ import { afterEach, describe, expect, test } from "bun:test"; +import { routeModel } from "../../src/router"; +import { createOpenAIChatAdapter } from "../../src/adapters/openai-chat"; +import { buildOpenAIChatPassthroughRequest } from "../../src/adapters/openai-chat/passthrough"; import { providerConfigSeed } from "../../src/providers/derive"; import { getProviderRegistryEntry } from "../../src/providers/registry"; import { resolveWireProtocolOverride } from "../../src/server/adapter-resolve"; @@ -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,61 @@ 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(); + expect(resolved.provider.noStructuredOutputModels).toContain(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" }); + }); + } +}); 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/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"); + } +}); From 38d6116863b5b5c768103e720efcffad595ad40d Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:44:51 +0800 Subject: [PATCH 2/9] fix(providers): preserve legacy Token Plan rename repair --- src/providers/model-rename-migration.ts | 9 ++++++++- .../providers/model-rename-migration.test.ts | 20 +++++++++++++++++++ .../router-discarded-baseurl-warning.test.ts | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/providers/model-rename-migration.ts b/src/providers/model-rename-migration.ts index 37a778766a..99d35f40d8 100644 --- a/src/providers/model-rename-migration.ts +++ b/src/providers/model-rename-migration.ts @@ -256,7 +256,14 @@ 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] + // Retain the historical Token Plan destinations for configs saved before the + // canonical-host update. Matching them must not rewrite the operator's baseUrl. + const legacy = name === "alibaba-token-plan-intl" + ? ["https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"] + : name === "alibaba-token-plan" + ? ["https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"] + : []; + const known = [entry.baseUrl, ...choices, ...legacy] .filter((url): url is string => typeof url === "string") .map(normalizedProviderEndpoint); return known.includes(normalizedProviderEndpoint(prov.baseUrl)); diff --git a/tests/providers/model-rename-migration.test.ts b/tests/providers/model-rename-migration.test.ts index bf63bdd037..8590b5c500 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(config.providers[id]?.models).toContain("qwen3.8-max-preview"); + } + } +}); 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", () => { From c90ae398781c66ee4af25988651028145a9ee04f Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:58:49 +0800 Subject: [PATCH 3/9] test: match model rename in-place projection contract --- tests/providers/model-rename-migration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/providers/model-rename-migration.test.ts b/tests/providers/model-rename-migration.test.ts index 8590b5c500..130a2db904 100644 --- a/tests/providers/model-rename-migration.test.ts +++ b/tests/providers/model-rename-migration.test.ts @@ -653,7 +653,7 @@ test("Token Plan preview repair accepts canonical and legacy hosts without rewri expect(result.changed).toBe(true); expect(result.config.providers[id]?.models).toContain("qwen3.8-max"); expect(result.config.providers[id]?.baseUrl).toBe(baseUrl); - expect(config.providers[id]?.models).toContain("qwen3.8-max-preview"); + expect(result.config).toBe(config); // The migration intentionally updates this config in place. } } }); From b4169fe03c4517683c47bd3baf39e9e3e30263da Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:10:12 +0800 Subject: [PATCH 4/9] fix(providers): migrate untouched Token Plan model rosters --- src/providers/stale-model-roster-migration.ts | 39 ++++-- structure/providers-and-adapters.md | 2 +- .../model-roster-seed-repair.test.ts | 118 +++++++++++++++++- 3 files changed, 150 insertions(+), 9 deletions(-) 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 52a0e853d5..b577eaa4d0 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -597,4 +597,4 @@ The registry may seed `noStructuredOutputModels` at route resolution, unioned wi 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. +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. diff --git a/tests/providers/model-roster-seed-repair.test.ts b/tests/providers/model-roster-seed-repair.test.ts index 996e8ded9d..f2f85c7b19 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,119 @@ describe("MiniMax preview catalog retention", () => { } }); }); + +describe("Token Plan saved roster refresh", () => { + describe("alibaba-token-plan", () => { + const 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", +]; + const entry = STALE_MODEL_ROSTERS.find(row => row.provider === "alibaba-token-plan")!; + 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(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("alibaba-token-plan-intl", () => { + const 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", +]; + const entry = STALE_MODEL_ROSTERS.find(row => row.provider === "alibaba-token-plan-intl")!; + 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(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); + }); + }); +}); From b85b6fd2ce6e3b9415815d1efcd19d2e995cc694 Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:42:56 +0800 Subject: [PATCH 5/9] fix(providers): preserve historical Token Plan destination metadata --- src/providers/model-rename-migration.ts | 12 ++++-------- src/providers/registry/entries-extended.ts | 2 ++ structure/providers-and-adapters.md | 2 +- .../providers/alibaba-region-migration.test.ts | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/providers/model-rename-migration.ts b/src/providers/model-rename-migration.ts index 99d35f40d8..9ad05d3a72 100644 --- a/src/providers/model-rename-migration.ts +++ b/src/providers/model-rename-migration.ts @@ -256,14 +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) ?? []; - // Retain the historical Token Plan destinations for configs saved before the - // canonical-host update. Matching them must not rewrite the operator's baseUrl. - const legacy = name === "alibaba-token-plan-intl" - ? ["https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"] - : name === "alibaba-token-plan" - ? ["https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"] - : []; - const known = [entry.baseUrl, ...choices, ...legacy] + // 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 ebe726b614..307526cdd0 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -838,6 +838,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ id: "alibaba-token-plan", label: "Alibaba Token Plan (Beijing)", 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", @@ -919,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, diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index b577eaa4d0..c316d00cb1 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -597,4 +597,4 @@ The registry may seed `noStructuredOutputModels` at route resolution, unioned wi 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. +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 enrichment and saved-host model renames share that metadata without rewriting saved URLs. diff --git a/tests/providers/alibaba-region-migration.test.ts b/tests/providers/alibaba-region-migration.test.ts index 0050028ee3..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"; @@ -188,3 +191,18 @@ test("canonical international host is recognized while preserving the saved dest 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(); + } +}); From f09b117a563f952a53fa1600f0409803e0eb382f Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:59:22 +0800 Subject: [PATCH 6/9] fix(providers): retain destination structured-output restrictions --- src/providers/alibaba-region-migration.ts | 12 +++--- src/providers/derive.ts | 5 ++- src/providers/routed-config.ts | 3 ++ structure/providers-and-adapters.md | 2 +- ...alibaba-token-plan-responses-optin.test.ts | 40 ++++++++++++++++++- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/providers/alibaba-region-migration.ts b/src/providers/alibaba-region-migration.ts index 107be75062..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,10 +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 normalize(baseUrl) === "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1" - || 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/derive.ts b/src/providers/derive.ts index 5e01ebb876..b84cce4450 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -470,13 +470,16 @@ 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?.noStructuredOutputModels?.length) { + prov.noStructuredOutputModels = [...new Set([...(prov.noStructuredOutputModels ?? []), ...destination.noStructuredOutputModels])]; + } if (prov.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined) { prov.dropResponsesReasoningItems = destination.dropResponsesReasoningItems; } diff --git a/src/providers/routed-config.ts b/src/providers/routed-config.ts index 9ad0b1a4b4..b4077f059e 100644 --- a/src/providers/routed-config.ts +++ b/src/providers/routed-config.ts @@ -124,6 +124,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey), + ...(destination?.noStructuredOutputModels?.length ? { + noStructuredOutputModels: [...new Set([...(provider.noStructuredOutputModels ?? []), ...destination.noStructuredOutputModels])], + } : {}), ...(provider.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined ? { dropResponsesReasoningItems: destination.dropResponsesReasoningItems } : {}), diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index c316d00cb1..1179d7fe41 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -597,4 +597,4 @@ The registry may seed `noStructuredOutputModels` at route resolution, unioned wi 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 enrichment and saved-host model renames share that metadata without rewriting saved URLs. +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/providers/alibaba-token-plan-responses-optin.test.ts b/tests/providers/alibaba-token-plan-responses-optin.test.ts index fa8987beff..ca23786e8c 100644 --- a/tests/providers/alibaba-token-plan-responses-optin.test.ts +++ b/tests/providers/alibaba-token-plan-responses-optin.test.ts @@ -20,7 +20,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { routeModel } from "../../src/router"; import { createOpenAIChatAdapter } from "../../src/adapters/openai-chat"; import { buildOpenAIChatPassthroughRequest } from "../../src/adapters/openai-chat/passthrough"; -import { providerConfigSeed } from "../../src/providers/derive"; +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"; @@ -214,3 +214,41 @@ describe("1017 exact-ID Token Plan adapter policy", () => { }); } }); + + +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.noStructuredOutputModels).toContain("operator-model"); + expect(resolved.provider.noStructuredOutputModels).toContain(model); + const native = buildOpenAIChatPassthroughRequest(resolved.provider, { + messages: [{ role: "user", content: "hi" }], response_format: { type: "json_object" }, + }, 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: { type: "json_object" } }, stream: false, + }); + expect(JSON.parse(translated.body as string).response_format).toBeUndefined(); + }); + } + } + test("unrelated custom destinations do not inherit the restriction", () => { + const provider: OcxProviderConfig = { adapter: "openai-chat", baseUrl: "https://example.test/v1" }; + enrichProviderFromRegistry("custom-beijing", provider); + const resolved = routeModel({ port: 10100, providers: { custom: provider } } as OcxConfig, `custom/${model}`); + expect(resolved.provider.noStructuredOutputModels).toBeUndefined(); + }); +}); From a0305059feaeb887c17cb6a01f5dcf806498acb4 Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:05:18 +0800 Subject: [PATCH 7/9] test(providers): complete Token Plan policy boundary coverage --- .../src/content/docs/guides/providers.md | 4 +- src/providers/derive.ts | 3 +- src/providers/routed-config.ts | 3 +- ...alibaba-token-plan-responses-optin.test.ts | 41 +++-- .../model-roster-seed-repair.test.ts | 159 ++++++------------ 5 files changed, 87 insertions(+), 123 deletions(-) diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 77908c7261..23f346f718 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -1694,7 +1694,9 @@ it is not a measured exact boundary and differs from the lab's 384,000 value. The existing structured-output opt-out omits `response_format` for this exact ID: strict schemas were rejected, and `json_object` is unverified. This is best-effort text, not guaranteed JSON. Validate any structured response in the client. The bare model's -verified JSON downgrade is unchanged. A direct Responses success does not add a new +verified JSON downgrade is unchanged. Historical Beijing destinations retain this +restriction even when the provider is saved under a custom name; operator opt-outs +are combined with it, not replaced. A direct Responses success does not add a new Responses default pin or prove reasoning replay. `deepseek-v4-pro-0813` now joins the existing Personal curation based on current listing diff --git a/src/providers/derive.ts b/src/providers/derive.ts index b84cce4450..644633e543 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 { stableUnion } from "./resolved-model-policy-merge"; import { PROVIDER_REGISTRY, registryEntryForProviderDestination, @@ -478,7 +479,7 @@ function enrichReasoningMetadataByDestination(prov: OcxProviderConfig): void { const destination = registryEntryForProviderDestination(prov); applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries); if (destination?.noStructuredOutputModels?.length) { - prov.noStructuredOutputModels = [...new Set([...(prov.noStructuredOutputModels ?? []), ...destination.noStructuredOutputModels])]; + prov.noStructuredOutputModels = stableUnion(destination.noStructuredOutputModels, prov.noStructuredOutputModels)[0]; } if (prov.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined) { prov.dropResponsesReasoningItems = destination.dropResponsesReasoningItems; diff --git a/src/providers/routed-config.ts b/src/providers/routed-config.ts index b4077f059e..19011f15b4 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 { stableUnion } from "./resolved-model-policy-merge"; /** Same endpoint modulo surrounding space and trailing slashes — matches `matchBaseUrlChoice`. */ function isSameEndpoint(a: string, b: string): boolean { @@ -125,7 +126,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider ...provider, apiKey: usableResolvedApiKey(provider.apiKey), ...(destination?.noStructuredOutputModels?.length ? { - noStructuredOutputModels: [...new Set([...(provider.noStructuredOutputModels ?? []), ...destination.noStructuredOutputModels])], + noStructuredOutputModels: stableUnion(destination.noStructuredOutputModels, provider.noStructuredOutputModels)[0], } : {}), ...(provider.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined ? { dropResponsesReasoningItems: destination.dropResponsesReasoningItems } diff --git a/tests/providers/alibaba-token-plan-responses-optin.test.ts b/tests/providers/alibaba-token-plan-responses-optin.test.ts index ca23786e8c..71640920fb 100644 --- a/tests/providers/alibaba-token-plan-responses-optin.test.ts +++ b/tests/providers/alibaba-token-plan-responses-optin.test.ts @@ -190,8 +190,8 @@ describe("1017 exact-ID Token Plan adapter policy", () => { }); test(`${id} omits unknown structured-output formats on both Chat paths`, () => { - const resolved = route(); - expect(resolved.provider.noStructuredOutputModels).toContain(model); + 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({ @@ -233,22 +233,33 @@ describe("custom Beijing destination structured-output policy", () => { providers: { "custom-beijing": provider } } as OcxConfig, `custom-beijing/${model}`); expect(resolved.provider.noStructuredOutputModels).toContain("operator-model"); expect(resolved.provider.noStructuredOutputModels).toContain(model); - const native = buildOpenAIChatPassthroughRequest(resolved.provider, { - messages: [{ role: "user", content: "hi" }], response_format: { type: "json_object" }, - }, 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: { type: "json_object" } }, stream: false, - }); - expect(JSON.parse(translated.body as string).response_format).toBeUndefined(); + 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"]); }); } } - test("unrelated custom destinations do not inherit the restriction", () => { - const provider: OcxProviderConfig = { adapter: "openai-chat", baseUrl: "https://example.test/v1" }; - enrichProviderFromRegistry("custom-beijing", provider); + 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).toBeUndefined(); + 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 f2f85c7b19..3dffe4e6d2 100644 --- a/tests/providers/model-roster-seed-repair.test.ts +++ b/tests/providers/model-roster-seed-repair.test.ts @@ -106,118 +106,67 @@ describe("MiniMax preview catalog retention", () => { }); }); -describe("Token Plan saved roster refresh", () => { - describe("alibaba-token-plan", () => { - const before = [ +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", -]; - const entry = STALE_MODEL_ROSTERS.find(row => row.provider === "alibaba-token-plan")!; - 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(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("alibaba-token-plan-intl", () => { - const before = [ +] }, + { 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", -]; - const entry = STALE_MODEL_ROSTERS.find(row => row.provider === "alibaba-token-plan-intl")!; - 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(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"; +] }, +])("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.providers![entry.provider]!.models).toEqual(before); - }); + 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); }); }); From a61b59bf0b520850e8f2f7d9fd1134a7bc4d0afe Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:11:50 +0800 Subject: [PATCH 8/9] docs(providers): retain Token Plan product labels and focus guidance --- .../src/content/docs/guides/providers.md | 51 ++++++++----------- src/providers/registry/entries-extended.ts | 4 +- src/providers/registry/model-seeds.ts | 8 +-- 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 23f346f718..e0a7cbd473 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -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 @@ -1678,34 +1698,3 @@ An access-denied result does not by itself prove an expired login or an ineligib ## Large inline images on Chat providers Translated OpenAI-compatible Chat requests shrink inline images when their combined base64 data exceeds 3.5 MiB. Older images lose detail first. This is a best-effort image budget, so large text, schemas, or images that cannot be processed may still exceed an upstream request limit. Remote image URLs are not downloaded, and images that cannot be shrunk remain attached. Native Chat passthrough keeps its original image bytes. - - -### Alibaba Token Plan dated catalogue (2026-10-20) - -The Beijing preset curates Personal chat models; the International preset curates Team -chat models. Both regions offer both subscription tiers. Discovery remains disabled by -default because the upstream list also includes non-chat models. Saved model selections -and explicit International endpoint overrides are not rewritten by this catalogue refresh. - -Both presets include `deepseek-v4.1-flash-1017`: text and image input, text output, -1,000,000 context, documented `low`/`high`/`max` efforts (default `high`). The 393,216 -output budget follows the existing plan normalization of the vendor's 393K/384K display; -it is not a measured exact boundary and differs from the lab's 384,000 value. -The existing structured-output opt-out omits `response_format` for this exact ID: -strict schemas were rejected, and `json_object` is unverified. This is best-effort text, -not guaranteed JSON. Validate any structured response in the client. The bare model's -verified JSON downgrade is unchanged. Historical Beijing destinations retain this -restriction even when the provider is saved under a custom name; operator opt-outs -are combined with it, not replaced. A direct Responses success does not add a new -Responses default pin or prove reasoning replay. - -`deepseek-v4-pro-0813` now joins the existing Personal curation based on current listing -and documented entitlement. The International active catalogue removes retired -`deepseek-v3.2` and bare `deepseek-v4-flash`; `deepseek-v4-flash-0731` remains available. -The canonical Beijing endpoint is -`https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1`; the International endpoint is -`https://token-plan.maas.qwencloudapi.com/compatible-mode/v1`. - -Sources checked 2026-10-20: [model card](https://www.qianwenai.com/models/deepseek-v4.1-flash-1017), -[Chat API reference](https://docs.qwencloud.com/api-reference/chat/openai-chat), and -[Personal overview](https://help.aliyun.com/en/model-studio/token-plan-personal-overview). diff --git a/src/providers/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index 307526cdd0..96aeac789c 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -875,7 +875,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ "qwen3.7-plus": { wire: "openai-responses", inbound: ["responses"] }, "glm-5.3": { wire: "openai-responses", inbound: ["responses"] }, }, - note: "Personal-curated Token Plan preset · China (Beijing); both tiers supported", + note: "Token Plan Personal Edition · China (Beijing)", modelInputModalities: ALIBABA_TOKEN_PLAN_INPUT_MODALITIES, modelContextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS, modelMaxOutputTokens: ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS, @@ -929,7 +929,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ defaultModel: "qwen3.7-max", models: ALIBABA_INTL_TOKEN_PLAN_MODELS, liveModels: false, - note: "Team-curated Token Plan preset · International; both tiers supported", + note: "Token Plan Team Edition · Singapore (ap-southeast-1)", metadataModelIdNormalize: "case-insensitive", modelInputModalities: ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES, modelContextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS, diff --git a/src/providers/registry/model-seeds.ts b/src/providers/registry/model-seeds.ts index 9434d07f4e..674b37fe60 100644 --- a/src/providers/registry/model-seeds.ts +++ b/src/providers/registry/model-seeds.ts @@ -573,9 +573,9 @@ 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). -// Both regions support Personal and Team subscriptions. These presets deliberately curate -// a Personal subset for Beijing and a Team subset for international accounts; geography -// does not determine tier. Keep liveModels false: discovery includes non-chat models. +// 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. @@ -610,7 +610,7 @@ export const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record = { }; // 260721 Alibaba Token Plan International (ap-southeast-1 / Singapore, hardened 260721). -// Team-curated multi-vendor lineup, not a region-specific entitlement difference. +// 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. From 68eded73f247cd89f74a10434933c1dbce7df58e Mon Sep 17 00:00:00 2001 From: Oliver Mee <102673257+oliver-mee@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:17:35 +0800 Subject: [PATCH 9/9] fix(providers): preserve destination reasoning defaults and mappings --- src/providers/derive.ts | 8 ++++++- src/providers/routed-config.ts | 8 ++++++- ...alibaba-token-plan-responses-optin.test.ts | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 644633e543..bbb67c2701 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -3,7 +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 { stableUnion } from "./resolved-model-policy-merge"; +import { mapFill, nestedMapFill, stableUnion } from "./resolved-model-policy-merge"; import { PROVIDER_REGISTRY, registryEntryForProviderDestination, @@ -478,6 +478,12 @@ function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistry 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]; } diff --git a/src/providers/routed-config.ts b/src/providers/routed-config.ts index 19011f15b4..7189623ad8 100644 --- a/src/providers/routed-config.ts +++ b/src/providers/routed-config.ts @@ -21,7 +21,7 @@ import { providerSupportsLiveModelDiscovery, } from "./static-model-discovery"; import { resolveModelPolicy } from "./resolved-model-policy"; -import { stableUnion } from "./resolved-model-policy-merge"; +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 { @@ -125,6 +125,12 @@ 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], } : {}), diff --git a/tests/providers/alibaba-token-plan-responses-optin.test.ts b/tests/providers/alibaba-token-plan-responses-optin.test.ts index 71640920fb..f14d541082 100644 --- a/tests/providers/alibaba-token-plan-responses-optin.test.ts +++ b/tests/providers/alibaba-token-plan-responses-optin.test.ts @@ -231,6 +231,17 @@ describe("custom Beijing destination structured-output policy", () => { 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 }]) { @@ -245,6 +256,16 @@ describe("custom Beijing destination structured-output policy", () => { 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"); + }); } }