From 222cb0b84e3e73059fa6f69bd3320d16436a1de5 Mon Sep 17 00:00:00 2001 From: m4 Date: Thu, 23 Jul 2026 10:19:03 +0800 Subject: [PATCH] feat: drop auxiliary model from thread selection and registry defaults ThreadModelSelection is now "inherit" or { primary, reasoning_effort }; the BFF validator tolerates and drops legacy auxiliary keys, the runs route no longer snapshots an auxiliary ref, and the Registry Editor's default-auxiliary field is removed, matching the backend's single-role snapshot resolution. Co-Authored-By: Claude Opus 4.7 --- .env.example | 4 + .../conversations/[threadId]/runs/route.ts | 6 +- src/app/components/ChatInterface.tsx | 130 +++++++++++++++++- src/app/components/RegistryEditor.tsx | 27 ---- src/app/hooks/useAvailableModels.ts | 47 +++++-- src/app/hooks/useChat.ts | 35 +++-- src/lib/modelRegistry.ts | 10 +- src/lib/registryDraft.test.ts | 2 +- src/lib/server/activeDeployment.ts | 3 +- src/lib/server/threadModelSelection.test.ts | 51 +++++++ src/lib/server/threadModelSelection.ts | 31 +++-- 11 files changed, 277 insertions(+), 69 deletions(-) create mode 100644 src/lib/server/threadModelSelection.test.ts diff --git a/.env.example b/.env.example index 73d9f1e..f51f3f2 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,10 @@ EVOSCIENTIST_BACKEND_URL=http://127.0.0.1:6174 # Deployment workspace root. This is not a browser-selectable directory and is # shared with the trusted EvoScientist backend. Defaults to ~/.evoscientist/workspace. EVOSCIENTIST_WORKSPACE_DIR= +# Deployment id, matching the backend's EVOSCIENTIST_DEPLOYMENT_ID. Only needed +# when the backend was started manually (no deploy sidecar); otherwise the +# sidecar value wins. Defaults to a uuid derived from the workspace root. +EVOSCIENTIST_DEPLOYMENT_ID= # Optional for local processes running as the same OS user: both sides share # ~/.config/evoscientist/provider-admin-token automatically. Set this explicitly # when the WebUI and backend run on different hosts, users, or containers. diff --git a/src/app/api/conversations/[threadId]/runs/route.ts b/src/app/api/conversations/[threadId]/runs/route.ts index 2718fb6..bd76f3e 100644 --- a/src/app/api/conversations/[threadId]/runs/route.ts +++ b/src/app/api/conversations/[threadId]/runs/route.ts @@ -224,8 +224,10 @@ export async function POST(request: NextRequest, context: RouteContext) { model_selection_revision: selection.revision, primary: selectionRef === "inherit" ? null : selectionRef.primary, - auxiliary: - selectionRef === "inherit" ? null : selectionRef.auxiliary, + reasoning_effort: + selectionRef === "inherit" + ? null + : (selectionRef.reasoning_effort ?? null), }, } ); diff --git a/src/app/components/ChatInterface.tsx b/src/app/components/ChatInterface.tsx index fd0039a..1b03767 100644 --- a/src/app/components/ChatInterface.tsx +++ b/src/app/components/ChatInterface.tsx @@ -101,7 +101,7 @@ import { WorkspaceFileDialog } from "@/app/components/WorkspaceFileDialog"; import { MemoryFileDialog } from "@/app/components/MemoryFileDialog"; import { FILE_LINK_EVENT, type FileLinkEventDetail } from "@/lib/fileLink"; import { notifyWorkspaceChanged } from "@/lib/workspaceEvents"; -import type { ModelRef } from "@/lib/modelRegistry"; +import type { ModelRef, ReasoningEffort } from "@/lib/modelRegistry"; import { modelRefLabel, useAvailableModels, @@ -446,6 +446,7 @@ export const ChatInterface = React.memo( }, [modelPickerOpen]); const { models: selectableModels, + defaults: registryDefaults, loading: modelRegistryLoading, error: modelRegistryError, } = useAvailableModels(); @@ -709,7 +710,8 @@ export const ChatInterface = React.memo( // 1. Explicit per-thread selection (ModelRef picker) — the next run // freezes exactly this into its runtime snapshot. // 2. `inherit` — the registry default resolved server-side at snapshot - // creation; the pill shows the last observed model as a hint. + // creation; the pill shows the last observed model as a hint, or the + // registry default when no turn has run yet. // Token/context usage is intentionally NOT shown — the backend doesn't // persist usage_metadata, so it isn't reliably available here. const currentModel = useMemo(() => { @@ -734,8 +736,22 @@ export const ChatInterface = React.memo( ); if (info) return info; } + // Fresh thread on `inherit`: show the registry default so the picker + // entry point exists before the first turn (the run would use it). + const fallbackRef = registryDefaults?.primary ?? null; + if (fallbackRef) { + const match = selectableModels.find( + (m) => + m.model_ref.provider_id === fallbackRef.provider_id && + m.model_ref.model_key === fallbackRef.model_key + ); + return { + name: match?.name ?? fallbackRef.model_key, + provider: match?.provider_name || fallbackRef.provider_id, + }; + } return null; - }, [messages, modelSelection, selectableModels]); + }, [messages, modelSelection, selectableModels, registryDefaults]); // Bind captured sub-agent activity (keyed by subgraph namespace) to each task // tool call → its live steps. B': match a finished sub-agent to a task by its @@ -1626,7 +1642,6 @@ export const ChatInterface = React.memo( try { await setModelSelection({ primary: ref, - auxiliary: null, }); toast.success(`Model set to ${modelKey}.`); setModelPickerOpen(false); @@ -2215,6 +2230,36 @@ export const ChatInterface = React.memo( )} )} + {currentModel && ( + { + if (modelSelection === "inherit") return; + try { + await setModelSelection({ + primary: modelSelection.primary, + reasoning_effort: effort, + }); + toast.success( + effort === null + ? "Reasoning effort back to the registry default." + : `Reasoning effort set to ${effort}.` + ); + } catch (err) { + toast.error( + err instanceof Error + ? `Couldn't update reasoning effort: ${err.message}` + : "Couldn't update reasoning effort — try again." + ); + } + }} + /> + )} {threadId && (