Skip to content

feat(moa): support per-slot reasoning effort - #61711

Closed
justinschille wants to merge 2 commits into
NousResearch:mainfrom
justinschille:feat/moa-slot-reasoning-effort
Closed

feat(moa): support per-slot reasoning effort#61711
justinschille wants to merge 2 commits into
NousResearch:mainfrom
justinschille:feat/moa-slot-reasoning-effort

Conversation

@justinschille

Copy link
Copy Markdown
Contributor

Summary

Adds optional reasoning_effort support to Mixture of Agents reference and aggregator slots.

This lets a preset tune reasoning depth per slot instead of only at the global agent/session level. For example, a MoA preset can run the same model twice with different reasoning depths, then use a higher-reasoning aggregator:

moa:
  presets:
    deep_review:
      reference_models:
        - provider: openai-codex
          model: gpt-5.6-sol
          reasoning_effort: low
        - provider: openai-codex
          model: gpt-5.6-sol
          reasoning_effort: xhigh
      aggregator:
        provider: openai-codex
        model: gpt-5.6-sol
        reasoning_effort: high

What changed

  • Preserve optional reasoning_effort during MoA config normalization for reference and aggregator slots.
  • Parse per-slot reasoning_effort with the same Hermes reasoning parser used by agent.reasoning_effort.
  • Pass the resolved reasoning config into MoA reference and aggregator call_llm calls.
  • Teach the auxiliary LLM request builder to translate direct reasoning_config into the existing extra_body.reasoning request shape.
  • Show per-slot reasoning effort in hermes moa list output.
  • Document per-slot reasoning effort in the Mixture of Agents user guide.
  • Add regression tests for config normalization, runtime propagation, slot labels, and auxiliary request kwargs.

Why

MoA slots were previously provider/model only. That works for model diversity, but it cannot express useful same-model/different-depth councils. Per-slot reasoning effort makes MoA presets more expressive without changing existing preset behavior. Slots without reasoning_effort continue using provider/Hermes defaults.

Validation

uv run --extra dev python -m pytest \
  tests/hermes_cli/test_moa_config.py \
  tests/agent/test_moa_reasoning_effort.py \
  tests/agent/test_moa_slot_api_mode.py \
  tests/agent/test_moa_aggregator_cost_slot.py -q

Result:

33 passed

Also ran:

git diff --check origin/main...HEAD

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have labels Jul 9, 2026
@kiyo-e

kiyo-e commented Jul 10, 2026

Copy link
Copy Markdown

Thanks for adding per-slot reasoning controls — the config shape and MoA plumbing look useful. I think the native Anthropic auxiliary path still needs one provider-aware hop before this works for slots such as provider: anthropic, model: claude-fable-5.

On current main, call_llm() builds extra_body.reasoning, but _AnthropicCompletionsAdapter.create() then calls:

build_anthropic_kwargs(
    ...,
    reasoning_config=None,
)

and does not merge kwargs["extra_body"] into the native Anthropic request. Therefore this PR's _build_call_kwargs(... reasoning_config=...) -> extra_body.reasoning conversion appears to work for OpenAI/Codex-style transports, but it does not reach native Anthropic's required wire shape:

{
  "thinking": {"type": "adaptive", "display": "summarized"},
  "output_config": {"effort": "medium"}
}

Could the adapter accept the normalized Hermes reasoning_config directly and pass it to build_anthropic_kwargs() instead of hardcoding None? This should remain provider-aware rather than blindly forwarding raw extra_body.reasoning, as discussed in #53932.

A regression test at the final native client boundary would help: use a native Anthropic MoA reference slot with reasoning_effort: medium and assert the kwargs passed to messages.create() contain thinking.type == "adaptive" and output_config.effort == "medium". It would also be useful to run that same assertion through an Anthropic-Messages proxy base URL (e.g. Headroom), since the native Anthropic wire shape should be preserved there too.

I observed the practical impact locally with a Fable reference: acting-agent Fable at explicit medium had ~11.4 s median request latency over 80 calls, while MoA reference calls with no propagated reasoning config had ~61 s median over 19 calls. That is not a controlled causal A/B yet (routing also differed), but it makes correct effort propagation and final-wire testing particularly valuable.

@justinschille
justinschille force-pushed the feat/moa-slot-reasoning-effort branch from abe13b3 to bf15c45 Compare July 10, 2026 07:34
@justinschille

Copy link
Copy Markdown
Contributor Author

Thanks, agreed. I updated the PR to keep this provider-aware instead of relying on extra_body.reasoning for native Anthropic Messages.

What changed:

  • _build_call_kwargs() now carries the normalized Hermes reasoning_config through a private _reasoning_config kwarg only for native/Anthropic-Messages routes (provider: anthropic, api.anthropic.com, and /anthropic proxy URLs). It does not leak that private kwarg to ordinary OpenAI-compatible clients.
  • _AnthropicCompletionsAdapter.create() now passes that normalized config into build_anthropic_kwargs() instead of hardcoding reasoning_config=None.
  • Added a regression at the final native client boundary: test_reasoning_config_reaches_native_anthropic_wire_kwargs asserts the fake messages.create() kwargs contain:
thinking == {"type": "adaptive", "display": "summarized"}
output_config == {"effort": "medium"}
  • Added a second regression that checks the private kwarg is present for native Anthropic and /anthropic proxy URLs, and absent for ordinary OpenAI-wire custom endpoints.

Validation after the patch:

uv run --extra dev python -m pytest \
  tests/agent/test_auxiliary_client.py \
    -k 'AnthropicAuxiliaryReasoningTranslation or CodexAdapterReasoningTranslation' \
  tests/hermes_cli/test_moa_config.py \
  tests/agent/test_moa_reasoning_effort.py \
  tests/agent/test_moa_slot_api_mode.py \
  tests/agent/test_moa_aggregator_cost_slot.py -q

Result:

14 passed, 319 deselected

Also ran git diff --check and py_compile for the touched files.

@justinschille
justinschille force-pushed the feat/moa-slot-reasoning-effort branch from bf15c45 to ea05be7 Compare July 10, 2026 16:45

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for carrying the normalized config into the native Anthropic adapter; that directly addresses the provider-aware issue raised in the discussion.

Problems

  • agent/auxiliary_client.py:6282 emits extra_body.reasoning for every provider, but this direct auxiliary path does not call the provider-profile hooks. That diverges from the standard transport at agent/transports/chat_completions.py:562-594: Kimi requires top-level reasoning_effort (plugins/model-providers/kimi-coding/__init__.py:46-52), while Gemini emits thinking_config (plugins/model-providers/gemini/__init__.py:37-48). Thus configured MoA slots for those providers will not receive their documented reasoning request shape.

Suggested changes

  • Reuse the provider-profile reasoning request construction in the auxiliary builder, while retaining the native Anthropic _reasoning_config handoff.
  • Add final-kwargs coverage for Kimi and Gemini or a custom OpenAI-compatible endpoint, in addition to the Codex and Anthropic cases.

Automated hermes-sweeper review.

Comment thread agent/auxiliary_client.py Outdated

# Provider-specific extra_body
merged_extra = dict(extra_body or {})
if reasoning_config and isinstance(reasoning_config, dict):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generic extra_body.reasoning translation bypasses the provider profiles used by the standard transport. It is not the correct request shape for Kimi (top-level reasoning_effort), Gemini (thinking_config), or custom endpoints (reasoning_effort/think). Please route reasoning_config through the shared provider-profile hooks before retaining this generic fallback.

@justinschille

Copy link
Copy Markdown
Contributor Author

Thanks, this was a good catch. Addressed in 27f25374b.

_build_call_kwargs() now projects reasoning through the same provider-profile hooks as the standard chat-completions transport:

  • ProviderProfile.build_extra_body() for nested provider wire shapes such as Gemini thinking_config
  • ProviderProfile.build_api_kwargs_extras() for top-level or extra_body controls such as Kimi/custom reasoning_effort and OpenRouter/Nous reasoning
  • generic extra_body.reasoning fallback remains for providers without a reasoning-aware profile, including the Codex-compatible adapter path
  • the private _reasoning_config handoff remains for Anthropic-compatible adapters

I also carried reasoning_config through synchronous and asynchronous credential-refresh/fallback rebuilds so a retry or provider fallback cannot silently drop the requested slot effort.

Added final-kwargs regression coverage for Kimi, Gemini, custom OpenAI-compatible endpoints, and the async boundary. Verification:

  • 337 passed across the full auxiliary-client and relevant MoA suites
  • 57 passed in tests/providers/test_provider_profiles.py
  • git diff --check and Python compilation pass

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 11, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #64631 — both of your commits were cherry-picked onto current main with your authorship preserved in git history (rebase merge, commits 3dca75b + 5646dbd). Thanks @justinschille — the provider-profile projection commit in particular was exactly right: per-slot depth is only useful if each advisor's wire gets the correct request shape, and you covered OpenRouter, custom/vLLM/GLM, Ollama-disable, and the Anthropic Messages path.

On top of your commits we removed the ensemble-wide auxiliary.moa_reference / moa_aggregator reasoning_effort knob (it had landed separately in #64597 and would have fought your per-slot design) — task-level values on MoA tasks now warn and point at the preset config. Per-slot preset config is the single surface for MoA reasoning depth.

Closes #53932.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants