diff --git a/packages/core/src/__tests__/llm-connections.test.ts b/packages/core/src/__tests__/llm-connections.test.ts index 38f45d3718..e6cf144aed 100644 --- a/packages/core/src/__tests__/llm-connections.test.ts +++ b/packages/core/src/__tests__/llm-connections.test.ts @@ -172,6 +172,21 @@ test('the alias table is selected by provider and names only renames', () => { CLAUDE_SUBSCRIPTION_MODEL_ID_ALIASES, ); assert.equal(modelIdAliasesForProvider('anthropic'), undefined); + for (const providerType of ['alibaba-token-plan-cn', 'alibaba-token-plan'] as const) { + assert.deepEqual( + reconcileConnectionAfterModelFetch( + { + defaultModel: 'qwen3.8-max-preview', + enabledModelIds: ['qwen3.8-max-preview'], + hasModelInventory: true, + }, + [{ id: 'qwen3.8-max' }, { id: 'qwen3.7-max' }], + { aliases: modelIdAliasesForProvider(providerType) }, + ), + { defaultModel: 'qwen3.8-max', enabledModelIds: ['qwen3.8-max'] }, + providerType, + ); + } const offered = curatedCatalogFallbackModelsForProvider('claude-subscription') ?? []; for (const [renamed, target] of Object.entries(CLAUDE_SUBSCRIPTION_MODEL_ID_ALIASES)) { assert.ok(offered.includes(target), `${target} is not offered by the curated inventory`); diff --git a/packages/core/src/__tests__/model-catalog.test.ts b/packages/core/src/__tests__/model-catalog.test.ts index ffae8134bf..3a6c57a9ac 100644 --- a/packages/core/src/__tests__/model-catalog.test.ts +++ b/packages/core/src/__tests__/model-catalog.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert'; import { test } from 'node:test'; import { isConnectionReady } from '../connection-readiness.js'; -import type { LlmConnection, ProviderType } from '../llm-connections.js'; +import { PROVIDER_DEFAULTS, type LlmConnection, type ProviderType } from '../llm-connections.js'; import { buildConnectionModelCatalogEntries, buildModelCatalogEntries, @@ -144,3 +144,33 @@ test('unknown persisted provider ids return an empty catalog', () => { [], ); }); + +test('Alibaba Token Plan catalogs the formal Qwen3.8 model instead of its retired preview alias', () => { + const modelId = 'qwen3.8-max'; + for (const providerType of ['alibaba-token-plan-cn', 'alibaba-token-plan'] as const) { + const defaults = PROVIDER_DEFAULTS[providerType]; + assert.equal(defaults.fallbackModels[0], modelId, providerType); + assert.equal(defaults.fallbackModels.includes('qwen3.8-max-preview'), false, providerType); + + const entries = buildConnectionModelCatalogEntries({ + connection: { + slug: providerType, + providerType, + defaultModel: modelId, + modelSource: 'fallback', + }, + }); + const model = entries.find((entry) => entry.id === modelId); + assert.equal(model?.displayName, 'Qwen3.8 Max', providerType); + assert.equal(model?.contextWindow, 1_000_000, providerType); + assert.equal(model?.maxOutputTokens, 131_072, providerType); + assert.equal(model?.structuredOutput, true, providerType); + assert.deepEqual( + model?.capabilities, + { vision: true, reasoning: true, functionCalling: true }, + providerType, + ); + assert.deepEqual(model?.modalities, { input: ['text', 'image', 'pdf'], output: ['text'] }); + assert.equal(model?.canUseAsChatDefault, true, providerType); + } +}); diff --git a/packages/core/src/__tests__/model-thinking.test.ts b/packages/core/src/__tests__/model-thinking.test.ts index 46d86dd539..a136a28808 100644 --- a/packages/core/src/__tests__/model-thinking.test.ts +++ b/packages/core/src/__tests__/model-thinking.test.ts @@ -127,6 +127,17 @@ test('resolveThinkingLevel discards levels the model does not offer', () => { assert.equal(resolveThinkingLevel({ providerType: 'openai' }, 'gpt-5.5', 'xhigh'), 'xhigh'); }); +test('Alibaba Token Plan exposes the formal Qwen3.8 effort and disable contract', () => { + for (const providerType of ['alibaba-token-plan-cn', 'alibaba-token-plan'] as const) { + assert.deepEqual( + [...thinkingVariantsForModel(providerType, 'qwen3.8-max')], + ['off', 'low', 'medium', 'xhigh'], + providerType, + ); + assert.equal(resolveThinkingLevel({ providerType }, 'qwen3.8-max', 'off'), 'off', providerType); + } +}); + // Reasoning replay has no toggle: DeepSeek-like relays require // reasoning_content in tool-call history (400 otherwise), and other relays // ignore it, so the runtime replays unconditionally. That contract is diff --git a/packages/core/src/model-metadata.ts b/packages/core/src/model-metadata.ts index 8009513dfc..a448eaccb1 100644 --- a/packages/core/src/model-metadata.ts +++ b/packages/core/src/model-metadata.ts @@ -314,6 +314,16 @@ const ollamaCloudThinkingModels: Record = Object.fromEntr const STATIC_MODEL_METADATA: Partial>> = { anthropic: ANTHROPIC_MODEL_OVERRIDES, 'claude-subscription': CLAUDE_SUBSCRIPTION_MODEL_METADATA, + 'alibaba-token-plan-cn': { + 'qwen3.8-max': { + thinkingOptions: { efforts: ['none', 'low', 'medium', 'xhigh'], toggle: true }, + }, + }, + 'alibaba-token-plan': { + 'qwen3.8-max': { + thinkingOptions: { efforts: ['none', 'low', 'medium', 'xhigh'], toggle: true }, + }, + }, google: GOOGLE_MODEL_OVERRIDES, cohere: { 'command-a-plus-05-2026': { @@ -488,6 +498,11 @@ export const CLAUDE_SUBSCRIPTION_MODEL_ID_ALIASES: Readonly> = { + 'qwen3.8-max-preview': 'qwen3.8-max', +}; + /** * The rename table that applies to one provider's inventory, or undefined when * its ids carry no such guarantee. @@ -501,7 +516,11 @@ export const CLAUDE_SUBSCRIPTION_MODEL_ID_ALIASES: Readonly> | undefined { - return providerType === 'claude-subscription' ? CLAUDE_SUBSCRIPTION_MODEL_ID_ALIASES : undefined; + if (providerType === 'claude-subscription') return CLAUDE_SUBSCRIPTION_MODEL_ID_ALIASES; + if (providerType === 'alibaba-token-plan-cn' || providerType === 'alibaba-token-plan') { + return ALIBABA_TOKEN_PLAN_MODEL_ID_ALIASES; + } + return undefined; } const CURATED_CATALOG_FALLBACK_MODELS: Partial> = { diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index dd8fb6f811..faa5f5e3fb 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -499,6 +499,9 @@ if (!alibabaTokenPlanGlobal.api) { // the plan's image models (qwen-image / wan) are not tool-callable, so only the // tool-calling text models are pinned here. China and global share one model list. const alibabaTokenPlanModelIds = [ + // qwen3.8-max-preview is a retired compatibility alias which the service + // routes to this formal id. New selections must use the billed model id. + 'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', diff --git a/packages/runtime/src/__tests__/model-factory-thinking.test.ts b/packages/runtime/src/__tests__/model-factory-thinking.test.ts index c2ae66f347..1a5dc56ba8 100644 --- a/packages/runtime/src/__tests__/model-factory-thinking.test.ts +++ b/packages/runtime/src/__tests__/model-factory-thinking.test.ts @@ -224,6 +224,26 @@ describe('buildProviderOptions: thinking level', () => { assert.deepEqual(buildProviderOptions(conn('deepseek'), 'deepseek-chat', 'high'), {}); }); + test('Alibaba Token Plan sends the formal Qwen3.8 effort and disable wires', () => { + for (const providerType of ['alibaba-token-plan-cn', 'alibaba-token-plan'] as const) { + assert.deepEqual( + Object.values(buildProviderOptions(conn(providerType), 'qwen3.8-max', 'xhigh')), + [{ reasoningEffort: 'xhigh' }], + providerType, + ); + assert.deepEqual( + Object.values(buildProviderOptions(conn(providerType), 'qwen3.8-max', 'medium')), + [{ reasoningEffort: 'medium' }], + providerType, + ); + assert.deepEqual( + Object.values(buildProviderOptions(conn(providerType), 'qwen3.8-max', 'off')), + [{ reasoningEffort: 'none' }], + providerType, + ); + } + }); + test('family fallback wires per-model override adapters under their SDK namespaces', () => { // opencode serves models across several protocols via models.dev package // overrides; the family fallback must emit the namespace each SDK consumes. diff --git a/packages/runtime/src/__tests__/responses-wire-contract.test.ts b/packages/runtime/src/__tests__/responses-wire-contract.test.ts index 73bfc8f7e7..1d89f6592b 100644 --- a/packages/runtime/src/__tests__/responses-wire-contract.test.ts +++ b/packages/runtime/src/__tests__/responses-wire-contract.test.ts @@ -43,6 +43,16 @@ function openAiNamespace(options: Record): Record { + test('keeps Qwen3.8 Max on Token Plan Chat until the provider adapter supports Responses', () => { + for (const providerType of ['alibaba-token-plan-cn', 'alibaba-token-plan'] as const) { + assert.equal( + resolveModelRuntime({ providerType }, 'qwen3.8-max').wire, + 'openai-chat', + providerType, + ); + } + }); + test('every Responses model asks for encrypted reasoning', () => { // `store: false` is not a privacy preference here, it is the switch that // makes the SDK add `include: ['reasoning.encrypted_content']` and drop