Skip to content

feat(conversation): Auto phase routing with aionrs model hot-swap - #4146

Open
JeffC0628 wants to merge 7 commits into
iOfficeAI:mainfrom
JeffC0628:feat/auto-phase2-routing
Open

feat(conversation): Auto phase routing with aionrs model hot-swap#4146
JeffC0628 wants to merge 7 commits into
iOfficeAI:mainfrom
JeffC0628:feat/auto-phase2-routing

Conversation

@JeffC0628

Copy link
Copy Markdown

Summary

Test plan

Related: Phase 1 PR #4144

@IceyLiu

IceyLiu commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this — the direction is right and the engineering hygiene is good (self-contained autoModel/ module, PRD included, all 13 locales complete, unit tests present).

I ran the full gate locally on feat/auto-phase2-routing:

Check Result
bunx tsc --noEmit ✅ 0 errors
bun run test -- tests/unit/renderer/autoModel ✅ 9/9 passed
bun run i18n:types + scripts/check-i18n.js ✅ in sync, all locales
bun run lint --quiet ✅ 0 errors

Note for reviewers: the feat/auto-phase2-routing branch already contains all commits from #4144, so merging this PR covers both.

Below are the blocking and should-fix items before merge.


🔴 Blocking: unmerged AionCore dependency + a regression on the existing model-switch path

This PR depends on AionCore#923, which is still open. The Auto path itself degrades gracefully (setConfigOption is wrapped in try/catch), so that part is fine.

The problem is ChatConversation.tsxonSelectModel no longer stops the active turn when the provider is unchanged:

const sameProvider = conversation.model?.id === selected.id;
if (runtimeView.activeTurnId && !sameProvider) { /* stop */ }

This changes behaviour for all manual model switches, not just Auto. Without Core#923, Core does not hot-swap either, so a user switching models mid-turn within the same provider gets a UI that says the model changed while the turn keeps running on the old one — a silent no-op regression on a path everyone uses.

Please either land and release Core#923 first, or split this sameProvider stop-skip out of the PR so it ships together with the Core support.

🟠 Vision filtering is dead code — image sends can pick a text-only model

requireVision is declared in ResolveAutoModelInput and honoured inside resolveAutoModel, but no caller ever passes it:

resolveAutoModel.ts:34   if (input.requireVision && ...)
resolveAutoModel.ts:53   if (input.requireVision && ...)
types.ts:48              requireVision?: boolean;

AionrsSendBox has files in scope at the applyAutoModelForTurn call site, and handleSelectAuto has no vision context at all. Sending an image in an Auto conversation can therefore resolve a non-vision worker model and fail the request. Please thread requireVision through from the send path (and consider re-resolving when attachments are present).

🟠 The tier heuristics rank the wrong models, and the preference switch is a no-op

I ran scoreModelForSlots / preferenceWeight over a realistic catalogue (claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5, gpt-5, gpt-4o-mini, deepseek-chat, kimi-k2, glm-4.6, qwen3-coder-plus, gemini-3-pro-preview):

cost/planner    -> gemini-3-pro-preview:5   opus:4  gpt-5:4  sonnet:3
balance/planner -> gemini-3-pro-preview:7   opus:4  gpt-5:4  sonnet:3
quality/planner -> gemini-3-pro-preview:14  opus:8  gpt-5:8  sonnet:6

cost/worker     -> qwen3-coder-plus:12  gemini-3-pro-preview:10  sonnet:8  haiku:8
balance/worker  -> qwen3-coder-plus:12  sonnet:8  deepseek-chat:8
quality/worker  -> qwen3-coder-plus:12  sonnet:8.9  gemini-3-pro-preview:8.1

cost/utility    -> haiku:6  gpt-4o-mini:6  gemini-3-pro-preview:6
balance/utility -> haiku:4  gpt-4o-mini:4  gemini-3-pro-preview:4

Four concrete problems:

  1. gemini-3-pro-preview always wins planner, ahead of Opus and GPT-5, because it double-scores on both /pro-preview/i (+4) and /gemini.*pro/i (+3) while /opus/i only yields +4.
  2. qwen3-coder-plus always wins worker (12) by double-scoring /qwen.*plus/i and /coder/i. Any model with coder in its name captures the worker seat regardless of size or price.
  3. cost / balance / quality produce identical winners in every seat. The preference selector in Settings has no observable effect on this catalogue.
  4. tierHeuristics.ts:41 is an identity expression:
    utility: Math.max(utility, planner > 6 ? 0 : utility)
    Math.max(u, 0) === u for all u >= 0, and Math.max(u, u) === u. The intended "zero out utility for strong planners" never happens — which is why gemini-3-pro-preview also ties for the utility seat.

Also, kimi-k2 and glm-4.6 score {planner:0, worker:4, utility:0} — treated as unknown. Regex-guessing model names is going to keep drifting as new models ship. Consider an explicit capability/tier table, or requiring explicit slot bindings and treating automatic as a last resort.

🟠 hasPriorUserTurns resets on every conversation open, so reopening re-runs planner

useEffect(() => { autoUserTurnsRef.current = 0; }, [conversation_id]);
...
hasPriorUserTurns: autoUserTurnsRef.current > 0

The counter is in-memory only. Reopen a 50-turn Auto conversation after a restart and the next message is classified as the first turn → planner. That defeats the cost-saving goal in the most common long-running scenario, which is exactly the one #4143 targets. Derive this from message history or from the persisted extra.auto_model state instead.

🟡 Two features are wired to nothing

  • consecutiveWorkerFailures is read in decideAutoModelPhase and forwarded by applyAutoModelForTurn, but no production caller ever supplies it. The "≥2 worker failures escalate to planner" rule in the PRD is unreachable outside the unit test.
  • The utility slot is configurable in Settings but never consumed by any resolve path (PRD defers it to F-AUTO-07). Users can bind a model to a seat that does nothing — please hide it until it is wired, or wire it.

🟡 AutoModelSettingsBlock renders above the empty state

In ModelModalContent.tsx the block is now rendered unconditionally at the top of the scroll area, ahead of the !data || data.length === 0 branch. A first-run user with zero providers sees three empty slot dropdowns before the "no configured models" onboarding copy. Suggest moving it below the provider list and gating it on data?.length.

🟡 Auto is silently skipped for team conversations

The send path is guarded by if (autoEnabled && !teamSendMessage), but the pill still renders Auto · {phase}/{model}. The UI claims routing that isn't happening.

🟡 Per-turn latency: 3 extra round trips before each send

useAcpConfigOptions.setConfigOption does prepareSetRuntime()fetchConfigOptionsOnce()PUT config-options/model, and then applyAutoModelForTurn issues a PATCH /conversations/:id. That's 3–4 sequential round trips added in front of every Auto send, on the critical path to first token. Worth short-circuiting when the resolved model equals the current one (the alreadyOnModel check covers the PUT but not the PATCH), and consider skipping the pre-fetch.

🟡 Possible bypass of a locked model option

handleSheetModelSelect guards on runtimeConfig.isConfigOptionBlocked?.('model'), but the Auto send path does not. If setConfigOption throws config_update_in_progress (or the option is blocked by an assistant that pins the model), the error is swallowed by the try/catch and the code still persists conversation.model. Please confirm Auto respects the same block.


Housekeeping

The branch is 12 commits behind main — please rebase so the merge diff can be re-checked.


Happy to re-review once the Core dependency is resolved and the vision / first-turn / heuristics items are addressed. The module boundaries and test setup are solid, so most of this is contained work.

JeffC0628 and others added 5 commits August 28, 2026 08:53
Closes iOfficeAI#4143

Co-authored-by: Cursor <cursoragent@cursor.com>
Closes iOfficeAI#4143

Co-authored-by: Cursor <cursoragent@cursor.com>
Phase 2 for iOfficeAI#4143: decide planner/worker before each send, hot-swap via config-options when provider matches, show Auto · phase/model. Depends on AionCore aionrs model hot-swap.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore stop-on-model-change during active turns, pass requireVision when attachments include images, fix tier heuristics/preference scoring, and hide Auto settings when no providers are configured.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JeffC0628
JeffC0628 force-pushed the feat/auto-phase2-routing branch from 2ed4140 to 6ca8e31 Compare August 28, 2026 00:53
@JeffC0628

Copy link
Copy Markdown
Author

Addressed @IceyLiu review feedback in latest push (rebased on main):

  • Blocking: Restored stop-on-model-change for manual model switches during active turns (same-provider hot-swap only on Auto per-turn path).
  • Should-fix:
    equireVision\ wired when send includes image attachments; tier heuristics/preference scoring fixed; Auto settings hidden when no providers configured.

Superseded #4144 (closed). Auto hot-swap at send time still depends on AionCore #923.

2 similar comments
@JeffC0628

Copy link
Copy Markdown
Author

Addressed @IceyLiu review feedback in latest push (rebased on main):

  • Blocking: Restored stop-on-model-change for manual model switches during active turns (same-provider hot-swap only on Auto per-turn path).
  • Should-fix:
    equireVision\ wired when send includes image attachments; tier heuristics/preference scoring fixed; Auto settings hidden when no providers configured.

Superseded #4144 (closed). Auto hot-swap at send time still depends on AionCore #923.

@JeffC0628

Copy link
Copy Markdown
Author

Addressed @IceyLiu review feedback in latest push (rebased on main):

  • Blocking: Restored stop-on-model-change for manual model switches during active turns (same-provider hot-swap only on Auto per-turn path).
  • Should-fix:
    equireVision\ wired when send includes image attachments; tier heuristics/preference scoring fixed; Auto settings hidden when no providers configured.

Superseded #4144 (closed). Auto hot-swap at send time still depends on AionCore #923.

@JeffC0628

Copy link
Copy Markdown
Author

Addressed @IceyLiu review feedback (rebased on main, CI green):

  • Blocking: Restored stop-on-model-change for manual model switches during active turns.
  • Should-fix: requireVision when send includes images; tier heuristics/preference scoring fixed; Auto settings hidden when no providers configured.

#4144 closed (superseded). Auto per-turn hot-swap still depends on AionCore #923.

Use loaded user turns instead of session-local counter so reopened Auto conversations route to worker correctly. Add applyAutoModelForTurn and vision/history helper tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JeffC0628

Copy link
Copy Markdown
Author

Additional hardening after self-review:

  • Fix: \hasPriorUserTurns\ now derives from loaded message history (\conversationHasUserTurns) instead of a session-local counter — reopening an existing Auto conversation no longer mis-routes every send to planner.
  • Tests: added \�pplyAutoModelForTurn, vision/history helpers, requireVision filtering, and balance worker selection coverage (22 autoModel tests total).

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants