Унифицированный уровень усилий рассуждений (/effort)
Статус реализации. Готово: 5-уровневая шкала +
core/reasoning-effort.ts(ограничение/нормализация ранга), глобальная настройкаmodel.reasoningEffort+ runtimeConfig.setReasoningEffort/getReasoningEffort(повторно применяется при переключении моделей вhandleModelChange), команда/effort, адаптер дословного уплощения GLM (provider/zai.ts), маппинг Geminimedium/xhigh, помоделный гейтинг Anthropic (anthropicSupportedEffortTiers+ ограничение: Opus 4.7/4.8 и семейства 5.x пропускаютxhigh/max; Opus 4.6/Sonnet 4.6 принимают толькоmax; Opus 4.5 и неверсионированные id ограничиваются доhigh), и статусная строкаmodel-with-reasoning(обновляется в реальном времени при/effort), маппинг DashScope уровень→bool (установленное усилие включаетenable_thinkingдля гибридных моделей qwen ; единственная колонка для расширения, когда qwen выпустит реальное полеreasoning_effort), и интерактивныйEffortDialog— вызов/effortбез аргументов открывает выбор уровня в интерактивном режиме (и выводит список уровней неинтерактивно), подключенный черезuse-effort-command, UI контексты,DialogManagerиuseDialogClose. Ничего не отложено.
Проблема
Каждый провайдер с поддержкой рассуждений предоставляет свой собственный “регулятор” для “насколько глубоко модель должна думать”: OpenAI/DeepSeek/GLM используют плоскую строку reasoning_effort, Anthropic использует output_config.effort (плюс устаревший thinking.budget_tokens), Gemini 3 использует thinking_level (Gemini 2.5 использовал thinkingConfig.thinkingBudget), а Qwen/DashScope имеет только булево значение enable_thinking.
Ядро уже содержит унифицированную структуру конфига reasoning: { effort }, и каждый адаптер провайдера уже транслирует её (см. Текущее состояние), но не существует пользовательского способа выбрать уровень усилий во время выполнения. Уровень можно задать только вручную, отредактировав конфиг генерации для конкретной модели. Нам нужна единая команда /effort, которая предлагает небольшой набор уровней, сопоставляет их с тем, что поддерживает активный провайдер, и сохраняет выбор.
Унифицированный слой также должен сделать добавление нового провайдера тривиальным: когда у модели, которая сейчас имеет только переключатель вкл/выкл (например, qwen3), появятся настоящие уровни усилий, единственным изменением должна быть одна строка в таблице маппинга/возможностей.
Цели
- Единая шкала усилий для пользователя:
low | medium | high | xhigh | max(5 уровней). - Слэш-команда
/effort:/effort <tier>устанавливает напрямую; вызов/effortбез аргументов открывает диалог выбора. - Единая глобальная настройка, применяемая ко всем моделям и сохраняемая между сессиями.
- Слой трансляции и ограничения (clamp) для каждого провайдера: неподдерживаемый уровень откатывается к ближайшему поддерживаемому для активной модели с одноразовым предупреждением (переиспользуя существующий UX ограничения Anthropic).
- Отображение в реальном времени через существующий пресет статусной строки
model-with-reasoning. - Добавление/настройка провайдера = редактирование одной таблицы возможностей/маппинга, без новой обвязки.
Нецели
- Нет уровня
off. Полное отключение рассуждений остается отдельной существующей концепциейreasoning: false;/effortпереключает только между активными уровнями. - Нет сохраняемых усилий для каждой модели (решение: единая глобальная настройка).
- Нет UI для сырого
budget_tokens. Провайдеры, основанные на бюджете (Gemini 2.5, устаревший Anthropic), управляются маппингом уровень→бакет, а не раскрываются численно. - Никаких изменений в существующей обвязке запросов для каждого провайдера, кроме заполнения пробелов в маппинге и ограничений.
- Никакой интеграции с десктопом (в десктопе есть собственная обвязка
thinkingLevel; вне скоупа).
Текущее состояние
Унифицированный тип конфига — [packages/core/src/core/contentGenerator.ts:104-118]:
reasoning?: false | { effort?: 'low' | 'medium' | 'high' | 'max'; budget_tokens?: number }Существующие трансляторы для каждого провайдера:
| Провайдер | Файл | Поведение |
|---|---|---|
| DeepSeek | provider/deepseek.ts:176-218 | вложенный → плоский reasoning_effort; low/medium→high, xhigh→max |
| Anthropic | anthropicContentGenerator.ts:521-593, ограничение 665-693, beta hdr 393-431 | output_config.effort + thinking; ограничение max→high + одноразовое предупреждение; beta effort-2025-11-24 |
| Gemini | geminiContentGenerator.ts:107-146 | thinkingConfig/thinkingLevel; low→LOW, high/max→HIGH |
| OpenAI/GLM/DashScope | openaiContentGenerator/pipeline.ts:689-717 (buildReasoningConfig), strip 597-602 | пробрасывает/удаляет reasoning_effort; DashScope добавляет preserve_thinking |
Пробелы: в объединении отсутствует xhigh; у Gemini отсутствует medium и правило xhigh→high;
необходимо подтвердить, что общий пайплайн выдает reasoning_effort для обычного
OpenAI/GLM и ограничивает max→xhigh; у DashScope нет маппинга уровень→bool.
Предшествующий опыт: openclaw
openclaw/openclaw решает ту же проблему с помощью более зрелой архитектуры, которую
мы заимствуем (изучено в ~/Documents/openclaw):
- Единая каноническая шкала + числовые ранги (
src/auto-reply/thinking.shared.ts):ThinkLevel = off|minimal|low|medium|high|xhigh|adaptive|maxсTHINKING_LEVEL_RANKS(off:0 … high:40, xhigh:60, max:70; adaptive≡30). - Ограничение на основе ранга (
src/llm/model-utils.ts:59clampThinkingLevel): если модель поддерживает уровень, используется он; явныйnullдля отказа от xhigh/max является жестким лимитом (сначала понижаем); иначе предпочитаем следующий более сильный поддерживаемый уровень, иначе понижаем — никогда молча не повышаем стоимость выше лимита модели. - Возможности для каждой модели, а не только для провайдера: каталог содержит
compat.supportedReasoningEffortsиthinkingLevelMapдля каждой модели (значение илиnull). - Три маппера форматов, по одному на каждое семейство API:
- OpenAI-совместимый —
mapThinkingLevelToReasoningEffort():off→none,adaptive→medium,max→xhigh, иначе проброс →none|minimal|low|medium|high|xhigh. - Anthropic —
mapThinkingLevelToEffort(model, level): ограничение, затем выдачаoutput_config.effortдля моделей с адаптивным мышлением, или конвертация вthinkingBudgetTokens(сadjustMaxTokensForThinking) для старых. - Gemini —
resolveGoogleGemini3ThinkingLevel(): Gemini 3 Pro → LOW/HIGH, Flash → MINIMAL/LOW/MEDIUM/HIGH; Gemini 2.5 маппит бюджет на уровень (≤0→MINIMAL, ≤2048→LOW, ≤8192→MEDIUM, иначе HIGH;gemini-2.5-proотклоняет бюджет 0 — мышление обязательно). - Обертка DeepSeek V4:
off→удаление;xhigh|max→max, иначеhigh.
- OpenAI-совместимый —
- Профиль мышления провайдера (
src/plugins/provider-thinking.types.ts): объявляетlevels/defaultLevel; бинарные провайдеры хранятlow, но отображаютon. - Санитайзер рассуждений (
extensions/opencode-go/reasoning-sanitizer.ts): удаляетreasoning_content/reasoning_effortи части мышления при воспроизведении истории для провайдеров, которые их отклоняют.
Что мы берем: центральное ограничение на основе ранга, объявление возможностей для каждой модели,
три маппера форматов и точные бакеты бюджета Gemini 2.5. Что мы отбрасываем для v1: пользовательские уровни minimal/adaptive (решение = 5
уровней) — они остаются валидными внутренними целями нормализации, чтобы каталог моделей мог
по-прежнему их объявлять.
Дизайн
Шкала усилий и таблица возможностей
Каноническая упорядоченная шкала: low < medium < high < xhigh < max.
Каждый провайдер объявляет поддерживаемое подмножество; транслятор ограничивает запрошенный
уровень вниз по шкале до ближайшего поддерживаемого. Маппинг (канонический →
значение в wire-формате), где ↓ обозначает ограничение:
| Уровень | OpenAI reasoning_effort | DeepSeek reasoning_effort | GLM-5.2+ reasoning_effort | Anthropic output_config.effort | Gemini 3 thinking_level | Qwen DashScope |
|---|---|---|---|---|---|---|
| low | low | high¹ | low | low | low | enable_thinking:true |
| medium | medium | high¹ | medium | medium | medium | true |
| high | high | high | high | high (default) | high | true |
| xhigh | xhigh | max¹ | xhigh | xhigh ↓high² | high ↓² | true |
| max | xhigh ↓ (нет max) | max | max | max ↓high² | high ↓² | true |
¹ Документированная внутренняя группировка DeepSeek/GLM (low/medium ≡ high, xhigh ≡ max).
² Ограничено до документированного потолка модели (варьируется для моделей Anthropic; Gemini 3
ограничен на high). Модели Gemini 2.5 маппят уровень на бакет thinkingConfig.thinkingBudget
вместо thinking_level.
Ограничение является центральным и основано на рангах (заимствовано из openclaw
clampThinkingLevel): каждому уровню присваивается ранг
(low:20, medium:30, high:40, xhigh:60, max:70); провайдер/модель объявляет свой
поддерживаемый набор (и опциональные null жесткие лимиты для xhigh/max); ограничение выбирает
ближайший поддерживаемый уровень — запросы с жестким лимитом понижаются, иначе предпочитается
следующий поддерживаемый уровень на или ниже запрошенного. Это заменяет ad-hoc ограничения для каждого адаптера
(например, текущее max→high у Anthropic).
Возможности объявляются для каждой модели, а не только для провайдера (урок openclaw):
запись модели в каталоге / пресет провайдера содержит
supportedReasoningEfforts?: EffortTier[] (и опциональную карту переопределений для каждой модели).
По умолчанию, если не задано = полный поддерживаемый набор провайдера. Новый
провайдер/модель — это одна строка в таблице; ограничение + три маппера форматов остаются неизменными.
Три маппера форматов отвечают за трансляцию в wire-формат (по одному на каждое семейство API), получая уже ограниченный уровень:
toReasoningEffort(tier)— плоскийreasoning_effortдля OpenAI/DeepSeek/GLM/DashScope (для DashScope вместо этого → булевоenable_thinking).toAnthropicThinking(tier, model)—output_config.effortдля адаптивных моделей, иначеthinking.budget_tokens.toGeminiThinking(tier, model)—thinking_level(Gemini 3) или бакетthinkingConfig.thinkingBudget(Gemini 2.5, пороги согласно openclaw).
Гигиена параметров сэмплирования
DeepSeek и GLM отклоняют temperature/top_p/presence_penalty/frequency_penalty
в режиме мышления. Когда транслятор включает мышление для этих провайдеров, он должен
удалить эти параметры сэмплирования из тела запроса.
Расхождения в форме полей для OpenAI-совместимых API
“OpenAI-совместимый” НЕ подразумевает единое поле для усилий. Канонический конфиг — это
вложенный объект reasoning: { effort }; buildReasoningConfig()
(pipeline.ts:689-717) пробрасывает его дословно, без маппинга значений. Каждый
провайдер, чье wire-поле отличается, должен преобразовать его в своем хуке buildRequest.
Известные формы:
| Wire-форма | Провайдеры | Обработка в qwen-code |
|---|---|---|
вложенный reasoning: { effort } | OpenAI Responses, OpenRouter, gpt-5.x | проброс (по умолчанию) ✅ |
плоский reasoning_effort верхнего уровня | DeepSeek, GLM/z.ai, OpenAI Chat Completions, Groq | Адаптер DeepSeek делает плоским ✅; у GLM нет адаптера → сейчас отправляет вложенную форму, вероятно, неверно ❌ |
булево enable_thinking | qwen3 / DashScope | адаптер выдает булево значение (только отключение); уровней усилий пока нет |
переключатель extra_body.thinking.enabled | GLM | отдельный переключатель вкл/выкл, не связанный со значением усилий |
Следствие: чистый passthrough “просто работает” только для провайдеров, принимающих вложенную структуру. В PR1 необходимо добавить flattening для GLM/z.ai (по аналогии с deepseek.ts), а когда qwen добавит поле effort, расширить адаптер DashScope для вывода той структуры, которую документирует API qwen (скорее всего, плоский reasoning_effort). Новый провайдер поддерживается автоматически только если он принимает вложенную каноническую структуру; в противном случае требуется преобразование (reshape) через один хук. |
Поток конфигурации и сохранение
- Новая глобальная настройка
model.reasoningEffort:'low' | 'medium' | 'high' | 'xhigh' | 'max', добавляется вsettingsSchema.ts(рядом с узломgenerationConfig,1412-1504). - На этапе сборки content-generator слой конфигурации маппит
model.reasoningEffortвgenerationConfig.reasoning.effort(единый источник истины для существующих трансляторов). Одно глобальное значение для всех моделей. - Изменение в runtime: добавить
config.setReasoningEffort(tier)(рядом сswitchModel,config.ts:~2047), который обновляетgenerationConfig.reasoning.effortв памяти и пересобирает активный ContentGenerator, затем вызываетpersist_setting('model.reasoningEffort', tier).
Интерфейс CLI
- Новый
effortCommand.ts(по аналогии сmodelCommand.ts:39-79):/effort→{ type: 'dialog', dialog: 'effort' }/effort high→ валидация уровня (tier), вызовconfig.setReasoningEffort, сохранение, сообщение-подтверждение.completion()предлагает 5 уровней.
- Новый Ink-компонент
EffortDialog+ регистрация типа диалога'effort'вcommands/types.ts:168-198. Диалог выводит список 5 уровней и помечает, какие из них будут ограничены (clamped) для текущей модели (например, “max → high для этой модели”). - Строка состояния: существующий пресет
model-with-reasoning(statusLinePresets.ts:13,46-51) считывает актуальное значение effort — новый пресет не требуется.
Изменение типов
Расширить union-тип effort в contentGenerator.ts:104-118, добавив 'xhigh'. Путь отключения через reasoning: false остается без изменений.
Этапы (небольшие PR, каждый привязан к issue)
- core: ladder + mappings + clamps. Расширить union-тип с помощью
xhigh; добавить центральное ограничение на основе рангов (rank-based central clamp) +supportedReasoningEffortsдля каждой модели; вынести три маппера структур в отдельные функции; заполнить бакеты Geminimedium/xhigh↓+ 2.5, подтвердить выводreasoning_effortдля OpenAI/GLM +max↓xhigh, добавить преобразование tier→bool для DashScope; удаление параметров сэмплирования; проверить, что существующий путь удаления reasoning (pipeline.ts:597-602) покрывает повторное воспроизведение истории (history replay) аналогично санитизатору openclaw. Unit-тесты для каждого транслятора провайдера + границы ограничений. Без UI. - cli: setting + direct command. Схема
model.reasoningEffort, маппинг конфигурации + runtime-обновление черезsetReasoningEffort,/effort <tier>, чтение актуального значения в строке состояния. Тесты. - cli: picker dialog.
EffortDialog+ базовый/effort, подсказки об ограничениях для каждой модели. - docs. Страница effort в
docs/users/; перекрестные ссылки на документацию по reasoning/token-caching.
Покрытие тестами
Наиболее ценные проверки: каждый транслятор провайдера выводит правильное сетевое поле (wire field) для каждого уровня, включая границы ограничений (max в OpenAI → xhigh, xhigh/max в модели Gemini-3 / ограниченной Anthropic → high); параметры сэмплирования удаляются, когда thinking включен для DeepSeek/GLM; model.reasoningEffort проходит полный цикл (round-trip) через настройки и попадает в generationConfig.reasoning.effort; setReasoningEffort пересобирает ContentGenerator; одноразовое предупреждение об ограничении срабатывает один раз для комбинации модель+уровень.