Skip to content

fix(mcp): route t3_thread_start/t3_spawn_subagent by plain model name#6

Merged
wizzoapp[bot] merged 1 commit into
mainfrom
fix/model-routing-tools
Jul 1, 2026
Merged

fix(mcp): route t3_thread_start/t3_spawn_subagent by plain model name#6
wizzoapp[bot] merged 1 commit into
mainfrom
fix/model-routing-tools

Conversation

@wizzoapp

@wizzoapp wizzoapp Bot commented Jul 1, 2026

Copy link
Copy Markdown

Problem

To run t3_thread_start / t3_spawn_subagent with a specific model, callers had to pass a full modelSelection object with the correct harness/instance id (e.g. "claudeAgent") — which the calling model had to guess.

Fix

These tools now accept an optional plain model name (e.g. claude-opus-4-8, gpt-5.4). The provider/harness is inferred from the live provider model lists — the same upstream-maintained source the model picker uses — so:

  • No hardcoded model-name patterns. New models (e.g. claude-fable-5) and custom models route correctly with zero code change here, purely from the registry data.
  • When several providers serve the same model (Claude via both claudeAgent and the Cursor aggregator), the native provider wins via PROVIDER_INFERENCE_PRIORITY.
  • Registry aliases (e.g. opus) resolve to the canonical live slug.
  • An explicit but unresolvable model fails with a clear error (rather than silently running on the inherited model); omitting model inherits as before.

Key files

  • packages/shared/src/model.ts: pickModelSelectionFromInstances(model, sources) (pure, 17 unit tests incl. the Fable-5 case + native tie-break).
  • thread/handlers.ts + subagent/handlers.ts: build sources from providerInstanceRegistry.listInstances (+ each instance's getSnapshot) and resolve.
  • Tool descriptions updated so callers know to pass a plain model.

Test

vp check clean; typecheck clean; model tests 17/17; thread+subagent handler suites 9/9. Autoreview (Codex+Claude) clean.

This is Fix #2 of the harness fixes. The t3_schedule_* model field (persisted per-task) is a focused follow-up (needs a DB migration + JSON column).

@wizzoapp

wizzoapp Bot commented Jul 1, 2026

Copy link
Copy Markdown
Author

@codex review

@wizzoapp wizzoapp Bot enabled auto-merge (squash) July 1, 2026 19:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af5fa2340a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/server/src/mcp/toolkits/thread/handlers.ts Outdated
Comment thread packages/shared/src/model.ts Outdated
Comment thread packages/shared/src/model.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR routes t3_thread_start and t3_spawn_subagent by plain model name (e.g. claude-opus-4-8) rather than requiring callers to supply a full modelSelection object with the correct harness/instance id. It also addresses all three issues raised in the prior review round.

  • pickModelSelectionFromInstances (pure, 17 unit tests): resolves a plain model name against the live provider model lists using a two-pass algorithm — direct slug match then alias-map resolution — with PROVIDER_INFERENCE_PRIORITY applied in both passes so native providers always win over aggregators like Cursor, even when the input alias is Cursor-specific.
  • subagent/handlers.ts and thread/handlers.ts: build live modelSources from providerInstanceRegistry.listInstances, resolve the model once, then strip the model field and inject the resolved modelSelection before calling spawnRuntime so the coordinator record and the started thread always share a single resolved selection.
  • PROVIDER_INFERENCE_PRIORITY added to contracts and the model field added to ThreadStartToolInput (inherited by SpawnSubagentInput via field spread).

Confidence Score: 5/5

Safe to merge. The new model-resolution path is covered by 17 unit tests including the previously-missing cursor-alias case, and the single-resolution invariant between the coordinator and the thread is now enforced by stripping the bare model field before delegating to spawnRuntime.

All three issues from the prior review round are fully addressed: pass-2 alias resolution now re-runs the priority walk on the canonical slug rather than returning the aliasing provider, the double-resolution race between coordinator and thread is eliminated by stripping the model field before spawnRuntime, and the missing cursor-only-alias test is in place. No new defects were found in the changed paths.

No files require special attention. The handler integration tests mock ProviderInstanceRegistry with an empty list and do not exercise the new model field directly, but the pure-function unit tests cover that logic exhaustively.

Important Files Changed

Filename Overview
packages/shared/src/model.ts Adds pickModelSelectionFromInstances, ProviderModelEntry, and ProviderModelSource interfaces. Pass-2 alias resolution now collects all canonicals from all providers into a Set, then re-runs the priority-sorted resolveSlug for each canonical — fixing the prior bug where cursor-only aliases bypassed PROVIDER_INFERENCE_PRIORITY.
packages/shared/src/model.test.ts Adds 9 unit tests for pickModelSelectionFromInstances, including the cursor-only-alias test (opus-4.6-thinking → claudeAgent) and the Fable-5 live-list case. Complete coverage of null/empty inputs, default options, multi-instance tie-break, and alias resolution.
apps/server/src/mcp/toolkits/thread/handlers.ts Injects ProviderInstanceRegistry, builds modelSources per-invocation, and delegates to pickModelSelectionFromInstances when input.model is set. resolveModelSelection is now an Effect returning ThreadStartToolError on unknown model — no silent fallback.
apps/server/src/mcp/toolkits/subagent/handlers.ts Resolves model against live provider snapshots before calling spawnRuntime; strips model and injects the already-resolved modelSelection so the thread handler does not re-resolve against a potentially-different registry snapshot — fixing the double-resolution race from the prior review.
packages/contracts/src/model.ts Adds PROVIDER_INFERENCE_PRIORITY constant ordering native providers before aggregators. No model-name patterns — purely provider-identity ordering.
apps/server/src/mcp/toolkits/thread/handlers.test.ts Adds ProviderInstanceRegistry mock (empty list) to satisfy the new service dependency; existing 9 test cases continue to exercise inheritance paths correctly.
apps/server/src/mcp/toolkits/thread/tools.ts Adds optional model: TrimmedNonEmptyString field to ThreadStartToolInput and updates the tool description to guide callers toward plain model names.
apps/server/src/mcp/toolkits/subagent/tools.ts Description-only update to surface the model field (inherited via ...ThreadStartToolInput.fields spread) for callers of t3_spawn_subagent.

Reviews (2): Last reviewed commit: "fix(mcp): route t3_thread_start/t3_spawn..." | Re-trigger Greptile

Comment thread packages/shared/src/model.ts Outdated
Comment thread apps/server/src/mcp/toolkits/subagent/handlers.ts
Comment thread packages/shared/src/model.test.ts
@wizzoapp

wizzoapp Bot commented Jul 1, 2026

Copy link
Copy Markdown
Author

[automated] Addressed in 91f8c3d: (1) exclude disabled provider instances from routing; (2) prefer the source thread's instance when multiple instances of a provider serve the model; (3) preserve the matched model's default option selections. Resolving.

@wizzoapp wizzoapp Bot force-pushed the fix/model-routing-tools branch from af5fa23 to 91f8c3d Compare July 1, 2026 20:05
@wizzoapp

wizzoapp Bot commented Jul 1, 2026

Copy link
Copy Markdown
Author

@codex review

Callers had to pass a full modelSelection with the correct harness/instance
id (e.g. "claudeAgent"), which the model had to guess. Now these tools accept
an optional bare `model` name and resolve it against the LIVE provider model
lists — the same upstream-maintained source the model picker uses — so new
models (e.g. claude-fable-5) and custom models route correctly with no code
change here; there are no hardcoded model-name patterns.

- packages/shared/src/model.ts: pickModelSelectionFromInstances(model, sources)
  matches a plain name against each provider instance's live `snapshot.models`,
  resolving registry aliases (e.g. "opus") to the canonical slug. When several
  providers serve the same model (Claude via both claudeAgent and the Cursor
  aggregator), the native one wins via PROVIDER_INFERENCE_PRIORITY. Returns null
  for unknown models so callers fall back. Returns the actual serving instance,
  so custom instances work too.
- thread/subagent handlers build the sources from
  providerInstanceRegistry.listInstances (+ each instance's getSnapshot) and
  resolve `model` -> selection, else explicit modelSelection, else inherit the
  source thread.
- 17 model unit tests incl. the Fable-5 case (a live model absent from the alias
  maps) and native-provider tie-break; thread/subagent handler suites updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wizzoapp

wizzoapp Bot commented Jul 1, 2026

Copy link
Copy Markdown
Author

[automated] Addressed in 5261fcb: (1) alias resolution now re-runs the priority-ranked match on the canonical slug, so a provider-specific alias (e.g. Cursor's opus-4.6-thinking) still routes its canonical model to the native provider (+test); (2) subagent spawns with the already-resolved selection so the coordinator and thread share one snapshot. Resolving.

@wizzoapp wizzoapp Bot force-pushed the fix/model-routing-tools branch from 91f8c3d to 5261fcb Compare July 1, 2026 20:10
@wizzoapp

wizzoapp Bot commented Jul 1, 2026

Copy link
Copy Markdown
Author

@codex review

@wizzoapp wizzoapp Bot merged commit eabad92 into main Jul 1, 2026
8 checks passed
@wizzoapp wizzoapp Bot deleted the fix/model-routing-tools branch July 1, 2026 20:18

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5261fcbe47

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const modelSelection = resolveModelSelection(input, sourceThread);
const providerInstances = yield* providerInstanceRegistry.listInstances;
const modelSources = yield* Effect.forEach(
providerInstances.filter((providerInstance) => providerInstance.enabled),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude unavailable provider snapshots from model inference

When a provider is enabled but currently unavailable, its snapshot can still advertise fallback models (for example checkClaudeProviderStatus returns models: allModels on CLI/auth probe failures). Because inference only filters on providerInstance.enabled, model: "claude-sonnet-4-6" will prefer the native claudeAgent entry over a working Cursor instance whenever Claude is enabled-but-broken, causing new threads/subagents to route to a provider that cannot run instead of to an actually available provider. Please filter the snapshot by provider status/auth availability, or otherwise rank unavailable snapshots after available ones; the same source-building pattern in the subagent handler needs the same treatment.

Useful? React with 👍 / 👎.

const modelSources = yield* Effect.forEach(
providerInstances.filter((providerInstance) => providerInstance.enabled),
(providerInstance) =>
Effect.map(providerInstance.snapshot.getSnapshot, (snapshot) => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the merged provider catalog for model lookup

This reads each raw instance snapshot, but the model picker is backed by ProviderRegistry, which hydrates cached provider status and merges prior models when a live/pending snapshot is empty. Right after restart, Codex's pending snapshot contains only custom models until the probe finishes, while the picker can still show cached built-in models such as gpt-5.4; an explicit model: "gpt-5.4" then fails as “not served” even though the user selected a visible model. Resolve against the same merged provider catalog (or equivalent cached merge) for both thread and subagent source-building.

Useful? React with 👍 / 👎.

Comment on lines +388 to +392
const canonicals = new Set<string>();
for (const source of sources) {
const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[source.driverKind] ?? {};
const canonical = Object.prototype.hasOwnProperty.call(aliases, trimmed)
? aliases[trimmed]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve aliases independent of enabled source kinds

Alias expansion is limited to driver kinds present in sources, so if claudeAgent is disabled/absent but an enabled aggregator like Cursor serves claude-opus-4-8, an alias such as opus never enters canonicals and the tool rejects the request even though a live provider can run the canonical model. Collect aliases from the registry maps before matching the resulting canonical slugs against live sources (or otherwise allow aliases whose canonical slug is served by an enabled provider).

Useful? React with 👍 / 👎.

* attribute of provider identity, not of model names, so it needs no change
* when new models ship: those are picked up from the registry maps above.
*/
export const PROVIDER_INFERENCE_PRIORITY: ReadonlyArray<ProviderDriverKind> = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Move inference priority out of contracts

AGENTS.md says packages/contracts must stay schema-only, but this new export is runtime routing policy consumed by @t3tools/shared/model to decide which provider wins. Keeping provider inference priority in contracts makes contract consumers depend on server/model-picker policy and expands the package boundary; move this constant to a runtime/shared module instead of adding more non-schema logic here.

Useful? React with 👍 / 👎.

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.

0 participants