mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 00:17:13 +00:00
Revert "Gateway/TUI: enforce strict model refs and sync selected model"
This reverts commit 0da3cd2f18.
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<string, SessionEntry> = {};
|
||||
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<string, SessionEntry> = {};
|
||||
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<string, SessionEntry> = {};
|
||||
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<string, SessionEntry> = {};
|
||||
const cfg = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -67,8 +67,6 @@ export type GatewaySessionList = {
|
||||
updatedAt?: number | null;
|
||||
sendPolicy?: string;
|
||||
responseUsage?: ResponseUsageMode;
|
||||
providerOverride?: string;
|
||||
modelOverride?: string;
|
||||
label?: string;
|
||||
provider?: string;
|
||||
groupChannel?: string;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user