Skip to Content
ДизайнУнифицированный уровень усилий рассуждений (/effort)

Унифицированный уровень усилий рассуждений (/effort)

Статус реализации. Готово: 5-уровневая шкала + core/reasoning-effort.ts (ограничение/нормализация ранга), глобальная настройка model.reasoningEffort + runtime Config.setReasoningEffort/getReasoningEffort (повторно применяется при переключении моделей в handleModelChange), команда /effort, адаптер дословного уплощения GLM (provider/zai.ts), маппинг Gemini medium/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 }

Существующие трансляторы для каждого провайдера:

ПровайдерФайлПоведение
DeepSeekprovider/deepseek.ts:176-218вложенный → плоский reasoning_effort; low/medium→high, xhigh→max
AnthropicanthropicContentGenerator.ts:521-593, ограничение 665-693, beta hdr 393-431output_config.effort + thinking; ограничение maxhigh + одноразовое предупреждение; beta effort-2025-11-24
GeminigeminiContentGenerator.ts:107-146thinkingConfig/thinkingLevel; low→LOW, high/max→HIGH
OpenAI/GLM/DashScopeopenaiContentGenerator/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:59 clampThinkingLevel): если модель поддерживает уровень, используется он; явный 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 для моделей с адаптивным мышлением, или конвертация в thinkingBudgetTokensadjustMaxTokensForThinking) для старых.
    • 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.
  • Профиль мышления провайдера (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_effortDeepSeek reasoning_effortGLM-5.2+ reasoning_effortAnthropic output_config.effortGemini 3 thinking_levelQwen DashScope
lowlowhigh¹lowlowlowenable_thinking:true
mediummediumhigh¹mediummediummediumtrue
highhighhighhighhigh (default)hightrue
xhighxhighmax¹xhighxhigh ↓high²high ↓²true
maxxhigh ↓ (нет max)maxmaxmax ↓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_thinkingqwen3 / DashScopeадаптер выдает булево значение (только отключение); уровней усилий пока нет
переключатель extra_body.thinking.enabledGLMотдельный переключатель вкл/выкл, не связанный со значением усилий
Следствие: чистый 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)

  1. core: ladder + mappings + clamps. Расширить union-тип с помощью xhigh; добавить центральное ограничение на основе рангов (rank-based central clamp) + supportedReasoningEfforts для каждой модели; вынести три маппера структур в отдельные функции; заполнить бакеты Gemini medium/xhigh↓ + 2.5, подтвердить вывод reasoning_effort для OpenAI/GLM + max↓xhigh, добавить преобразование tier→bool для DashScope; удаление параметров сэмплирования; проверить, что существующий путь удаления reasoning (pipeline.ts:597-602) покрывает повторное воспроизведение истории (history replay) аналогично санитизатору openclaw. Unit-тесты для каждого транслятора провайдера + границы ограничений. Без UI.
  2. cli: setting + direct command. Схема model.reasoningEffort, маппинг конфигурации + runtime-обновление через setReasoningEffort, /effort <tier>, чтение актуального значения в строке состояния. Тесты.
  3. cli: picker dialog. EffortDialog + базовый /effort, подсказки об ограничениях для каждой модели.
  4. 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; одноразовое предупреждение об ограничении срабатывает один раз для комбинации модель+уровень.

Last updated on