diff --git a/lat.md/model-selection.md b/lat.md/model-selection.md index 8a08d8867..6c677ee94 100644 --- a/lat.md/model-selection.md +++ b/lat.md/model-selection.md @@ -4,6 +4,8 @@ The in-chat (bottom) model picker selects a model for the **current conversation The override is held in renderer state on each `` run ([[src/renderer/src/screens/Chat/Chat.tsx]]), persisted by session id, and sent with every message; it is cleared when the conversation is cleared/reset and is absent on a fresh chat, so new conversations start on the global default. This is distinct from the persisted [[model-context]] default that non-chat surfaces read. +Pre-send readiness uses the same active override. [[src/renderer/src/screens/Chat/Chat.tsx#Chat]] passes the current picker `{provider, model, baseUrl}` into [[src/main/validation.ts#validateChatReadiness]], so a session-scoped pick does not keep showing "No model selected" just because the global `config.yaml` default is empty. + ## Full identity, not just the model name The override is a `SessionModelOverride` (`{provider, model, baseUrl}`), not a bare model string — because switching across providers must change routing, not only the `model` field. diff --git a/src/main/ipc/register.ts b/src/main/ipc/register.ts index 23e5eafdd..36aeb2a35 100644 --- a/src/main/ipc/register.ts +++ b/src/main/ipc/register.ts @@ -850,9 +850,13 @@ export function registerIpcHandlers(context: IpcContext): void { // Pre-send chat readiness — answers "if Send is clicked right now, // will it work?". Fail-open semantics: any uncertain state returns // `ok: true`, so the renderer never false-blocks a Send. - ipcMain.handle("validate-chat-readiness", (_event, profile?: string) => { - return validateChatReadiness(profile); - }); + type ChatReadinessOverride = Parameters[1]; + ipcMain.handle( + "validate-chat-readiness", + (_event, profile?: string, override?: ChatReadinessOverride) => { + return validateChatReadiness(profile, override); + }, + ); // Config-health audit + per-issue auto-fix. The renderer renders a // dismissible banner above the chat input and a full report in the diff --git a/src/main/validation.ts b/src/main/validation.ts index 14857adb1..fcde83d99 100644 --- a/src/main/validation.ts +++ b/src/main/validation.ts @@ -45,6 +45,12 @@ export interface ChatReadiness { expectedEnvKey?: string; } +export interface ChatReadinessModelConfig { + provider?: string; + model?: string; + baseUrl?: string; +} + const OK: ChatReadiness = { ok: true }; // Provider ids that authenticate ONLY via interactive OAuth login — @@ -79,14 +85,20 @@ const NO_KEY_PROVIDERS = new Set(["auto"]); * Synchronous readiness check against the desktop's own config — * no network calls. Fast (single readEnv + getModelConfig). * - * `profile` defaults to the active profile. + * `profile` defaults to the active profile. `override` is the chat-local model + * picker selection; when present it is the model that will actually be sent. */ -export function validateChatReadiness(profile?: string): ChatReadiness { +export function validateChatReadiness( + profile?: string, + override?: ChatReadinessModelConfig, +): ChatReadiness { try { const mc = getModelConfig(profile); - const provider = (mc.provider || "").trim().toLowerCase(); - const model = (mc.model || "").trim(); - const baseUrl = (mc.baseUrl || "").trim(); + const provider = (override?.provider ?? mc.provider ?? "") + .trim() + .toLowerCase(); + const model = (override?.model ?? mc.model ?? "").trim(); + const baseUrl = (override?.baseUrl ?? mc.baseUrl ?? "").trim(); // Provider="auto" lets hermes-agent pick a model at runtime based // on whatever keys are present in .env. No key-presence check diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 5dfcdf97f..a48436646 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -283,7 +283,10 @@ interface HermesAPI { // Configuration (profile-aware) getEnv: (profile?: string) => Promise>; setEnv: (key: string, value: string, profile?: string) => Promise; - validateChatReadiness: (profile?: string) => Promise<{ + validateChatReadiness: ( + profile?: string, + override?: { provider?: string; model?: string; baseUrl?: string }, + ) => Promise<{ ok: boolean; code?: | "NO_ACTIVE_MODEL" diff --git a/src/preload/index.ts b/src/preload/index.ts index ade4550fe..20188acae 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -234,6 +234,7 @@ const hermesAPI = { validateChatReadiness: ( profile?: string, + override?: { provider?: string; model?: string; baseUrl?: string }, ): Promise<{ ok: boolean; code?: @@ -245,7 +246,7 @@ const hermesAPI = { message?: string; fixLocation?: "providers" | "models" | "gateway" | "setup"; expectedEnvKey?: string; - }> => ipcRenderer.invoke("validate-chat-readiness", profile), + }> => ipcRenderer.invoke("validate-chat-readiness", profile, override), getConfigHealth: (profile?: string): Promise => ipcRenderer.invoke("get-config-health", profile), diff --git a/src/renderer/src/screens/Chat/Chat.tsx b/src/renderer/src/screens/Chat/Chat.tsx index a0212957e..b59f42d04 100644 --- a/src/renderer/src/screens/Chat/Chat.tsx +++ b/src/renderer/src/screens/Chat/Chat.tsx @@ -365,7 +365,11 @@ function Chat({ let cancelled = false; (async (): Promise => { try { - const r = await window.hermesAPI.validateChatReadiness(profile); + const r = await window.hermesAPI.validateChatReadiness(profile, { + provider: chatCurrentProvider, + model: chatCurrentModel, + baseUrl: chatCurrentBaseUrl, + }); if (!cancelled) setReadiness(r); } catch { // Fail open on IPC error — never block Send on validation failure diff --git a/tests/validation.test.ts b/tests/validation.test.ts index b83e878a7..f5880ac26 100644 --- a/tests/validation.test.ts +++ b/tests/validation.test.ts @@ -84,6 +84,28 @@ describe("validateChatReadiness", () => { expect(validateChatReadiness()).toEqual({ ok: true }); }); + it("uses the chat picker model override instead of blocking on an empty persisted model", async () => { + writeConfig( + [ + "model:", + " provider: alibaba", + " default: ''", + " base_url: https://dashscope.aliyuncs.com/compatible-mode/v1", + "", + ].join("\n"), + ); + writeEnv("DASHSCOPE_API_KEY=sk-dashscope-test\n"); + const { validateChatReadiness } = await freshValidation(TEST_DIR); + + expect( + validateChatReadiness(undefined, { + provider: "alibaba", + model: "qwen3.7-plus", + baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", + }), + ).toEqual({ ok: true }); + }); + it("treats whitespace-only key value as missing", async () => { writeConfig( [