fix(tui): resolve wrong provider prefix when session has model without modelProvider (#25874)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f0953a7284
Co-authored-by: lbo728 <72309817+lbo728@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
byungsker
2026-02-25 14:36:27 +09:00
committed by GitHub
parent 8f5f599a34
commit 177386ed73
12 changed files with 559 additions and 13 deletions

View File

@@ -387,6 +387,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
reasoningLevel: entry?.reasoningLevel,
responseUsage: entry?.responseUsage,
model: entry?.model,
modelProvider: entry?.modelProvider,
contextTokens: entry?.contextTokens,
sendPolicy: entry?.sendPolicy,
label: entry?.label,

View File

@@ -202,6 +202,8 @@ describe("gateway server sessions", () => {
main: {
sessionId: "sess-main",
updatedAt: recent,
modelProvider: "anthropic",
model: "claude-sonnet-4-6",
inputTokens: 10,
outputTokens: 20,
thinkingLevel: "low",
@@ -456,11 +458,13 @@ describe("gateway server sessions", () => {
const reset = await rpcReq<{
ok: true;
key: string;
entry: { sessionId: string };
entry: { sessionId: string; modelProvider?: string; model?: string };
}>(ws, "sessions.reset", { key: "agent:main:main" });
expect(reset.ok).toBe(true);
expect(reset.payload?.key).toBe("agent:main:main");
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
expect(reset.payload?.entry.modelProvider).toBe("anthropic");
expect(reset.payload?.entry.model).toBe("claude-sonnet-4-6");
const filesAfterReset = await fs.readdir(dir);
expect(filesAfterReset.some((f) => f.startsWith("sess-main.jsonl.reset."))).toBe(true);

View File

@@ -13,6 +13,7 @@ import {
parseGroupKey,
pruneLegacyStoreKeys,
resolveGatewaySessionStoreTarget,
resolveSessionModelIdentityRef,
resolveSessionModelRef,
resolveSessionStoreKey,
} from "./session-utils.js";
@@ -339,6 +340,159 @@ describe("resolveSessionModelRef", () => {
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
});
test("falls back to resolved provider for unprefixed legacy runtime model", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelRef(cfg, {
sessionId: "legacy-session",
updatedAt: Date.now(),
model: "claude-sonnet-4-6",
modelProvider: undefined,
});
expect(resolved).toEqual({
provider: "google-gemini-cli",
model: "claude-sonnet-4-6",
});
});
test("preserves provider from slash-prefixed model when modelProvider is missing", () => {
// When model string contains a provider prefix (e.g. "anthropic/claude-sonnet-4-6")
// parseModelRef should extract it correctly even without modelProvider set.
const cfg = {
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelRef(cfg, {
sessionId: "slash-model",
updatedAt: Date.now(),
model: "anthropic/claude-sonnet-4-6",
modelProvider: undefined,
});
expect(resolved).toEqual({ provider: "anthropic", model: "claude-sonnet-4-6" });
});
});
describe("resolveSessionModelIdentityRef", () => {
test("does not inherit default provider for unprefixed legacy runtime model", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelIdentityRef(cfg, {
sessionId: "legacy-session",
updatedAt: Date.now(),
model: "claude-sonnet-4-6",
modelProvider: undefined,
});
expect(resolved).toEqual({ model: "claude-sonnet-4-6" });
});
test("infers provider from configured model allowlist when unambiguous", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
models: {
"anthropic/claude-sonnet-4-6": {},
},
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelIdentityRef(cfg, {
sessionId: "legacy-session",
updatedAt: Date.now(),
model: "claude-sonnet-4-6",
modelProvider: undefined,
});
expect(resolved).toEqual({ provider: "anthropic", model: "claude-sonnet-4-6" });
});
test("keeps provider unknown when configured models are ambiguous", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
models: {
"anthropic/claude-sonnet-4-6": {},
"minimax/claude-sonnet-4-6": {},
},
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelIdentityRef(cfg, {
sessionId: "legacy-session",
updatedAt: Date.now(),
model: "claude-sonnet-4-6",
modelProvider: undefined,
});
expect(resolved).toEqual({ model: "claude-sonnet-4-6" });
});
test("preserves provider from slash-prefixed runtime model", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelIdentityRef(cfg, {
sessionId: "slash-model",
updatedAt: Date.now(),
model: "anthropic/claude-sonnet-4-6",
modelProvider: undefined,
});
expect(resolved).toEqual({ provider: "anthropic", model: "claude-sonnet-4-6" });
});
test("infers wrapper provider for slash-prefixed runtime model when allowlist match is unique", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
models: {
"vercel-ai-gateway/anthropic/claude-sonnet-4-6": {},
},
},
},
} as OpenClawConfig;
const resolved = resolveSessionModelIdentityRef(cfg, {
sessionId: "slash-model",
updatedAt: Date.now(),
model: "anthropic/claude-sonnet-4-6",
modelProvider: undefined,
});
expect(resolved).toEqual({
provider: "vercel-ai-gateway",
model: "anthropic/claude-sonnet-4-6",
});
});
});
describe("deriveSessionTitle", () => {
@@ -529,6 +683,99 @@ describe("listSessionsFromStore search", () => {
expect(result.sessions.map((session) => session.key)).toEqual(["agent:main:cron:job-1"]);
});
test("does not guess provider for legacy runtime model without modelProvider", () => {
const cfg = {
session: { mainKey: "main" },
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
},
},
} as OpenClawConfig;
const now = Date.now();
const store: Record<string, SessionEntry> = {
"agent:main:main": {
sessionId: "sess-main",
updatedAt: now,
model: "claude-sonnet-4-6",
} as SessionEntry,
};
const result = listSessionsFromStore({
cfg,
storePath: "/tmp/sessions.json",
store,
opts: {},
});
expect(result.sessions[0]?.modelProvider).toBeUndefined();
expect(result.sessions[0]?.model).toBe("claude-sonnet-4-6");
});
test("infers provider for legacy runtime model when allowlist match is unique", () => {
const cfg = {
session: { mainKey: "main" },
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
models: {
"anthropic/claude-sonnet-4-6": {},
},
},
},
} as OpenClawConfig;
const now = Date.now();
const store: Record<string, SessionEntry> = {
"agent:main:main": {
sessionId: "sess-main",
updatedAt: now,
model: "claude-sonnet-4-6",
} as SessionEntry,
};
const result = listSessionsFromStore({
cfg,
storePath: "/tmp/sessions.json",
store,
opts: {},
});
expect(result.sessions[0]?.modelProvider).toBe("anthropic");
expect(result.sessions[0]?.model).toBe("claude-sonnet-4-6");
});
test("infers wrapper provider for slash-prefixed legacy runtime model when allowlist match is unique", () => {
const cfg = {
session: { mainKey: "main" },
agents: {
defaults: {
model: { primary: "google-gemini-cli/gemini-3-pro-preview" },
models: {
"vercel-ai-gateway/anthropic/claude-sonnet-4-6": {},
},
},
},
} as OpenClawConfig;
const now = Date.now();
const store: Record<string, SessionEntry> = {
"agent:main:main": {
sessionId: "sess-main",
updatedAt: now,
model: "anthropic/claude-sonnet-4-6",
} as SessionEntry,
};
const result = listSessionsFromStore({
cfg,
storePath: "/tmp/sessions.json",
store,
opts: {},
});
expect(result.sessions[0]?.modelProvider).toBe("vercel-ai-gateway");
expect(result.sessions[0]?.model).toBe("anthropic/claude-sonnet-4-6");
});
test("exposes unknown totals when freshness is stale or missing", () => {
const now = Date.now();
const store: Record<string, SessionEntry> = {

View File

@@ -4,6 +4,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
import { lookupContextTokens } from "../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import {
inferUniqueProviderFromConfiguredModels,
parseModelRef,
resolveConfiguredModelRef,
resolveDefaultModelForAgent,
@@ -692,6 +693,39 @@ export function resolveSessionModelRef(
return { provider, model };
}
export function resolveSessionModelIdentityRef(
cfg: OpenClawConfig,
entry?:
| SessionEntry
| Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">,
agentId?: string,
): { provider?: string; model: string } {
const runtimeModel = entry?.model?.trim();
const runtimeProvider = entry?.modelProvider?.trim();
if (runtimeModel) {
if (runtimeProvider) {
return { provider: runtimeProvider, model: runtimeModel };
}
const inferredProvider = inferUniqueProviderFromConfiguredModels({
cfg,
model: runtimeModel,
});
if (inferredProvider) {
return { provider: inferredProvider, model: runtimeModel };
}
if (runtimeModel.includes("/")) {
const parsedRuntime = parseModelRef(runtimeModel, DEFAULT_PROVIDER);
if (parsedRuntime) {
return { provider: parsedRuntime.provider, model: parsedRuntime.model };
}
return { model: runtimeModel };
}
return { model: runtimeModel };
}
const resolved = resolveSessionModelRef(cfg, entry, agentId);
return { provider: resolved.provider, model: resolved.model };
}
export function listSessionsFromStore(params: {
cfg: OpenClawConfig;
storePath: string;
@@ -782,8 +816,8 @@ export function listSessionsFromStore(params: {
const deliveryFields = normalizeSessionDeliveryFields(entry);
const parsedAgent = parseAgentSessionKey(key);
const sessionAgentId = normalizeAgentId(parsedAgent?.agentId ?? resolveDefaultAgentId(cfg));
const resolvedModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
const modelProvider = resolvedModel.provider ?? DEFAULT_PROVIDER;
const resolvedModel = resolveSessionModelIdentityRef(cfg, entry, sessionAgentId);
const modelProvider = resolvedModel.provider;
const model = resolvedModel.model ?? DEFAULT_MODEL;
return {
key,