diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index 9fc62ba8e2c..f657764f313 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -285,104 +285,6 @@ describe("model-selection", () => { ref: { provider: "anthropic", model: "claude-sonnet-4-6" }, }); }); - - it("rejects non-alias direct model ids when strict mode is enabled", () => { - const cfg: OpenClawConfig = { - agents: { - defaults: { - model: { primary: "openai/gpt-5.2" }, - models: { - "anthropic/claude-sonnet-4-6": { alias: "sonnet" }, - }, - }, - }, - } as OpenClawConfig; - - const catalog = [ - { provider: "anthropic", id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, - { provider: "openai", id: "gpt-5.2", name: "gpt-5.2" }, - ]; - - const result = resolveAllowedModelRef({ - cfg, - catalog, - raw: "claude-sonnet-4-6", - defaultProvider: "openai", - defaultModel: "gpt-5.2", - requireProviderOrAlias: true, - }); - - expect(result).toEqual({ - error: "invalid model: claude-sonnet-4-6 (use provider/model or alias)", - }); - }); - - it("accepts aliases when strict mode is enabled", () => { - const cfg: OpenClawConfig = { - agents: { - defaults: { - model: { primary: "openai/gpt-5.2" }, - models: { - "anthropic/claude-sonnet-4-6": { alias: "sonnet" }, - }, - }, - }, - } as OpenClawConfig; - - const catalog = [ - { provider: "anthropic", id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, - { provider: "openai", id: "gpt-5.2", name: "gpt-5.2" }, - ]; - - const result = resolveAllowedModelRef({ - cfg, - catalog, - raw: "sonnet", - defaultProvider: "openai", - defaultModel: "gpt-5.2", - requireProviderOrAlias: true, - }); - - expect(result).toEqual({ - key: "anthropic/claude-sonnet-4-6", - ref: { provider: "anthropic", model: "claude-sonnet-4-6" }, - }); - }); - - it("accepts provider-prefixed refs whose model id contains slashes in strict mode", () => { - const cfg: OpenClawConfig = { - agents: { - defaults: { - model: { primary: "openai/gpt-5.2" }, - models: { - "vercel-ai-gateway/anthropic/claude-opus-4.6": { alias: "gateway-opus" }, - }, - }, - }, - } as OpenClawConfig; - - const catalog = [ - { - provider: "vercel-ai-gateway", - id: "anthropic/claude-opus-4.6", - name: "Gateway Claude Opus 4.6", - }, - ]; - - const result = resolveAllowedModelRef({ - cfg, - catalog, - raw: "vercel-ai-gateway/anthropic/claude-opus-4.6", - defaultProvider: "openai", - defaultModel: "gpt-5.2", - requireProviderOrAlias: true, - }); - - expect(result).toEqual({ - key: "vercel-ai-gateway/anthropic/claude-opus-4.6", - ref: { provider: "vercel-ai-gateway", model: "anthropic/claude-opus-4.6" }, - }); - }); }); describe("resolveModelRefFromString", () => { diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index c1b7a254fb3..cf66d1d1c68 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -514,7 +514,6 @@ export function resolveAllowedModelRef(params: { raw: string; defaultProvider: string; defaultModel?: string; - requireProviderOrAlias?: boolean; }): | { ref: ModelRef; key: string } | { @@ -537,9 +536,6 @@ export function resolveAllowedModelRef(params: { if (!resolved) { return { error: `invalid model: ${trimmed}` }; } - if (params.requireProviderOrAlias === true && !resolved.alias && !trimmed.includes("/")) { - return { error: `invalid model: ${trimmed} (use provider/model or alias)` }; - } const status = getModelRefStatus({ cfg: params.cfg, diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 5dcd5b127f0..357d1f4e563 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -2,9 +2,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { clearBootstrapSnapshot } from "../../agents/bootstrap-cache.js"; -import { resolveDefaultModelForAgent } from "../../agents/model-selection.js"; import { abortEmbeddedPiRun, waitForEmbeddedPiRunEnd } from "../../agents/pi-embedded.js"; -import { resolveSelectedAndActiveModel } from "../../auto-reply/model-runtime.js"; import { stopSubagentsForRequester } from "../../auto-reply/reply/abort.js"; import { clearSessionQueues } from "../../auto-reply/reply/queue.js"; import { loadConfig } from "../../config/config.js"; @@ -43,6 +41,7 @@ import { pruneLegacyStoreKeys, readSessionPreviewItemsFromTranscript, resolveGatewaySessionStoreTarget, + resolveSessionModelRef, resolveSessionTranscriptCandidates, type SessionsPatchResult, type SessionsPreviewEntry, @@ -325,26 +324,15 @@ export const sessionsHandlers: GatewayRequestHandlers = { } const parsed = parseAgentSessionKey(target.canonicalKey ?? key); const agentId = normalizeAgentId(parsed?.agentId ?? resolveDefaultAgentId(cfg)); - const agentDefault = resolveDefaultModelForAgent({ cfg, agentId }); - const selectedProvider = applied.entry.providerOverride?.trim() || agentDefault.provider; - const selectedModel = applied.entry.modelOverride?.trim() || agentDefault.model; - const selectedActive = resolveSelectedAndActiveModel({ - selectedProvider, - selectedModel, - sessionEntry: applied.entry, - }); + const resolved = resolveSessionModelRef(cfg, applied.entry, agentId); const result: SessionsPatchResult = { ok: true, path: storePath, key: target.canonicalKey, entry: applied.entry, resolved: { - modelProvider: selectedActive.active.provider, - model: selectedActive.active.model, - selectedModelProvider: selectedActive.selected.provider, - selectedModel: selectedActive.selected.model, - activeModelProvider: selectedActive.active.provider, - activeModel: selectedActive.active.model, + modelProvider: resolved.provider, + model: resolved.model, }, }; respond(true, result, undefined); diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 52cc013d06c..b05cf2220ed 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -419,14 +419,6 @@ describe("gateway server sessions", () => { const modelPatched = await rpcReq<{ ok: true; entry: { modelOverride?: string; providerOverride?: string }; - resolved?: { - modelProvider?: string; - model?: string; - selectedModelProvider?: string; - selectedModel?: string; - activeModelProvider?: string; - activeModel?: string; - }; }>(ws, "sessions.patch", { key: "agent:main:main", model: "openai/gpt-test-a", @@ -434,10 +426,6 @@ describe("gateway server sessions", () => { expect(modelPatched.ok).toBe(true); expect(modelPatched.payload?.entry.modelOverride).toBe("gpt-test-a"); expect(modelPatched.payload?.entry.providerOverride).toBe("openai"); - expect(modelPatched.payload?.resolved?.selectedModelProvider).toBe("openai"); - expect(modelPatched.payload?.resolved?.selectedModel).toBe("gpt-test-a"); - expect(modelPatched.payload?.resolved?.activeModelProvider).toBe("anthropic"); - expect(modelPatched.payload?.resolved?.activeModel).toBe("claude-sonnet-4-6"); const compacted = await rpcReq<{ ok: true; compacted: boolean }>(ws, "sessions.compact", { key: "agent:main:main", diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 4a7e91c9a4e..c1505642742 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -846,8 +846,6 @@ export function listSessionsFromStore(params: { responseUsage: entry?.responseUsage, modelProvider, model, - providerOverride: entry?.providerOverride, - modelOverride: entry?.modelOverride, contextTokens: entry?.contextTokens, deliveryContext: deliveryFields.deliveryContext, lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel, diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts index 605e89cbf28..233a3d7c782 100644 --- a/src/gateway/session-utils.types.ts +++ b/src/gateway/session-utils.types.ts @@ -37,8 +37,6 @@ export type GatewaySessionRow = { responseUsage?: "on" | "off" | "tokens" | "full"; modelProvider?: string; model?: string; - providerOverride?: string; - modelOverride?: string; contextTokens?: number; deliveryContext?: DeliveryContext; lastChannel?: SessionEntry["lastChannel"]; @@ -90,9 +88,5 @@ export type SessionsPatchResult = { resolved?: { modelProvider?: string; model?: string; - selectedModelProvider?: string; - selectedModel?: string; - activeModelProvider?: string; - activeModel?: string; }; }; diff --git a/src/gateway/sessions-patch.test.ts b/src/gateway/sessions-patch.test.ts index 2f2f1f8725e..6bf20d32641 100644 --- a/src/gateway/sessions-patch.test.ts +++ b/src/gateway/sessions-patch.test.ts @@ -243,104 +243,6 @@ describe("gateway sessions patch", () => { expect(res.entry.modelOverride).toBe("claude-sonnet-4-6"); }); - test("rejects non-alias direct model ids from sessions.patch", async () => { - const store: Record = {}; - const cfg = { - agents: { - defaults: { - model: { primary: "openai/gpt-5.2" }, - models: { - "anthropic/claude-sonnet-4-6": { alias: "sonnet" }, - }, - }, - }, - } as OpenClawConfig; - - const res = await applySessionsPatchToStore({ - cfg, - store, - storeKey: "agent:main:main", - patch: { key: "agent:main:main", model: "claude-sonnet-4-6" }, - loadGatewayModelCatalog: async () => [ - { provider: "anthropic", id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, - { provider: "openai", id: "gpt-5.2", name: "gpt-5.2" }, - ], - }); - - expect(res.ok).toBe(false); - if (res.ok) { - return; - } - expect(res.error.message).toContain("invalid model: claude-sonnet-4-6"); - }); - - test("accepts alias refs in strict sessions.patch parsing", async () => { - const store: Record = {}; - const cfg = { - agents: { - defaults: { - model: { primary: "openai/gpt-5.2" }, - models: { - "anthropic/claude-sonnet-4-6": { alias: "sonnet" }, - }, - }, - }, - } as OpenClawConfig; - - const res = await applySessionsPatchToStore({ - cfg, - store, - storeKey: "agent:main:main", - patch: { key: "agent:main:main", model: "sonnet" }, - loadGatewayModelCatalog: async () => [ - { provider: "anthropic", id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, - { provider: "openai", id: "gpt-5.2", name: "gpt-5.2" }, - ], - }); - - expect(res.ok).toBe(true); - if (!res.ok) { - return; - } - expect(res.entry.providerOverride).toBe("anthropic"); - expect(res.entry.modelOverride).toBe("claude-sonnet-4-6"); - }); - - test("accepts provider-prefixed refs with slash-containing model ids in strict sessions.patch parsing", async () => { - const store: Record = {}; - const cfg = { - agents: { - defaults: { - model: { primary: "openai/gpt-5.2" }, - models: { - "vercel-ai-gateway/anthropic/claude-opus-4.6": { alias: "gateway-opus" }, - }, - }, - }, - } as OpenClawConfig; - - const res = await applySessionsPatchToStore({ - cfg, - store, - storeKey: "agent:main:main", - patch: { key: "agent:main:main", model: "vercel-ai-gateway/anthropic/claude-opus-4.6" }, - loadGatewayModelCatalog: async () => [ - { - provider: "vercel-ai-gateway", - id: "anthropic/claude-opus-4.6", - name: "Gateway Claude Opus 4.6", - }, - ], - }); - - expect(res.ok).toBe(true); - if (!res.ok) { - return; - } - expect(res.entry.providerOverride).toBe("vercel-ai-gateway"); - expect(res.entry.modelOverride).toBe("anthropic/claude-opus-4.6"); - }); - test("accepts explicit allowlisted refs absent from bundled catalog", async () => { const store: Record = {}; const cfg = { diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index d6b5cd8848b..d55cf2cf1a4 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -304,7 +304,6 @@ export async function applySessionsPatchToStore(params: { raw: trimmed, defaultProvider: resolvedDefault.provider, defaultModel: subagentModelHint ?? resolvedDefault.model, - requireProviderOrAlias: true, }); if ("error" in resolved) { return invalid(resolved.error); diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index c6777a699ee..f55bbf5f354 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -67,8 +67,6 @@ export type GatewaySessionList = { updatedAt?: number | null; sendPolicy?: string; responseUsage?: ResponseUsageMode; - providerOverride?: string; - modelOverride?: string; label?: string; provider?: string; groupChannel?: string; diff --git a/src/tui/tui-session-actions.test.ts b/src/tui/tui-session-actions.test.ts index 2bed6c9e92e..067222811be 100644 --- a/src/tui/tui-session-actions.test.ts +++ b/src/tui/tui-session-actions.test.ts @@ -111,92 +111,4 @@ describe("tui session actions", () => { expect(updateFooter).toHaveBeenCalledTimes(2); expect(requestRender).toHaveBeenCalledTimes(2); }); - - it("keeps selected model visible across refresh when runtime model still lags", async () => { - const listSessions = vi.fn().mockResolvedValue({ - ts: Date.now(), - path: "/tmp/sessions.json", - count: 1, - defaults: { - modelProvider: "anthropic", - model: "claude-opus-4-6", - }, - sessions: [ - { - key: "agent:main:main", - updatedAt: 10, - modelProvider: "anthropic", - model: "claude-opus-4-6", - providerOverride: "anthropic", - modelOverride: "claude-sonnet-4-5", - }, - ], - }); - - const state: TuiStateAccess = { - agentDefaultId: "main", - sessionMainKey: "agent:main:main", - sessionScope: "global", - agents: [], - currentAgentId: "main", - currentSessionKey: "agent:main:main", - currentSessionId: null, - activeChatRunId: null, - historyLoaded: false, - sessionInfo: {}, - initialSessionApplied: true, - isConnected: true, - autoMessageSent: false, - toolsExpanded: false, - showThinking: false, - connectionStatus: "connected", - activityStatus: "idle", - statusTimeout: null, - lastCtrlCAt: 0, - }; - - const { applySessionInfoFromPatch, refreshSessionInfo } = createSessionActions({ - client: { listSessions } as unknown as GatewayChatClient, - chatLog: { addSystem: vi.fn() } as unknown as import("./components/chat-log.js").ChatLog, - tui: { requestRender: vi.fn() } as unknown as import("@mariozechner/pi-tui").TUI, - opts: {}, - state, - agentNames: new Map(), - initialSessionInput: "", - initialSessionAgentId: null, - resolveSessionKey: vi.fn(), - updateHeader: vi.fn(), - updateFooter: vi.fn(), - updateAutocompleteProvider: vi.fn(), - setActivityStatus: vi.fn(), - }); - - applySessionInfoFromPatch({ - ok: true, - path: "/tmp/sessions.json", - key: "agent:main:main", - entry: { - sessionId: "sess-main", - updatedAt: 10, - modelProvider: "anthropic", - model: "claude-opus-4-6", - providerOverride: "anthropic", - modelOverride: "claude-sonnet-4-5", - }, - resolved: { - modelProvider: "anthropic", - model: "claude-opus-4-6", - selectedModelProvider: "anthropic", - selectedModel: "claude-sonnet-4-5", - activeModelProvider: "anthropic", - activeModel: "claude-opus-4-6", - }, - }); - expect(state.sessionInfo.modelProvider).toBe("anthropic"); - expect(state.sessionInfo.model).toBe("claude-sonnet-4-5"); - - await refreshSessionInfo(); - expect(state.sessionInfo.modelProvider).toBe("anthropic"); - expect(state.sessionInfo.model).toBe("claude-sonnet-4-5"); - }); }); diff --git a/src/tui/tui-session-actions.ts b/src/tui/tui-session-actions.ts index c04bc1ba5bc..7255b712936 100644 --- a/src/tui/tui-session-actions.ts +++ b/src/tui/tui-session-actions.ts @@ -115,18 +115,17 @@ export function createSessionActions(context: SessionActionContext) { }; const resolveModelSelection = (entry?: SessionInfoEntry) => { - const overrideModel = entry?.modelOverride?.trim(); - if (overrideModel) { - const overrideProvider = - entry?.providerOverride?.trim() || entry?.modelProvider || state.sessionInfo.modelProvider; - return { modelProvider: overrideProvider, model: overrideModel }; - } if (entry?.modelProvider || entry?.model) { return { modelProvider: entry.modelProvider ?? state.sessionInfo.modelProvider, model: entry.model ?? state.sessionInfo.model, }; } + const overrideModel = entry?.modelOverride?.trim(); + if (overrideModel) { + const overrideProvider = entry?.providerOverride?.trim() || state.sessionInfo.modelProvider; + return { modelProvider: overrideProvider, model: overrideModel }; + } return { modelProvider: state.sessionInfo.modelProvider, model: state.sessionInfo.model, @@ -271,21 +270,14 @@ export function createSessionActions(context: SessionActionContext) { updateHeader(); } const resolved = result.resolved; - const hasSelected = Boolean(resolved?.selectedModelProvider || resolved?.selectedModel); - const entry: SessionInfoEntry = { - ...result.entry, - ...(hasSelected + const entry = + resolved && (resolved.modelProvider || resolved.model) ? { - modelProvider: - resolved?.selectedModelProvider ?? - result.entry.providerOverride ?? - result.entry.modelProvider, - model: resolved?.selectedModel ?? result.entry.modelOverride ?? result.entry.model, - providerOverride: resolved?.selectedModelProvider ?? result.entry.providerOverride, - modelOverride: resolved?.selectedModel ?? result.entry.modelOverride, + ...result.entry, + modelProvider: resolved.modelProvider ?? result.entry.modelProvider, + model: resolved.model ?? result.entry.model, } - : {}), - }; + : result.entry; applySessionInfo({ entry, force: true }); };