Skip to content

fix(webui): route Cursor ACP and gate reasoning effort by model#2792

Merged
6 commits merged into
nesquena:masterfrom
RobertoVillegas:fix/cursor-acp-provider-routing
May 27, 2026
Merged

fix(webui): route Cursor ACP and gate reasoning effort by model#2792
6 commits merged into
nesquena:masterfrom
RobertoVillegas:fix/cursor-acp-provider-routing

Conversation

@RobertoVillegas
Copy link
Copy Markdown
Contributor

@RobertoVillegas RobertoVillegas commented May 23, 2026

Complements NousResearch/hermes-agent#30835, which adds cursor-acp as a Hermes provider. This PR is the WebUI-side wiring so users can select Cursor Composer in the browser, route new sessions to the selected provider, and only see the reasoning-effort chip when the active model actually supports configurable effort levels.

Thinking Path

  • Hermes WebUI aims for near 1:1 parity with the Hermes CLI while preserving the project's simple Python + vanilla JavaScript architecture.
  • Cursor ACP is a subprocess provider (agent acp) with slash model IDs like cursor/composer-2.5.
  • WebUI already routes some slash models through @provider: hints, but ACP subprocess IDs could be treated like ordinary slash paths and inherit the configured default provider.
  • New chat creation also reused config.yaml defaults instead of the visible composer picker, so the chip could show Composer while the new session actually started on another provider/model.
  • Model changes mid-session did not evict cached session agents, so provider/model switches could keep serving the previous backend.
  • The composer footer also exposed a reasoning-effort chip globally, even for models/providers that do not support user-selectable effort levels.
  • This PR keeps the composer honest: the selected provider/model is what new sessions use, cached agents are refreshed after model changes, and reasoning controls are shown only when the active model supports them.

What Changed

Cursor ACP picker and routing

api/config.py

  • Add cursor-acp to provider labels and the static model catalog (cursor/composer-2.5, cursor/composer-2, cursor/default).
  • Introduce _ACP_SUBPROCESS_PROVIDERS.
  • Ensure ACP subprocess providers emit explicit @cursor-acp: / @copilot-acp: routing hints in model_with_provider_context() so slash IDs do not inherit the configured default provider.

static/sessions.js

  • newSession() now sends the visible picker model and model_provider in POST /api/session/new.

static/boot.js

  • The model picker onchange persists the selected model/provider and updates the chip even when there is no active session yet.
  • The reasoning chip is re-synchronized after model/provider changes.

api/routes.py

  • Evict the cached session agent on model/provider updates via _evict_session_agent() so subsequent turns use the selected backend.

Model-aware reasoning effort chip

api/config.py

  • Add resolve_model_reasoning_efforts(model_id, provider_id, base_url) with hermes_cli.models lookups and a heuristic fallback when CLI modules are unavailable.
  • Return [] for ACP subprocess providers such as cursor-acp and copilot-acp.
  • Strip WebUI routing hints such as @openai-codex:gpt-5.5 before provider-specific reasoning capability lookup.
  • Extend get_reasoning_status() with supported_efforts and supports_reasoning_effort.

api/routes.py

  • GET /api/reasoning accepts optional model, provider, and base_url query params and passes them to get_reasoning_status().

static/ui.js

  • Add _reasoningEffortQuery() so /api/reasoning is queried with the active session model/provider.
  • Hide the reasoning chip when supported_efforts is empty.
  • Filter dropdown options to the effort levels supported by the active model.

static/commands.js

  • /reasoning status uses the active model/provider context.

CHANGELOG.md

  • Document Cursor ACP picker integration, provider routing fixes, and model-aware reasoning chip visibility.

Tests

  • tests/test_provider_mismatch.py
  • tests/test_new_chat_default_model_frontend.py
  • tests/test_reasoning_effort_model_capabilities.py
  • tests/test_issue1103_reasoning_chip_visibility.py
  • tests/test_reasoning_chip_btw_fixes.py

Why It Matters

Without these fixes, Cursor Composer can appear selectable in the composer while requests silently route to a different provider. That breaks the mental model of the WebUI and makes model switching hard to trust.

The reasoning chip had a similar trust issue: showing effort-level controls for cursor-acp implied the user could configure thinking effort, but that provider does not expose those levels. Separating "display reasoning blocks" (display.show_reasoning) from "choose effort level" (model capability) keeps the composer footer aligned with actual backend capabilities.

Verification

Automated:

pytest tests/test_provider_mismatch.py::test_cursor_acp_slash_model_always_gets_provider_hint \
       tests/test_new_chat_default_model_frontend.py \
       tests/test_provider_mismatch.py::TestFrontendModelProviderState \
       tests/test_reasoning_effort_model_capabilities.py \
       tests/test_issue1103_reasoning_chip_visibility.py \
       tests/test_reasoning_chip_btw_fixes.py \
       -q

Latest focused local result:

31 passed

Manual browser verification on WebUI running at port 8787:

  • Hard-refresh the browser after restart.
  • Select Cursor ACP / cursor/composer-2.5.
    • Expected: reasoning-effort chip is hidden.
    • Expected: new chat preserves model: cursor/composer-2.5 and model_provider: cursor-acp.
  • Select OpenAI Codex / gpt-5.5.
    • Expected: reasoning-effort chip is visible with supported effort levels.
    • Expected: switching back to Composer hides the chip again.
  • Send a message after switching models.
    • Expected: the response comes from the selected provider/model, not a stale cached session agent.

Responsive/UI notes:

  • The PR does not add new composer controls; it conditionally hides/shows an existing chip.
  • The affected surface is the composer footer model/reasoning controls, which remains within the existing no-build, vanilla JS interaction model.

UI Evidence

Before: reasoning effort shown for Cursor Composer even though it has no effect

The reasoning-effort chip was visible in the composer footer for cursor/composer-2.5, implying configurable effort levels for a provider that does not expose them.

Before: reasoning chip visible for cursor/composer-2.5

After: reasoning effort hidden when the active model lacks effort levels

With the same Cursor Composer context, the chip is hidden because /api/reasoning reports no supported effort levels for cursor-acp.

After: reasoning chip hidden for cursor/composer-2.5

Risks / Follow-ups

  • End-to-end Cursor Composer use depends on the upstream provider PR landing first: NousResearch/hermes-agent#30835.
  • copilot-acp gets the same explicit-hint treatment; behavior should be unchanged but is worth a smoke test.
  • Capability detection delegates to hermes_cli.models when available; the heuristic fallback may miss edge-case custom providers until catalog metadata improves.
  • This PR does not change whether thinking/reasoning blocks are rendered in messages (display.show_reasoning remains separate).
  • No new dependencies, build tools, frontend frameworks, or build steps.

Model Used

  • Provider: Cursor ACP (cursor-acp) and OpenAI Codex (openai-codex) during validation/debugging.
  • Models: cursor/composer-2.5, gpt-5.5.
  • AI-assisted implementation, debugging, and PR description review through Hermes/WebUI sessions.

@RobertoVillegas RobertoVillegas changed the title fix(cursor-acp): route slash models and honor picker on new chat fix(webui): route Cursor ACP and gate reasoning effort by model May 23, 2026
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Read the diff at cron-pr-2792 against origin/master and cross-checked the hermes-agent side. Two threads here:

  1. WebUI-side wiring for the new cursor-acp provider so subprocess slash IDs (cursor/composer-2.5) don't inherit the configured default HTTP provider.
  2. Model-capability-aware reasoning chip — /api/reasoning now accepts model/provider/base_url and returns supported_efforts, and the composer hides the chip entirely for providers that don't support configurable effort.

The frontend session-creation honesty fix (visible picker selection actually carried into /api/session/new) is a real bug and lives in the same PR — that's the sort of cross-cutting scope a fix(webui) title is fine with.

Code reference

Cross-repo concern first: the WebUI side adds cursor-acp to _PROVIDER_DISPLAY (api/config.py:697) and _PROVIDER_MODELS (api/config.py:1120-1125), but I cannot find cursor-acp in the agent provider tables:

$ grep -rn "cursor-acp\|cursor_acp" ~/.hermes/hermes-agent/hermes_cli/ | wc -l
0

hermes_cli/models.py has copilot-acp only (line 204+, 937, etc.). This PR explicitly states it complements NousResearch/hermes-agent#30835. Until the agent-side provider lands, the WebUI picker would surface a provider that fails at the resolver. That's fine for a WebUI-side PR that's gated on the agent PR — but please coordinate the merge order: the agent PR has to land first (or land together) or the picker is misleading for any user on the latest WebUI without the matching agent. The CHANGELOG should mention the required hermes-agent minimum version.

The ACP routing hint at api/config.py:1989-2001:

_ACP_SUBPROCESS_PROVIDERS = frozenset({"cursor-acp", "copilot-acp"})

def model_with_provider_context(model_id: str, model_provider: str | None = None) -> str:
    ...
    # ACP subprocess providers always need the explicit hint — their slash IDs
    # are not OpenRouter paths and must not inherit config_provider routing.
    if provider in _ACP_SUBPROCESS_PROVIDERS:
        return f"@{provider}:{model}"

This is the right shape — cursor/composer-2.5 looks like an OpenRouter path but isn't, so without the @cursor-acp: prefix, resolve_model_provider(model) at api/config.py:1641 would fall through to the configured default provider.

The reasoning chip resolver at api/config.py:2106-2178 is well-factored. The capability lookup at api/config.py:2152-2156:

if provider in {"copilot", "github-copilot"}:
    return github_model_reasoning_efforts(hinted_model)

if provider == "openai-codex":
    bare = hinted_model.rsplit("/", 1)[-1]
    return github_model_reasoning_efforts(bare)

Reuses hermes_cli.models.github_model_reasoning_efforts which I confirmed exists at hermes_cli/models.py:3017. The heuristic fallback at api/config.py:2078-2103 is conservative — returns [] for unknown providers rather than the universal effort list — which is the right default for a chip that hides when efforts are empty.

The new-chat picker fix at static/sessions.js:473-486:

const modelSelForNew=$('modelSelect');
let newModelState=null;
if(modelSelForNew&&modelSelForNew.value&&typeof _modelStateForSelect==='function'){
  newModelState=_modelStateForSelect(modelSelForNew,modelSelForNew.value);
}else if(typeof _readPersistedModelState==='function'){
  newModelState=_readPersistedModelState();
}
if(newModelState&&newModelState.model){
  reqBody.model=newModelState.model;
  reqBody.model_provider=newModelState.model_provider||null;
}

/api/session/new at api/routes.py:4677-4679 already passes body.get("model") and body.get("model_provider") into _session_model_state_from_request(...), so this just lights up the picker→server path that was already accepted server-side. Good.

The cached-agent eviction at api/routes.py:5039-5041:

from api.config import _evict_session_agent
_evict_session_agent(body["session_id"])

_evict_session_agent is defined at api/config.py:4259 — uses the right lock contract (_get_session_agent_lock(sid) was already acquired at line 5020 in the same handler). This fixes the subtle bug where switching from cursor/composer-2.5 back to openai/gpt-5.5 in mid-session would keep dispatching to the cached ACP subprocess.

Diagnosis

The technical work is solid and the test surface is the right shape (test_cursor_acp_slash_model_always_gets_provider_hint, test_reasoning_effort_model_capabilities, etc.). The CHANGELOG entry should explicitly call out that cursor-acp requires hermes-agent #30835 to be live or it'll surface a broken picker. CI is green 3.11/3.12/3.13.

Two things to address before merge:

  1. Merge coordination with hermes-agent#30835. As-is, this PR makes Cursor ACP appear in the picker on a stock WebUI install with an older agent. Either gate the entry in _PROVIDER_MODELS["cursor-acp"] on detection that the agent supports the provider (e.g. via hermes_cli.models import probe), or document the minimum agent version in the README / CHANGELOG and add the gate later.

  2. The boot.js change at static/boot.js:950 removes the if(!S.session)return; early-out for modelSelect.onchange. The new path handles the no-session case by syncing chips and persisting the model state, which is the right shape — but please double-check that none of the downstream paths (syncModelChip, syncReasoningChip) assume an active session in their internals. A quick grep -n "S.session" static/ui.js against those functions would confirm.

LGTM otherwise. Solid contributor PR with good test coverage on a non-trivial cross-cutting fix.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Friendly bump @RobertoVillegas — wanted to make sure the comment 32h ago didn't get lost in your inbox. Three concrete next steps on this PR:

  1. The branch is CONFLICTING against master — needs a rebase (we shipped 3 stage batches since this PR was opened).
  2. Once rebased, the /api/reasoning model-capability-gated chip needs cross-provider test coverage so we don't regress the providers that DO support effort tuning.
  3. The Cursor ACP wiring is the part most likely to land first if split into its own PR — happy to land that as a smaller change.

Will keep watching this thread. If you don't have bandwidth in the next few days, no worries — flag it and we'll plan the cherry-pick ourselves.

@RobertoVillegas RobertoVillegas force-pushed the fix/cursor-acp-provider-routing branch from 785b34c to d8e9e87 Compare May 25, 2026 00:41
RobertoVillegas and others added 6 commits May 24, 2026 18:42
- Add cursor-acp to _PROVIDER_DISPLAY with label 'Cursor ACP'
- Add cursor-acp static model list to _PROVIDER_MODELS
- composer-2.5, composer-2, default, cursor-acp
Ensure cursor/composer IDs always resolve via @cursor-acp:, carry the
visible picker selection into POST /api/session/new, persist model
changes before a session exists, and evict cached agents on model switch.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve supported reasoning efforts per active model/provider and pass
that context through /api/reasoning so Composer and other non-configurable
models no longer show a misleading effort picker.

Co-authored-by: Cursor <cursoragent@cursor.com>
Model picker onchange now calls syncReasoningChip after session model/
provider updates, and dropdown selections pass providerId so duplicate
bare model ids resolve to the correct backend capabilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
@RobertoVillegas RobertoVillegas force-pushed the fix/cursor-acp-provider-routing branch from d8e9e87 to 4c4922a Compare May 25, 2026 00:43
@RobertoVillegas
Copy link
Copy Markdown
Contributor Author

Updated this PR after the review feedback:

  • Rebased on current master; GitHub now reports mergeStateStatus: UNSTABLE instead of DIRTY.
  • Added cross-provider reasoning-effort coverage:
    • Cursor/Copilot ACP providers return no configurable reasoning efforts.
    • OpenAI Codex and GitHub Copilot GPT-5 models keep effort levels.
    • OpenRouter Anthropic models keep effort levels, while non-reasoning HTTP models hide the chip.
  • Hardened modelSelect.onchange so pre-session model changes sync both the model chip and reasoning chip without assuming S.session exists.
  • Kept provider context flowing through temporary model options and custom model selections.
  • Kept this as one PR because the Cursor ACP routing, new-chat model selection, and reasoning-chip visibility all depend on the same provider-aware picker/session state. Splitting would either duplicate the provider-state plumbing or leave one half temporarily broken.

Local validation:

pytest tests/test_reasoning_effort_model_capabilities.py \
  tests/test_issue1103_reasoning_chip_visibility.py \
  tests/test_new_chat_default_model_frontend.py \
  tests/test_provider_mismatch.py \
  tests/test_reasoning_chip_btw_fixes.py \
  tests/test_reasoning_chip_js_behaviour.py \
  tests/test_mobile_layout.py::test_reasoning_chip_updates_desktop_and_mobile_controls \
  tests/test_issue2545_xai_oauth_provider.py::test_xai_oauth_model_picker_group_uses_live_catalog \
  tests/test_issue2569_model_provider_picker.py::test_temporary_configured_model_option_carries_provider_badge \
  tests/test_ollama_model_chip_label_regression.py::test_select_model_custom_option_uses_friendly_label_helper -q
# 123 passed, 1 warning

Full-suite note from local macOS: unrelated environment-sensitive tests still fail here (ctl state path uses /Users/rob/.hermes/webui instead of the tmp HERMES_HOME, and two workspace-git tests assume the initial branch is master while local git creates main).

Dependency note: Cursor ACP itself still depends on NousResearch/hermes-agent#30835 landing or a released Agent version containing cursor-acp.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Merged in Release DM / v0.51.141 (stage-batch23 — 4-PR second hold-bucket pass with PRs #2506 #2792 #2888 #2958).

Thanks @RobertoVillegas! 🚢

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants