Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/design/2026-09-04-model-configured-reasoning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Model-configured reasoning capabilities

## Goal

Make reasoning support part of each existing provider model declaration. After
this foundation lands, adding a model must only require editing its provider
configuration entry.

## Design

`ModelSpec.capabilities.reasoning` declares the model's selectable effort tiers,
provider default, disable support, and disable wire field. Provider setup copies
the capability into `ModelConfig`; the existing model registry then preserves it
with the model's protocol and endpoint identity.

ACP, session restoration, workspace previews, and TUI effort controls read the
capability from the resolved model. The request pipeline resolves the request
model through the same registry, maps a supported tier to top-level
`reasoning_effort`, and emits the configured disable field. Unsupported stored
tiers fall back through the existing default-selection path. Models without an
explicit capability keep existing behavior and receive no new provider field.

## Proof model

This PR configures only the native `deepseek-v4-pro` entry:

- efforts: `high`, `max`;
- default: `high`;
- disable: `thinking.type = disabled`.

Evidence: [DeepSeek Chat Completions](https://api-docs.deepseek.com/zh-cn/api/create-chat-completion/).

No DeepSeek model id or endpoint is added to generic control code. The model's
configuration is the only source of its new capability.

## Follow-up

Qwen 3.8, DeepSeek snapshots, Kimi K3, and GLM models are deliberately deferred
to a separate configuration-only PR. MiniMax M3, Step 3.7, and Coding Plan
remain excluded.
37 changes: 23 additions & 14 deletions packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ import {
buildModelReasoningConfigOption,
buildModelReasoningConfigPreview,
clearReasoningRequestOverrides,
getModelConfiguration,
getConfiguredModelReasoning,
isReasoningSelectionSupported,
PERSIST_REASONING_SELECTION_META_KEY,
parseReasoningSelection,
Expand Down Expand Up @@ -7280,7 +7280,9 @@ class QwenAgent implements Agent {
model.id,
model.registryBaseUrl ?? model.baseUrl,
)?.generationConfig.thinkingMandatory === true,
model.capabilities?.reasoning,
),
model.capabilities?.reasoning,
);
const providerModel: ServeWorkspaceProviderModel = {
modelId,
Expand Down Expand Up @@ -13732,16 +13734,17 @@ class QwenAgent implements Agent {
}
const generation = config.getContentGeneratorConfig?.();
const modelId = generation?.model ?? config.getModel();
const modelReasoning = getConfiguredModelReasoning(config, modelId);
if (
!isReasoningSelectionSupported(
modelId,
selection,
generation?.thinkingMandatory === true,
modelReasoning,
)
) {
return;
}
const modelReasoning = this.getModelReasoningConfiguration(config);
if (generation && modelReasoning && !modelReasoning.toggleOnly) {
clearReasoningRequestOverrides(generation);
}
Expand Down Expand Up @@ -14291,12 +14294,19 @@ class QwenAgent implements Agent {
options: configModelOptions,
};

const modelReasoning = this.getModelReasoningConfiguration(
config,
currentModelId,
);

if (
activeRuntimeSnapshot ||
currentModelId.startsWith(ACP_ROUTE_ID_PREFIX) ||
!isReasoningSelectionSupported(
rawCurrentModelId,
REASONING_EFFORT_DEFAULT,
false,
modelReasoning,
)
) {
return [modeConfigOption, modelConfigOption];
Expand All @@ -14306,10 +14316,6 @@ class QwenAgent implements Agent {
if (!generation) {
return [modeConfigOption, modelConfigOption];
}
const modelReasoning = this.getModelReasoningConfiguration(
config,
currentModelId,
);
const currentModelEffort = config.getReasoningEffort?.();
const reasoningOverride = config.getReasoningEffortOverride?.();
const reasoningOverrideValue = reasoningOverride
Expand Down Expand Up @@ -14353,7 +14359,7 @@ class QwenAgent implements Agent {
? mandatoryUsesDefaultEffort
? modelReasoning.defaultEffort
: normalizedOverrideEffort
? (modelReasoning.efforts.find(
? (modelReasoning.efforts?.find(
(effort) => effort === normalizedOverrideEffort,
) ?? modelReasoning.defaultEffort)
: currentModelEffort
Expand All @@ -14363,11 +14369,15 @@ class QwenAgent implements Agent {
(!reasoningOverride || !overrideDisablesReasoning);
const canDisableReasoning = generation.thinkingMandatory !== true;
const reasoningEffortConfigOption: SessionConfigOption = (modelReasoning
? buildModelReasoningConfigOption(rawCurrentModelId, {
enabled: reasoningEnabled,
effort: effectiveModelEffort,
thinkingMandatory: generation.thinkingMandatory === true,
})
? buildModelReasoningConfigOption(
rawCurrentModelId,
{
enabled: reasoningEnabled,
effort: effectiveModelEffort,
thinkingMandatory: generation.thinkingMandatory === true,
},
modelReasoning,
)
: undefined) ?? {
id: 'reasoning_effort',
name: 'Reasoning effort',
Expand Down Expand Up @@ -14423,8 +14433,7 @@ class QwenAgent implements Agent {
if (completeModelId.startsWith(ACP_ROUTE_ID_PREFIX)) {
return undefined;
}
const reasoning = getModelConfiguration(config.getModel())?.reasoning;
return reasoning?.thinking ? reasoning : undefined;
return getConfiguredModelReasoning(config);
}

private buildSelectableModelOptions(config: Config) {
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/src/acp-integration/model-configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,50 @@ import {
applyReasoningSelection,
buildModelReasoningConfigOption,
buildModelReasoningConfigPreview,
getConfiguredModelReasoning,
getModelConfiguration,
isReasoningSelectionSupported,
resolvePersistedReasoningConfigState,
} from './model-configuration.js';

describe('model configuration manifest', () => {
it('projects reasoning declared by the resolved provider model', () => {
const reasoning = {
thinking: true,
efforts: ['high', 'max'],
defaultEffort: 'high',
disableField: 'thinking',
} as const;
const config = {
getModel: () => 'deepseek-v4-pro',
getAuthType: () => 'openai',
getContentGeneratorConfig: () => ({
model: 'deepseek-v4-pro',
authType: 'openai',
baseUrl: 'https://api.deepseek.com',
}),
getResolvedModelConfig: () => ({ capabilities: { reasoning } }),
} as unknown as Config;

expect(getConfiguredModelReasoning(config)).toBe(reasoning);
expect(
buildModelReasoningConfigOption('deepseek-v4-pro', {}, reasoning),
).toMatchObject({
currentValue: 'high',
options: [{ value: 'none' }, { value: 'high' }, { value: 'max' }],
});
expect(
isReasoningSelectionSupported('deepseek-v4-pro', 'low', false, reasoning),
).toBe(false);
});

it('registers the exact stable qwen3.8-max reasoning controls', () => {
expect(getModelConfiguration('qwen3.8-max')).toEqual({
reasoning: {
thinking: true,
efforts: ['low', 'medium', 'xhigh'],
defaultEffort: 'xhigh',
disableField: 'reasoning_effort',
},
});
});
Expand Down Expand Up @@ -147,6 +179,7 @@ describe('model configuration manifest', () => {
reasoning: {
thinking: true,
toggleOnly: true,
disableField: 'enable_thinking',
},
});
});
Expand Down
Loading
Loading