Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex review |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Final validation checkpoint — not merged
Pausing for explicit acceptance of that UAT carry-forward. No merge was performed. All owned monitoring processes have ended. Generated with |
Readiness record — ready, not mergedFollowing the instruction to continue, I accepted carrying the remote UAT evidence forward across the test-only commit. The evidence retains its actual tested SHA, Re-verified just now on Generated with |
530d9eb to
1eb6069
Compare
Pure refactor with no behavior change, preparing a backend models_list tool that must offer exactly what the composer picker offers. - Move parseModelString / isModelAllowedByPolicy / isGatewayModelAccessibleForUi verbatim to src/common/utils/policy/modelPolicy.ts; policyUi.ts re-exports. - Share DEFAULT_ROUTE_PRIORITY from src/common/routing so UI and backend default identically. - Move the useModelsFromSettings selector pipeline (custom models, hidden filter, authoritative catalogs, route availability, direct-route Codex OAuth gating, policy on the active route) into computeSelectableModels in src/common/utils/ai/selectableModels.ts; the hook now calls it. - Add explicit-expectation tests for the pure pipeline. The 42 existing useModelsFromSettings tests pass unchanged before and after. --- _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$unknown`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=unknown -->
…values Read-only agent tool returning the models selectable under the current configuration — the same set the composer picker offers — each with the aliases and thinking levels the task tool accepts, so agents can pass a valid model/thinking to task instead of guessing. - computeSelectableModels (shared with the picker) → listAvailableModels: normalize via normalizeModelInput, drop malformed persisted IDs, re-check changed identities against the selectable set, dedupe, attach MODEL_ABBREVIATIONS aliases and getThinkingPolicyForModel levels. - Strict input (no parameters) and strict result schemas; failure messages are fixed constants so exception text never reaches the model. - ToolConfiguration.listAvailableModels closure wired in TurnRequestBuilder, reading providerService.getConfig(), config.loadConfigOrDefault() and the enforced policy on every call (no cache). - Exposed through baseTools; the task.model description gains a hint to use models_list. Generated docs/skill snapshot regenerated via the repo scripts. - Tests: pipeline enrichment, tool result/leak/failure paths, task-input compatibility (parseTaskAiOverrides), both task kinds forwarding, exec/plan/ explore + sub-agent gating, and the production closure recomputing across provider/config/policy mutations. Checkpoint commit: the full make test-unit gate was red on unrelated pre-existing host failures at this point; not accepted as validated. _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$9.23`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=9.23 -->
Main now selects GROK_47 instead of GROK_46. Keep the explicit provider fixture aligned with that catalog entry after rebasing; no selection behavior changes. Validation: 399 targeted tests, typecheck, ESLint, and Prettier pass. --- _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$55.44`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=55.44 -->
bun test runs every suite in one process, so file-scope stubs and globals leak into later files. Four suites leaked state that only surfaces in the full `bun test src` order (each victim passes isolated): - useSmoothStreamingText.test: set window/document to undefined on teardown, leaving WorkspaceFooterBar's popup tests without a usable document (3 failures). Use the shared installDom/cleanup harness instead (same change as 6e89d52). - WorkspaceHeartbeatModal.test: open-only Dialog stub never restored; every later uncontrolled dialog rendered nothing (WorkflowLongText x3, WorkflowRunHeader x1). Snapshot the real module before mocking and restore. - MessageWindow.test: Tooltip stub with TooltipContent: () => null never restored; MemoryTab's tooltip block could never appear. Restore Tooltip and ChatHostContext after the suite. - agent_skill_delete.test: a one-shot mockRejectedValueOnce on the shared fs.rm can be consumed by any other in-flight rm from an earlier suite (several callers swallow rm errors), letting the tool's own rm succeed. Reject the legacy-manifest path specifically; assertions unchanged. Exact-order replays: 7-file footer prefix 3 fail -> 41 pass; 7-file Workflow/MemoryTab prefix 5 fail -> 58 pass; agent_skill_delete 47 pass. _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$13.36`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=13.36 --> Signed-off-by: Thomas Kosiewski <tk@coder.com>
CI tests the merge tree, where the opus alias now points to Opus 5.5. Keep enrichment assertions tied to the authoritative thinking policy and derive the gateway allowlist entry from the selected model metadata. Fixed-model fixtures retain explicit capability expectations. Reproduced both failures on CI merge 3c5df45 with Bun 1.3.5; all 270 selector, AI service and thinking-policy tests pass on both trees after the correction. No production or dependency files change. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$199.44`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=199.44 -->
Merge-queue admission failed after #4348 renamed GPT_56_LUNA to GPT_6_LUNA and moved the `gpt` alias to gpt-6-sol. Swap the renamed key in the explicit picker-order lists and derive the gateway catalog entries for the routed `gpt` built-in from KNOWN_MODELS, so the gating assertions keep testing routing instead of one historical model ID. No production files change. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high` • Cost: `$237.64`_ <!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high costs=237.64 -->
1eb6069 to
c9f7dd4
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9f7dd4f80
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
When a policy refresh moves a running turn into the blocked state, PolicyService reports enforcement with no effective policy and denies every model. The models_list closure passed that null policy to shared filtering, which reads null as "unenforced", so it advertised configured models the runtime would reject. Return an empty catalog in that state instead. Addresses Codex review thread PRRT_kwDOPxxmWM6lFBNA. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high` • Cost: `$257.67`_ <!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high costs=257.67 -->
Summary
Add
models_list, a read-only tool that returns the visible models selectable under the current configuration, with their accepted aliases and thinking levels. It shares the composer picker's selection pipeline, so agents can discover a validtask.modelinstead of guessing.The catalog is advisory. It does not probe providers or grant permission to override the model.
task.modelstill says to omit it unless the user requests a specific model, and send-time validation remains authoritative.Implementation
Validation
1eb6069124(rebased ontomain80efaa2ac5after merge-queue admission failed on 🤖 feat: add GPT-6 Sol and Luna model support #4348's GPT-6 catalog rename):make static-checkpassed. Fullmake test-unitpassed in 1,173 seconds: 20,089 source tests passed, 9 skipped, and 41 Storybook/DOM tests passed. The follow-up commit only updatesselectableModels.test.tsfixtures (renamedGPT_6_LUNAkey; gateway catalog entries derived from the movinggptalias).GIT_TEMPLATE_DIR=/usr/share/git-core/templatesto avoid host-specific Git templates. No test-runner flags, timeout increases, or skipped coverage were added.31d4e64ab(the recorded/tested SHA; not a rerun on the final head): picker/tool parity; tool availability in exec, plan, explore and sub-agents; fixture-backed launches for both task kinds; disabled-provider negative checks; and desktop/mobile rendering. Requests went to controlled loopback fixtures, not real providers.Test-gate repairs and plan corrections
The full run exposed test isolation leaks. The separate repair commit restores Dialog/Tooltip module mocks, uses the shared DOM teardown harness, and targets filesystem error injection at the intended path. Ordered reproductions for the DOM and module-mock failures went from red to green; the final full run passed.
CI then exposed two moving-model fixture assumptions after the base branch promoted the Opus alias to 5.5. Reproducing the exact CI merge tree produced both failures. The final test-only commit derives the gateway allowlist from model metadata and verifies enrichment against the authoritative thinking policy, while keeping fixed-model capability expectations explicit. All 270 selector, AI-service, and thinking-policy tests pass on both the feature tree and repaired CI merge tree. Only two test files differ from the UAT-tested commit; all other tracked files, including product code and dependencies, are identical. Following the user's instruction to continue (standing delegation), the existing UAT evidence was accepted as carried forward to this test-only commit on the basis of the verified two-test-file delta and green final-head validation. The evidence retains its actual tested SHA (
31d4e64ab); no extra recording was made.The retained Storybook budget had fallen behind the current inventory. Its separate repair sets zero-headroom limits at 115 enabled files and 605 estimated snapshots, without removing coverage. An audited inventory and disposable extra-file/extra-export mutations verify both guards still fail on growth. These are estimates, not executed capture counts.
Three plan assumptions were corrected against existing behavior: the hook-doc generator omits zero-input tools, so only the
task.modelhint changes; custom IDs are trimmed before selection; internal colons are valid model-ID characters. The generator and task normalization behavior are unchanged.Risks
The shared selection code is on the composer hot path. The extraction preserves its behavior and has explicit parity coverage. Backend discovery reads persisted hidden-model preferences, so it can briefly differ from the UI immediately after a preference change. The catalog is not a promise that a provider will accept a request.
Mobile UAT evidence
Continuous mobile recording: expand the card and scroll both model entries.
36306eae-mobile-expanded-r4.webm
📋 Implementation Plan
Plan:
models_listtool — let agents discover validmodelvalues fortaskGoal
Add a read-only agent tool,
models_list, that returns the models selectable under the user's current configuration (the same set the chat composer's model picker offers), each with the aliases and thinking levels thetasktool accepts. Agents can then pass a validmodel/thinkingtotask(bothkind: "subagent"andkind: "workspace") instead of guessing. The list is computed by one shared function used by both the UI picker and the tool, so the two cannot drift.The catalog is advisory: it reflects configuration (credentials present, routing, policy, user-hidden models), not a guarantee that a provider will accept a request. Send-time validation in
providerModelFactorystays authoritative. It is also a visible catalog: user-hidden models are omitted even thoughtask.modelwould still accept them.Evidence (verified in repo)
src/browser/hooks/useModelsFromSettings.ts(useMemo~L347–418): dedupe(custom models of enabled providers,KNOWN_MODELS) → minushiddenModels→ authoritative-catalog + route availability (isModelAvailable) → OpenAI Codex-OAuth gating on the direct route → policy on the active route. The composer picker (src/browser/features/ChatInput/index.tsx:2941–2959→ModelSelector) rendersuseModelsFromSettings().modelsas-is (no sorting, no "recent" entries); Settings → Models shows inventory tables, not this list. Parity reference =useModelsFromSettings().models.task.modelis validated syntactically only, for both kinds:parseTaskAiOverrides(src/node/services/tools/task.ts:120–147, called at L419 before thekindbranch) →normalizeModelInput(src/common/utils/ai/normalizeModelInput.ts) →MODEL_ABBREVIATIONSalias table (src/common/constants/knownModels.ts:307) →normalizeSelectedModel(preserves explicit gateway prefixes) →isValidModelFormat. A well-formed but unavailable model passes creation and fails at first send (api_key_not_found,model_not_available,policy_denied, …), surfacing as a failedtaskcall or aninterruptedtask.src/common:resolveRoute/isModelAvailable(src/common/routing/resolve.ts:280,305),isProviderModelAccessibleFromAuthoritativeCatalog/isGatewayModelAccessibleFromAuthoritativeCatalog(src/common/utils/providers/gatewayModelCatalog.ts),isCodexOauthAllowedModel/isCodexOauthRequiredModel(src/common/constants/codexOAuth.ts),getThinkingPolicyForModel(model, providersConfig?: ProvidersConfigMap | null): readonly ThinkingLevel[](src/common/utils/thinking/policy.ts:108, honorsmappedToModel),normalizeModelInput,KNOWN_MODELS,DEFAULT_HIDDEN_MODELS,MODEL_ABBREVIATIONS.parseModelString,isModelAllowedByPolicy,isGatewayModelAccessibleForUiinsrc/browser/utils/policyUi.ts(imports are all@/common; 10 importers incl.src/node/services/providerService.test.ts), plus private helpers in the hook (getCustomModels,getSuggestedModels,filterHiddenModels,dedupeKeepFirst,resolvesToDirectOpenAI,isModelAllowedByPolicyOnActiveRoute; none imported elsewhere).providersConfig=api.providers.getConfig()→providerService.getConfig()(ProvidersConfigMap);routePriority/routeOverrides=config.getConfig()with UI defaults["direct"]/{}(src/browser/hooks/useRouting.ts:19–21,82);hiddenModels= persisted browser state seeded from backend model prefs on mount (src/browser/contexts/WorkspaceContext.tsx:683–705), defaultDEFAULT_HIDDEN_MODELS;effectivePolicy=usePolicy()when enforced.TurnRequestBuilderDependencies(src/node/services/turnRequestBuilder.ts:585–600) hasconfig: Config,providerService: ProviderService,policyService?: PolicyService.Config.loadConfigOrDefault()(src/node/config/index.ts:1456) returnsProjectsConfig(src/common/types/project.ts:79; fieldshiddenModels?,routePriority?,routeOverrides?) from a warm in-memory snapshot; it is the accessor every backend read uses (there is no separategetConfig()), and its one-time migration persistence is pre-existing behavior shared by all reads — the tool adds no write path.TurnRequestBuilderbuildsToolConfigurationat L2247 and callsgetToolsForModelat L2518. CLIxum run, ACP, workflows, and the PTC bridge all flow through it (src/cli/run.ts:651–684,src/node/acp/agent.ts:121,src/node/services/workflowContinuation.ts:28,src/node/services/ptc/toolBridge.ts:43receives already-assembled tools). The only otherToolConfigurationconstruction (src/node/services/refinement/refineService.ts:1476–1488) builds skill-write tools only and never callsgetToolsForModel.TOOL_DEFINITIONS(src/common/utils/tools/toolDefinitions.ts, optionalresultSchema), arg/result types insrc/common/types/tools.ts, implementationsrc/node/services/tools/<name>.ts(ToolFactory = (config: ToolConfiguration) => Tool), registration innonRuntimeTools(src/common/utils/tools/tools.ts~L871), gating ingetAvailableTools()baseTools(~L3770). Built-in agentsexec.md/plan.md/explore.mdusetools.add: [".*"]with explicit removes, so a new tool reaches exec/plan/explore and sub-agents without agent-definition edits (SUBAGENT_HARD_DENYonly deniesask_user_question; depth denial coverstask*only). UI falls back toGenericToolCall; icon viaTOOL_NAME_TO_ICON(src/browser/features/Tools/Shared/ToolPrimitives.tsx:244).docs/hooks/tools.mdxis regenerated fromTOOL_DEFINITIONSbybun scripts/gen_docs.ts(make fmt). No test asserts a total tool count or schema token budget.agent_skill_list(src/node/services/tools/agent_skill_list.ts:158):{ success: true, … } | { success: false, error },.strict()schema,.nullish()optionals.src/common/utils/tools/toolDefinitions.test.ts:683–753(getAvailableTools),src/common/utils/tools/tools.test.ts(getToolsForModel),src/node/services/toolAssembly.test.ts(applyToolPolicyAndExperiments+resolveToolPolicyForAgentfor built-in agents).Design decisions
models_list(plain snake_case liketask_list,agent_skill_list;mux_prefix is reserved for config/agent-store tools).z.object({}).strict()— no parameters, noincludeHiddenoption.ModelsListToolResultSchema, also attached asresultSchema). The domain type lives in the shared module; the schema only mirrors it (type-only dependency, no cycle):getThinkingPolicyForModelreturns a readonly array → spread into a mutable array when building the entry. Nothing else (no display names, context windows, pricing,defaultModel, credentials, provider config).computeSelectableModelsreproduces the hook pipeline exactly (including theprovidersConfig == null"still loading → skip availability filters" branch, used only by the UI) and returns raw picker strings. The enrichment step then, for each raw entryrawwithselectable = new Set(rawList):n = normalizeModelInput(raw).model; ifnull(e.g. a persisted custom ID with a second colon) → omit (persisted custom IDs are input data, not programmer invariants);n !== raw(normalization changed the identity —normalizeModelInputtrims and canonicalizes; explicit gateway selections such asopenrouter:…are preserved bynormalizeSelectedModel, so in practice this is e.g. a persisted custom entry with surrounding whitespace) → emitnonly ifselectable.has(n), i.e. the emitted identity itself passed routing, credential, catalog, policy and hidden-model checks; otherwise omit rather than advertise an unchecked replacement;aliases=MODEL_ABBREVIATIONSkeys withnormalizeModelInput(alias).model === n.Omissions are reported through an optional
onSkipped?: (raw: string, reason: "malformed" | "unchecked_identity") => voidcallback so the Node boundary canlog.debug; the shared module itself imports no logger.computeSelectableModels; the hook's own code shrinks to input plumbing.ToolConfiguration,listAvailableModels?: () => AvailableModel[], built inTurnRequestBuilderat the existing construction site. The closure readsproviderService.getConfig(),config.loadConfigOrDefault(), and the effective policy (policyService?.isEnforced() ? policyService.getEffectivePolicy() : null) at each invocation — no cache, no new service class, noPolicyServicepassed into shared code. Absent closure (test contexts,refineService) →{ success: false, error: "Model catalog unavailable in this context" }(mirrorstaskwithouttaskService). A throwing closure → fixed message{ success: false, error: "Failed to compute the model catalog" }with the exception logged vialog.erroron the Node side — exception text (which could echo configuration) never reaches the model.ptcExcluded; generic CLI formatter and UI renderer; one icon line.models_listmay be called whenever the agent needs to know which models exist (e.g. the user asks "which models are available?"). The override restriction stays where it is: thetask.modeldescription keeps its "omit unless the user explicitly instructed a specific model" sentence verbatim and gains only the hint "Usemodels_listto see valid values."Delivery: one change set, two gated phases
Work lands on this branch as two commits (Phase A, Phase B). Publication (PR,
gh stacksplit) only when requested; the phases are independently reviewable if a split is wanted later.Phase A — pure refactor, zero behavior change: shared selectable-models pipeline
src/common/utils/policy/modelPolicy.tswithparseModelString,isModelAllowedByPolicy,isGatewayModelAccessibleForUimoved verbatim fromsrc/browser/utils/policyUi.ts;policyUi.tsre-exports them so its 10 importers andpolicyUi.test.tsare untouched (getAllowedProvidersForUistays in the browser file).→ verify:
make typecheck;bun test src/browser/utils/policyUi.test.ts.DEFAULT_ROUTE_PRIORITY(["direct"]) fromuseRouting.tstosrc/common/routing/resolve.ts(reuse an existing direct-route constant there if one exists) so UI and backend default identically;useRouting.tsimports it.src/common/utils/ai/selectableModels.ts:BUILT_IN_MODELS,getCustomModels,getSuggestedModels,filterHiddenModels,dedupeKeepFirst,resolvesToDirectOpenAI,isModelAllowedByPolicyOnActiveRoute, and theisConfigured/isGatewayModelAccessible/isAuthoritativeProviderModelAccessiblepredicates from the hook into this file without logic changes (predicates become plain closures overprovidersConfig/effectivePolicy). ExportgetSuggestedModels/filterHiddenModelsonly if the hook still needs them (getAllCustomModels/providerHiddenModelsbucket stays in the hook).useModelsFromSettings: replace theuseMemobody withcomputeSelectableModels({ providersConfig: config, hiddenModels, effectivePolicy, routePriority, routeOverrides }); keepproviderHiddenModels/ensureModelInSettingslogic in place, reusing the moved predicates where it needs them.rg '@/browser' src/commonstays empty (lintno-restricted-importsboundary also enforces this).Gate A (must pass before Phase B):
make typecheck && make lint && bun test src/browser/hooks/useModelsFromSettings.test.ts src/browser/utils src/common/utils/ai src/common/routing— the 40 existing hook tests run before (baseline = this branch's pre-change HEAD9a6a2e2bf, recorded before the first edit) and after the extraction with identical results.Tests added in Phase A —
src/common/utils/ai/selectableModels.test.ts, each with an explicit expected list (never "compare two calls"):mux-gatewayandgithub-copilotentries skipped; a custom entry equal to a built-in ID deduped (custom first); object entries ({ id, mappedToModel }) surface byid.hiddenModelsexcluded; empty list is a no-op.routeOverridespinning a model to an unavailable gateway falls back perresolveRoute(assert the actualisModelAvailableoutcome); explicit gateway-prefixed IDs (coder:openai/gpt-…) survive.discoveredModelspresent/absent andremovedModelsexclusion; github-copilot catalog gating of a built-in model.effectivePolicy: nullskips filtering.providersConfig: null: returns the hidden-filtered suggested list (loading semantics), still policy-filtered when a policy is present.(Move equivalent cases out of
useModelsFromSettings.test.tsonly when they test pure pipeline logic; leave cases that exercise React state, persistence, orensureModelInSettings.)Phase B — the tool
src/common/utils/tools/toolDefinitions.tsAvailableModelSchema,ModelsListToolResultSchema(Design §3) near the other result schemas (importThinkingLevelSchemafromsrc/common/types/thinking.ts;import type { AvailableModel }from the shared module).models_list: { description, schema: z.object({}).strict(), resultSchema: ModelsListToolResultSchema }. Description: "List models selectable under the current configuration, with aliases and thinking levels. Hidden models are omitted. This is a configuration snapshot, not a provider availability probe. Use returned IDs when a model override is requested; otherwise leavetask.modelunset.""models_list"tobaseToolsingetAvailableTools().models_listto see valid values." to thetaskmodeldescription.src/common/types/tools.ts:ModelsListToolArgs,ModelsListToolResult(z.infer); re-exporttype AvailableModelfrom the shared module for tool consumers.src/common/utils/ai/selectableModels.ts: add the pure enrichment step (Design §4)computeSelectableModels→ normalize / recheck / dedupe per §4 →aliases→thinkingLevels = [...getThinkingPolicyForModel(model, providersConfig)].assert(thinkingLevels.length > 0)(the policy helper always returns a non-empty fallback; this documents the assumption). No logger import; no runtime import of tool-definition modules.src/common/utils/tools/tools.ts: addlistAvailableModels?: () => AvailableModel[]toToolConfigurationwith a doc comment (import type); registermodels_list: createModelsListTool(config)innonRuntimeTools.src/node/services/turnRequestBuilder.ts(~L2247): wire the closure:src/node/services/tools/models_list.ts(new):createModelsListTool: ToolFactory—config.listAvailableModels == null→{ success: false, error: "Model catalog unavailable in this context" }; else call it inside try/catch; on throwlog.error(...)and return the fixed{ success: false, error: "Failed to compute the model catalog" }; otherwise{ success: true, models }(an initialized configuration with nothing selectable yieldsmodels: [], stillsuccess: true).src/browser/features/Tools/Shared/ToolPrimitives.tsx:models_list: <lucide icon already imported there, e.g. Cpu/Boxes>inTOOL_NAME_TO_ICON.make fmt→ commit the regenerateddocs/hooks/tools.mdxrow.Tests added in Phase B:
src/common/utils/ai/selectableModels.test.ts(extend):listAvailableModelswith a fixtureProvidersConfigMap(anthropic configured, openai unconfigured, openrouter configured with custom entryanthropic/claude-sonnet-5, customfixtureprovider with["fixture-echo", "fixture-echo " (trailing space), { id: "fixture-mapped", mappedToModel: "anthropic:claude-opus-4-7" }, "bad:id:colon", "lonely "], one hidden built-in) → explicit expected entries: aliases only on the built-in they normalize to (sonnet→anthropic:claude-sonnet-5),[]on custom models; malformedfixture:bad:id:colonomitted (onSkippedcalled with"malformed") while the rest survive; changed-identity regression (an input demonstrably changed bynormalizeModelInput):fixture:fixture-echonormalizes tofixture:fixture-echo, which is independently selectable → one deduped entry;fixture:lonelynormalizes tofixture:lonely, which is not in the selectable set → omitted with"unchecked_identity"; explicit gateway selection preserved:openrouter:anthropic/claude-sonnet-5is emitted unchanged (not collapsed into the canonical ID) — assert the actualnormalizeModelInputoutput rather than assuming;fixture-mappedgets the mapped target's thinking policy; a model whose policy excludesoffvs one on the default policy; every entry passesAvailableModelSchema.strict().parse. Do not change task normalization to make a fixture pass; adjust expectations to observed behavior.src/node/services/tools/models_list.test.ts: (a) no closure →{ success: false, error: "Model catalog unavailable in this context" }; (b) closure result returned andModelsListToolResultSchema.parsesucceeds; (c) closure throws an error whose message contains a fixture secret → result is the fixed failure message and does not contain the secret; (d) closure returns[]→{ success: true, models: [] }; (e) task-input compatibility: for every returned entry,parseTaskAiOverrides({ model, thinking })acceptsmodel, eachalias, and eachthinkingLevel; (f) serialized success result contains none of the fixture's secret strings (apiKey,baseUrl, header values).src/node/services/turnRequestBuildercoverage (pattern:aiService.test.tsgetToolsForModelSpy/workspaceService.multiProject.test.tscapturedToolConfig): capture the realToolConfiguration, assertlistAvailableModelsis present, call it with fakeproviderService.getConfig(),config.loadConfigOrDefault(), andpolicyServicewhose return values are mutated between invocations (provider configured→disabled,routePrioritygateway added, a model added tohiddenModels, policy enforced→denying) and assert each subsequent call returns the explicit expected list — the tool is instantiated once, the closure recomputes.src/node/services/tools/task.test.ts(existingcreateTaskToolharness with stubbedtaskService/workspaceTurnManager): call the publictaskhandler with amodels_list-shaped entry (model= canonical ID, then its alias) and a listedthinkinglevel forkind: "subagent"andkind: "workspace"(mode: "new"), asserting the stubs receive the normalizedmodelStringand resolvedthinkingLevel. No new full-stack harness.src/node/services/toolAssembly.test.ts(existingresolveToolPolicyForAgentpattern) assertmodels_listsurvives the exec, plan, and explore policies and the sub-agent hard-deny; extendtoolDefinitions.test.ts:683–753if it enumerates exposed base tools.Gate B: new tests green;
make static-check; fullmake test-unit(the repo's required gate — no touched-suite substitute; if the known Bun 1.3.5 Wasm SIGSEGV appears, rerun with the documented invocation-scopedBUN_JSC_useWasmIPInt=0and report it); then the hands-on dogfood below.Files touched (product code)
src/common/utils/policy/modelPolicy.ts(new) +src/browser/utils/policyUi.tssrc/common/routing/resolve.ts+src/browser/hooks/useRouting.tsDEFAULT_ROUTE_PRIORITYsrc/common/utils/ai/selectableModels.ts(new)computeSelectableModels(moved, not counted) +AvailableModel+listAvailableModelswith normalize/recheck/dedupe/onSkipped (new)src/browser/hooks/useModelsFromSettings.tssrc/common/utils/tools/toolDefinitions.tssrc/common/types/tools.tssrc/common/utils/tools/tools.tsToolConfiguration.listAvailableModels, registrationsrc/node/services/turnRequestBuilder.tssrc/node/services/tools/models_list.ts(new)src/browser/features/Tools/Shared/ToolPrimitives.tsxdocs/hooks/tools.mdxEstimated net new product LoC: ≈ +170 (range 150–200), excluding moved code, tests (≈ +350), and generated docs.
Acceptance criteria
models_listis exposed through the real assembly path for exec, plan, explore, and sub-agents (test + dogfood), and returns{ success: true, models }withmodel,aliases,thinkingLevelsper entry;{ success: false, error }where no catalog closure exists.modelset equals the valid, normalized, selectable entries of the settled composer picker (computeSelectableModelsis the shared implementation; malformed picker entries and changed identities that are not themselves selectable are the only omissions; dogfood compares the rendered picker with the tool output).model, alias, and thinking level is accepted byparseTaskAiOverrides(unit test), and launchingtaskwith a returned model succeeds against controlled loopback fixtures for bothkind: "subagent"andkind: "workspace"(dogfood). Real-provider acceptance is not claimed.useModelsFromSettings.test.tsresults are identical before/after Phase A;make static-checkand fullmake test-unitpass on the final commit;docs/hooks/tools.mdxgained exactly themodels_listrow.Dogfooding (deterministic, loopback fixtures by default)
No real provider key is used unless the user explicitly authorizes it. Everything runs against an owned temp
XUM_ROOTand a loopback OpenAI-compatible SSE fixture; every request must reach only that fixture.dev-server-sandboxskill. Start the dev server under an allowlisted environment (env -i PATH=… HOME=… XUM_ROOT=<temp> …plus only the variables the sandbox script needs) so no inheritedANTHROPIC_*/OPENAI_*/Coder gateway credential can make a real provider "configured". Seedproviders.jsoncwith two customopenai-compatibleproviders:fixture(baseUrl= loopback fixture,apiKey: "fixture",models: ["fixture-driver", "fixture-echo", "fixture-hidden"]) andfixture-off(samebaseUrl,isEnabled: false,models: ["never"]);config.jsonhiddenModels: ["fixture:fixture-hidden"], default modelfixture:fixture-driver. Before sending anything, verify the effective snapshot withxum api providers get-config(onlyfixtureisisConfigured && isEnabled; no built-in provider configured) andxum api config get-config(routePriority["direct"]). Keep fixture scripts, logs, and artifacts outside the checkout.models_list {}; latest = tool result ofmodels_list→ text turn echoing the result JSON, then stop.task { agentId: "explore", title: "Echo", prompt: "reply ok", model: "fixture:fixture-echo", thinking: "low" }(second variant:kind: "workspace", workspace: { mode: "new" }); latest = tool result oftask→ text summary, stop.fixture-echo, latest user message contains "reply ok") → textok(sub-agents auto-report after the first turn; the workspace turn completes on the text).task { …, model: "fixture-off:never" }; latest = tool result (error) → text summary, stop.unexpected request, logged with the request body, so a loop is visible immediately.The fixture logs every request (
model, latest message kind) to a file that is checked at the end: only expected shapes, no repeats.agent-browserunder an owned--session <name>(neverclose --all);agent-browser record start(ffmpeg present) before the steps below.agent-browser snapshot -ito capture the rendered entries; screenshot →picker.png. Expected:fixture:fixture-driver,fixture:fixture-echo; no built-ins, nofixture-offmodel, no hidden model.models_list.png. Expected set equals step 4;aliases: [];thinkingLevels= default policy.ok, reports →task_subagent.png. Send the workspace-kind spawn prompt → new workspace turn completes withok→task_workspace.png. Send the negative prompt →taskfails at launch withprovider_disabledforfixture-off:never→task_negative.png(a model the catalog correctly omitted; no real provider is involved).models_list_mobile.png; assert viaagent-browser evalthatdocument.documentElement.scrollWidth <= window.innerWidth(long IDs wrap/truncate, no right-edge overflow).dogfood.webm; inspect frames withffmpeg -vf fps=1to confirm steps 4–7 are visible; confirm from the fixture log that every request hit the fixture and matched an expected shape;attach_filethe PNGs and video in the report.agent-browser close --session <name>, confirm no owned PIDs/ports remain, delete the tempXUM_ROOTonly if it holds nothing needed for the report.Risks and accepted trade-offs
config.json; backend readsconfig.json. Both are kept in sync byupdateModelPreferences; a transient mismatch right after hide/unhide is acceptable.task.modelstill accepts any well-formed string; validatingtask.modelagainst the catalog is out of scope.loadConfigOrDefault()may persist one-time migrations, but that is existing behavior of every backend read; the tool introduces no new writes.taskwould rewrite the raw form anyway and such entries indicate a configuration typo.Out of scope / follow-ups
defaultModel, display names, context windows, or cost data in the output; a dedicated tool card or CLI formatter.task.modelvalues at task creation.xum api models list).Generated with
xum• Model:coder:openai/gpt-6-astra• Thinking:high• Cost:$171.28