gut(agents): remove model-selection.ts — CLI runtimes own model selection per MBP (#2455)#2456
Conversation
…tion per MBP (#2455) Delete src/agents/model-selection.ts (170 LOC) per Middleware Boundary Principle. Model selection is owned by CLI runtimes (Claude, Gemini, Codex, OpenCode); middleware only reads the configured default from cfg.agents.defaults. Live config-reading utilities relocated to src/agents/provider-utils.ts (their natural home alongside existing provider normalization + model ref parsing helpers): - buildModelAliasIndex - resolveConfiguredModelRef - resolveDefaultModelForAgent - ModelAliasIndex type Dead stubs eliminated (zero production callers): - resolveModelRefFromString (only the test-mock consumed it) - resolveThinkingDefault (always returned undefined; caller block in chat.ts was unreachable — removed) - getModelRefStatus (always returned undefined) - inferUniqueProviderFromConfiguredModels (always returned undefined; caller block in session-utils.ts was unreachable — removed) - splitTrailingAuthProfile (internal helper; only served resolveModelRefFromString) 14 callers migrated (issue listed 13 + 1 phantom; this PR additionally covered provider-capabilities.ts, transcript-policy.ts, and get-reply.test-mocks.ts which were missed by the issue's path-qualified grep): - Category A (re-export redirects): agent-runner, directive-handling.model-picker, session-store, defaults, gateway-cli-backend.live.test, provider-capabilities, transcript-policy - Category B (type relocation): directive-handling - Category C (live-function callers): status.summary, sticker-cache, session-utils, chat (imports dropped + dead thinkingLevel fallback removed) - Category D (test mocks): agent.test (with modelSelectionModule → providerUtilsModule rename), run.test-harness (mock retarget + stale mock helpers removed), get-reply.test-mocks (dead vi.mock block removed) Zombie-import gate updated: - scripts/check-no-zombie-imports.mjs: removed "agents/model-selection" from fallbackDeadModulePatterns + explanatory block - scripts/data/zombie-import-allowlist.json: -12 entries - .zombie-import-allowlist-baseline: 23 → 11 Baselines self-healed: - .fork-boundary-mock-baseline: 133 → 132 (direct: removed vi.mock in get-reply.test-mocks.ts) - .obsolescence-baseline: 134 → 49 (this PR: -1; pre-existing unbaselined improvements: -84 — verified via stash-based pre-change gate run) Sibling precedents: #2414 (Discord model-picker gut), #2449/#2451 (Mattermost model-picker gut). Surfaced during #2454 H3 restoration; closes #2355 debt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Removes the legacy src/agents/model-selection.ts module per the Middleware Boundary Principle by relocating the remaining config-reading/model-ref utilities into src/agents/provider-utils.ts, and updates all in-repo callsites and gate baselines accordingly.
Changes:
- Delete
src/agents/model-selection.tsand migrate imports tosrc/agents/provider-utils.ts. - Remove unreachable/dead caller blocks that depended on stubbed model-selection exports.
- Update zombie-import gate allowlist/baselines and related scripts to reflect the removal.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/telegram/sticker-cache.ts | Switch default-model resolution import to provider-utils. |
| src/gateway/session-utils.ts | Migrate imports; remove unreachable inferred-provider fallback block. |
| src/gateway/server-methods/chat.ts | Drop unused model-selection import and unreachable thinking-default fallback. |
| src/gateway/gateway-cli-backend.live.test.ts | Update parseModelRef import to provider-utils. |
| src/cron/isolated-agent/run.test-harness.ts | Retarget vitest mock from model-selection to provider-utils; remove stale mock helpers. |
| src/config/defaults.ts | Update provider/model parsing imports to provider-utils. |
| src/commands/status.summary.ts | Update configured-model resolution import to provider-utils. |
| src/commands/agent/session-store.ts | Update isCliProvider import to provider-utils. |
| src/commands/agent.test.ts | Rename module import to providerUtilsModule and update mocks accordingly. |
| src/auto-reply/reply/get-reply.test-mocks.ts | Remove dead vi.mock for deleted model-selection helper. |
| src/auto-reply/reply/directive-handling.ts | Relocate ModelAliasIndex type import to provider-utils. |
| src/auto-reply/reply/directive-handling.model-picker.ts | Update ModelRef/normalizeProviderId import source. |
| src/auto-reply/reply/agent-runner.ts | Update isCliProvider import source. |
| src/agents/transcript-policy.ts | Update normalizeProviderId import source. |
| src/agents/provider-utils.ts | Add config-reading model resolution utilities formerly in model-selection.ts. |
| src/agents/provider-capabilities.ts | Update normalizeProviderId import source. |
| src/agents/model-selection.ts | Delete the module entirely. |
| scripts/data/zombie-import-allowlist.json | Remove allowlisted agents/model-selection callsites. |
| scripts/check-no-zombie-imports.mjs | Remove agents/model-selection from fallback dead-module patterns. |
| .zombie-import-allowlist-baseline | Rebaseline count to match updated allowlist. |
| .obsolescence-baseline | Update baseline after audit changes. |
| .fork-boundary-mock-baseline | Update baseline after removing one mock entry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function buildModelAliasIndex(params: { | ||
| cfg: RemoteClawConfig; | ||
| defaultProvider: string; | ||
| allowPluginNormalization?: boolean; | ||
| }): ModelAliasIndex { | ||
| const byAlias = new Map<string, { alias: string; ref: ModelRef }>(); | ||
| const byKey = new Map<string, string[]>(); | ||
|
|
||
| const rawModels = (params.cfg as any).agents?.defaults?.models ?? {}; | ||
| for (const [keyRaw, entryRaw] of Object.entries(rawModels)) { | ||
| const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider); | ||
| if (!parsed) { | ||
| continue; | ||
| } | ||
| const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim(); | ||
| if (!alias) { | ||
| continue; | ||
| } | ||
| const aliasKey = normalizeAliasKey(alias); | ||
| byAlias.set(aliasKey, { alias, ref: parsed }); | ||
| const key = modelKey(parsed.provider, parsed.model); | ||
| const existing = byKey.get(key) ?? []; | ||
| existing.push(alias); | ||
| byKey.set(key, existing); | ||
| } | ||
|
|
||
| return { byAlias, byKey }; | ||
| } | ||
|
|
||
| export function resolveConfiguredModelRef(params: { | ||
| cfg: RemoteClawConfig; | ||
| defaultProvider: string; | ||
| defaultModel: string; | ||
| allowPluginNormalization?: boolean; | ||
| }): ModelRef { | ||
| const rawModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model) ?? ""; | ||
| if (rawModel) { | ||
| const trimmed = rawModel.trim(); | ||
| const aliasIndex = buildModelAliasIndex({ | ||
| cfg: params.cfg, | ||
| defaultProvider: params.defaultProvider, | ||
| allowPluginNormalization: params.allowPluginNormalization, | ||
| }); | ||
| if (!trimmed.includes("/")) { | ||
| const aliasKey = normalizeAliasKey(trimmed); | ||
| const aliasMatch = aliasIndex.byAlias.get(aliasKey); | ||
| if (aliasMatch) { | ||
| return aliasMatch.ref; | ||
| } | ||
| } | ||
| const parsed = parseModelRef(trimmed, params.defaultProvider); | ||
| if (parsed) { | ||
| return parsed; | ||
| } | ||
| } | ||
| return { provider: params.defaultProvider, model: params.defaultModel }; | ||
| } | ||
|
|
||
| export function resolveDefaultModelForAgent(params: { | ||
| cfg: RemoteClawConfig; | ||
| agentId?: string; | ||
| }): ModelRef { | ||
| return resolveConfiguredModelRef({ | ||
| cfg: params.cfg, | ||
| defaultProvider: DEFAULT_PROVIDER, | ||
| defaultModel: DEFAULT_MODEL, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Newly added config-reading resolution helpers (buildModelAliasIndex, resolveConfiguredModelRef, resolveDefaultModelForAgent) are non-trivial (alias handling + fallback behavior) but src/agents/provider-utils.test.ts currently only covers provider normalization/CLI detection. Add unit tests for alias resolution, provider/model parsing, and default fallback to prevent regressions as config shape evolves.
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import type { RemoteClawConfig } from "../config/config.js"; |
There was a problem hiding this comment.
@typescript-eslint/no-explicit-any is disabled for the whole module, but RemoteClawConfig already types cfg.agents.defaults.models (see src/config/types.agent-defaults.ts). Instead of (params.cfg as any)..., access params.cfg.agents?.defaults?.models directly (and type entryRaw as AgentModelEntryConfig if needed) so the global eslint disable can be removed or scoped to a single line.
| /* eslint-disable @typescript-eslint/no-explicit-any */ | |
| import type { RemoteClawConfig } from "../config/config.js"; | |
| import type { RemoteClawConfig } from "../config/config.js"; | |
| import type { AgentModelEntryConfig } from "../config/types.agent-defaults.js"; |
| const aliasIndex = buildModelAliasIndex({ | ||
| cfg: params.cfg, | ||
| defaultProvider: params.defaultProvider, | ||
| allowPluginNormalization: params.allowPluginNormalization, | ||
| }); | ||
| if (!trimmed.includes("/")) { |
There was a problem hiding this comment.
resolveConfiguredModelRef builds the full alias index unconditionally, even when trimmed already contains / (so alias lookup is skipped). Consider only calling buildModelAliasIndex inside the !trimmed.includes("/") branch to avoid unnecessary work on hot paths like session listing/status.
| const aliasIndex = buildModelAliasIndex({ | |
| cfg: params.cfg, | |
| defaultProvider: params.defaultProvider, | |
| allowPluginNormalization: params.allowPluginNormalization, | |
| }); | |
| if (!trimmed.includes("/")) { | |
| if (!trimmed.includes("/")) { | |
| const aliasIndex = buildModelAliasIndex({ | |
| cfg: params.cfg, | |
| defaultProvider: params.defaultProvider, | |
| allowPluginNormalization: params.allowPluginNormalization, | |
| }); |
Closes #2455.
Summary
Delete
src/agents/model-selection.ts(170 LOC) per Middleware Boundary Principle. Model selection is owned by CLI runtimes (Claude, Gemini, Codex, OpenCode); middleware still reads the configured default fromcfg.agents.defaults.*, which is legitimate config-reading, not selection.Sibling precedents: #2414 (Discord model-picker gut), #2449/#2451 (Mattermost model-picker gut). Surfaced during #2454 H3 restoration; closes the pending-gutting debt at #2355.
Strategy
Live config-reading utilities relocated to
src/agents/provider-utils.ts(their natural home alongside existing provider normalization + model ref parsing helpers):buildModelAliasIndexresolveConfiguredModelRefresolveDefaultModelForAgentModelAliasIndextypeDead stubs eliminated (zero production callers — verified via grep):
resolveModelRefFromString(only the test mock consumed it)resolveThinkingDefault(always returnedundefined; caller block inchat.tswas unreachable — block removed)getModelRefStatus(always returnedundefined)inferUniqueProviderFromConfiguredModels(always returnedundefined; caller block insession-utils.tswas unreachable — block removed)splitTrailingAuthProfile(internal helper; only servedresolveModelRefFromString)Callers migrated (14 total)
The issue listed 13 callers; investigation found 14: issue's path-qualified grep missed same-directory relative-path imports in
src/agents/{provider-capabilities,transcript-policy}.tsand missed thevi.mockstring arg inget-reply.test-mocks.ts(not tracked by the zombie-gate since it is not an import statement). Issue'ssrc/plugin-sdk/mattermost.tsentry was phantom — no import existed.inferUniqueProviderFromConfiguredModelsblock), chat (imports dropped + deadthinkingLevelfallback removed)modelSelectionModule→providerUtilsModulerename), run.test-harness (retarget + stale mock helpers removed), get-reply.test-mocks (deadvi.mockblock removed)Zombie-gate updates
scripts/check-no-zombie-imports.mjs: removed"agents/model-selection"fromfallbackDeadModulePatterns+ its explanatory blockscripts/data/zombie-import-allowlist.json: -12 entries (issue said 13, actual allowlist had 12 — plugin-sdk/mattermost was phantom).zombie-import-allowlist-baseline: 23 → 11Baselines self-healed
.fork-boundary-mock-baseline: 133 → 132 (direct: removedvi.mockinget-reply.test-mocks.ts).obsolescence-baseline: 134 → 49 (this PR: -1; pre-existing unbaselined improvements: -84 — verified via stash-based pre-change gate run)Pre-existing issue surfaced (not caused by this PR, not blocking)
src/gateway/session-utils.test.tshas 20 failing tests inresolveSessionModelIdentityRef/listSessionsFromStoreblocks asserting provider inference viainferUniqueProviderFromConfiguredModels. Those tests fail onmaintoo —inferUniqueProviderFromConfiguredModelswas already a stub returningundefinedthere. The gateway suite is opt-in viaREMOTECLAW_TEST_INCLUDE_GATEWAY=1(seescripts/test-parallel.mjs:115); defaultpnpm testin CI does not run it. This PR removed the already-non-functional caller block; behavior is identical. Follow-up: delete the obsolete tests or restore a config-reading implementation.Test plan
pnpm tsgo— exit 0pnpm lint— 0 warnings, 0 errorspnpm format:check— clean (5115 files)pnpm test— 7298 passed, 3 skipped, 0 failed (288 unit + 7010 full suite)node scripts/check-no-zombie-imports.mjs— passed (11 == baseline 11)node scripts/check-stub-debt.mjs—@ts-expect-error: 12 == baseline 12; fork-boundary-mock: 132 == baseline 132node scripts/check-throwing-stub-callers.mjs— 0 violationsnode scripts/check-attestations.mjs— 92 modules, 315 attestationsnode scripts/check-obsolescence-audit.mjs— 49 == baseline 49bash scripts/ci/check-rebrand-leakage.sh— no files to checkmodel-selectionrefs in src/extensions/apps/packages/ui/test/scripts — zero import hits (only provenance comment inprovider-utils.ts)🤖 Generated with Claude Code