Skip to content
Merged
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
16 changes: 8 additions & 8 deletions docs/users/configuration/model-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,14 +583,14 @@ The optional `reasoning` field under `generationConfig` controls how aggressivel

### Per-provider behavior

| Protocol / provider | Wire shape | Notes |
| --------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **OpenAI / DashScope** (`qwen3.8-max` family) | Flat `reasoning_effort: <effort>` body parameter | The five `/effort` tiers (`low`, `medium`, `high`, `xhigh`, `max`) are passed through verbatim for any model id starting with `qwen3.8-max` (including dated snapshots and `-latest` aliases); DashScope applies any model-specific mapping. For this family, a configured `enable_thinking` or `thinking_budget` is dropped (with a debug log) whenever an effort tier ships, because DashScope rejects requests carrying `reasoning_effort` together with either field. Other Qwen models continue to map a selected effort to `enable_thinking: true`; a `reasoning_effort` override passes through there without dropping `enable_thinking`, and only a conflicting `thinking_budget` is dropped. |
| **OpenAI / DeepSeek** (`api.deepseek.com`) | Flat `reasoning_effort: <effort>` body parameter | When `reasoning.effort` is set in the nested config shape, it's rewritten to flat `reasoning_effort` and `'low'`/`'medium'` are normalized to `'high'`, `'xhigh'` to `'max'` — mirroring DeepSeek's [server-side back-compat](https://api-docs.deepseek.com/zh-cn/api/create-chat-completion). Top-level `samplingParams.reasoning_effort` or `extra_body.reasoning_effort` overrides skip this normalization and ship verbatim. |
| **OpenAI** (other compatible servers) | `reasoning: { effort, ... }` passed through verbatim | Set via `samplingParams` (e.g. `samplingParams.reasoning_effort` for GPT-5/o-series) when the provider expects a different shape. |
| **Anthropic** (real `api.anthropic.com`) | `output_config: { effort }` plus the `effort-2025-11-24` beta header | Real Anthropic accepts `'low'`/`'medium'`/`'high'` only. `'max'` is **clamped to `'high'`** with a `debugLogger.warn` line (once per generator); if you want max effort, switch the baseURL to a DeepSeek-compatible endpoint that supports it. |
| **Anthropic** (`api.deepseek.com/anthropic`) | Same `output_config: { effort }` + beta header | `'max'` is passed through unchanged. |
| **Gemini** (`@google/genai`) | `thinkingConfig: { includeThoughts: true, thinkingLevel }` | `'low'` → `LOW`, `'high'`/`'max'` → `HIGH`, others → `THINKING_LEVEL_UNSPECIFIED` (Gemini has no `MAX` tier). |
| Protocol / provider | Wire shape | Notes |
| --------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **OpenAI / DashScope** (`qwen3.8-max` family) | Flat `reasoning_effort: <effort>` body parameter | The five `/effort` tiers (`low`, `medium`, `high`, `xhigh`, `max`) are passed through verbatim for any model id starting with `qwen3.8-max` (including dated snapshots and `-latest` aliases); DashScope applies any model-specific mapping. For this family the tier ships alone: a conflicting `enable_thinking` or `thinking_budget` is dropped (warn-logged, once per generator) — DashScope rejects requests combining `reasoning_effort` with `thinking_budget`, and two thinking controls should not ship together. An explicit `enable_thinking: false` in `extra_body` is honoured rather than dropped: it overrides the configured tier as `reasoning_effort: 'none'`, one of the few places `extra_body` does not win verbatim. Other Qwen models continue to map a selected effort to `enable_thinking: true`; a `reasoning_effort` override passes through there unless it conflicts with a `thinking_budget` (a pair DashScope rejects), in which case the inert `reasoning_effort` is dropped and both `enable_thinking` and `thinking_budget` survive. |
| **OpenAI / DeepSeek** (`api.deepseek.com`) | Flat `reasoning_effort: <effort>` body parameter | When `reasoning.effort` is set in the nested config shape, it's rewritten to flat `reasoning_effort` and `'low'`/`'medium'` are normalized to `'high'`, `'xhigh'` to `'max'` — mirroring DeepSeek's [server-side back-compat](https://api-docs.deepseek.com/zh-cn/api/create-chat-completion). Top-level `samplingParams.reasoning_effort` or `extra_body.reasoning_effort` overrides skip this normalization and ship verbatim. |
| **OpenAI** (other compatible servers) | `reasoning: { effort, ... }` passed through verbatim | Set via `samplingParams` (e.g. `samplingParams.reasoning_effort` for GPT-5/o-series) when the provider expects a different shape. |
| **Anthropic** (real `api.anthropic.com`) | `output_config: { effort }` plus the `effort-2025-11-24` beta header | Real Anthropic accepts `'low'`/`'medium'`/`'high'` only. `'max'` is **clamped to `'high'`** with a `debugLogger.warn` line (once per generator); if you want max effort, switch the baseURL to a DeepSeek-compatible endpoint that supports it. |
| **Anthropic** (`api.deepseek.com/anthropic`) | Same `output_config: { effort }` + beta header | `'max'` is passed through unchanged. |
| **Gemini** (`@google/genai`) | `thinkingConfig: { includeThoughts: true, thinkingLevel }` | `'low'` → `LOW`, `'high'`/`'max'` → `HIGH`, others → `THINKING_LEVEL_UNSPECIFIED` (Gemini has no `MAX` tier). |

### `reasoning: false`

Expand Down
42 changes: 41 additions & 1 deletion packages/core/src/core/modalityDefaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
*/

import { describe, it, expect } from 'vitest';
import { defaultModalities } from './modalityDefaults.js';
import {
defaultModalities,
isQwenFamilyWireModel,
isTieredEffortWireModel,
} from './modalityDefaults.js';

describe('defaultModalities', () => {
describe('Google Gemini', () => {
Expand Down Expand Up @@ -295,3 +299,39 @@ describe('defaultModalities', () => {
});
});
});

describe('isQwenFamilyWireModel', () => {
it('matches qwen* ids case-insensitively', () => {
expect(isQwenFamilyWireModel('qwen3.8-max')).toBe(true);
expect(isQwenFamilyWireModel('Qwen3.7-Max')).toBe(true);
expect(isQwenFamilyWireModel('qwen-vl-max')).toBe(true);
});

it('matches the coder-model QWEN_OAUTH default', () => {
expect(isQwenFamilyWireModel('coder-model')).toBe(true);
});

it('rejects non-qwen ids and empty input', () => {
expect(isQwenFamilyWireModel('glm-5.2')).toBe(false);
expect(isQwenFamilyWireModel('kimi-k2.6')).toBe(false);
expect(isQwenFamilyWireModel('')).toBe(false);
expect(isQwenFamilyWireModel(undefined)).toBe(false);
});
});

describe('isTieredEffortWireModel', () => {
it('matches the qwen3.8-max family including snapshots and aliases', () => {
expect(isTieredEffortWireModel('qwen3.8-max')).toBe(true);
expect(isTieredEffortWireModel('qwen3.8-max-preview')).toBe(true);
expect(isTieredEffortWireModel('qwen3.8-max-2026-01-15')).toBe(true);
expect(isTieredEffortWireModel('qwen3.8-max-latest')).toBe(true);
expect(isTieredEffortWireModel('Qwen3.8-Max')).toBe(true);
});

it('rejects other qwen models and non-qwen ids', () => {
expect(isTieredEffortWireModel('qwen3.7-max')).toBe(false);
expect(isTieredEffortWireModel('coder-model')).toBe(false);
expect(isTieredEffortWireModel('glm-5.2')).toBe(false);
expect(isTieredEffortWireModel(undefined)).toBe(false);
});
});
31 changes: 31 additions & 0 deletions packages/core/src/core/modalityDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,34 @@ export function defaultModalities(model: string): InputModalities {
}
return {};
}

/**
* True for wire model ids in the qwen family: any `qwen*` id plus
* `coder-model`, the QWEN_OAUTH default (DEFAULT_QWEN_MODEL in
* config/models.ts, aliased to a Qwen 3.6 Plus hybrid), which doesn't
* start with `qwen` but is the most common hybrid-thinking model for
* first-time users. Shared by the pipeline's disable/tool-choice gates
* and the DashScope provider's effort mapping so the family fact lives
* in one place.
*/
export function isQwenFamilyWireModel(model: string | undefined): boolean {
if (!model) {
return false;
}
const normalized = model.toLowerCase();
return normalized.startsWith('qwen') || normalized === 'coder-model';
}

/**
* True for the qwen3.8-max wire model family — the only family that
* reads the tiered `reasoning_effort` field directly. Prefix-matched so
* dated snapshots and `-latest` aliases are covered, consistent with the
* family pattern in MODALITY_PATTERNS above. Older qwen hybrids expose
* only the on/off `enable_thinking` switch instead.
*/
export function isTieredEffortWireModel(model: string | undefined): boolean {
if (!model) {
return false;
}
return model.toLowerCase().startsWith('qwen3.8-max');
}
Loading
Loading