mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 04:57:26 +00:00
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:
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resetLogger, setLoggerOverride } from "../logging/logger.js";
|
||||
import {
|
||||
buildAllowedModelSet,
|
||||
inferUniqueProviderFromConfiguredModels,
|
||||
parseModelRef,
|
||||
buildModelAliasIndex,
|
||||
modelKey,
|
||||
@@ -134,6 +135,85 @@ describe("model-selection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("inferUniqueProviderFromConfiguredModels", () => {
|
||||
it("infers provider when configured model match is unique", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
cfg,
|
||||
model: "claude-sonnet-4-6",
|
||||
}),
|
||||
).toBe("anthropic");
|
||||
});
|
||||
|
||||
it("returns undefined when configured matches are ambiguous", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": {},
|
||||
"minimax/claude-sonnet-4-6": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
cfg,
|
||||
model: "claude-sonnet-4-6",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for provider-prefixed model ids", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
cfg,
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("infers provider for slash-containing model id when allowlist match is unique", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"vercel-ai-gateway/anthropic/claude-sonnet-4-6": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
cfg,
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
}),
|
||||
).toBe("vercel-ai-gateway");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildModelAliasIndex", () => {
|
||||
it("should build alias index from config", () => {
|
||||
const cfg: Partial<OpenClawConfig> = {
|
||||
|
||||
@@ -171,6 +171,42 @@ export function parseModelRef(raw: string, defaultProvider: string): ModelRef |
|
||||
return normalizeModelRef(providerRaw, model);
|
||||
}
|
||||
|
||||
export function inferUniqueProviderFromConfiguredModels(params: {
|
||||
cfg: OpenClawConfig;
|
||||
model: string;
|
||||
}): string | undefined {
|
||||
const model = params.model.trim();
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
const configuredModels = params.cfg.agents?.defaults?.models;
|
||||
if (!configuredModels) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = model.toLowerCase();
|
||||
const providers = new Set<string>();
|
||||
for (const key of Object.keys(configuredModels)) {
|
||||
const ref = key.trim();
|
||||
if (!ref || !ref.includes("/")) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseModelRef(ref, DEFAULT_PROVIDER);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
if (parsed.model === model || parsed.model.toLowerCase() === normalized) {
|
||||
providers.add(parsed.provider);
|
||||
if (providers.size > 1) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (providers.size !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
return providers.values().next().value;
|
||||
}
|
||||
|
||||
export function normalizeModelSelection(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
|
||||
@@ -4,7 +4,11 @@ import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import { isCliProvider } from "../../agents/model-selection.js";
|
||||
import { deriveSessionTotalTokens, hasNonzeroUsage } from "../../agents/usage.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||
import {
|
||||
setSessionRuntimeModel,
|
||||
type SessionEntry,
|
||||
updateSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
|
||||
type RunResult = Awaited<
|
||||
ReturnType<(typeof import("../../agents/pi-embedded.js"))["runEmbeddedPiAgent"]>
|
||||
@@ -58,10 +62,12 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
||||
...entry,
|
||||
sessionId,
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: providerUsed,
|
||||
model: modelUsed,
|
||||
contextTokens,
|
||||
};
|
||||
setSessionRuntimeModel(next, {
|
||||
provider: providerUsed,
|
||||
model: modelUsed,
|
||||
});
|
||||
if (isCliProvider(providerUsed, cfg)) {
|
||||
const cliSessionId = result.meta.agentMeta?.sessionId?.trim();
|
||||
if (cliSessionId) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from
|
||||
import {
|
||||
clearSessionStoreCacheForTest,
|
||||
loadSessionStore,
|
||||
mergeSessionEntry,
|
||||
resolveAndPersistSessionFile,
|
||||
updateSessionStore,
|
||||
} from "../sessions.js";
|
||||
@@ -215,6 +216,42 @@ describe("session store lock (Promise chain mutex)", () => {
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[key]?.modelOverride).toBe("recovered");
|
||||
});
|
||||
|
||||
it("clears stale runtime provider when model is patched without provider", () => {
|
||||
const merged = mergeSessionEntry(
|
||||
{
|
||||
sessionId: "sess-runtime",
|
||||
updatedAt: 100,
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
},
|
||||
{
|
||||
model: "gpt-5.2",
|
||||
},
|
||||
);
|
||||
expect(merged.model).toBe("gpt-5.2");
|
||||
expect(merged.modelProvider).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes orphan modelProvider fields at store write boundary", async () => {
|
||||
const key = "agent:main:orphan-provider";
|
||||
const { storePath } = await makeTmpStore({
|
||||
[key]: {
|
||||
sessionId: "sess-orphan",
|
||||
updatedAt: 100,
|
||||
modelProvider: "anthropic",
|
||||
},
|
||||
});
|
||||
|
||||
await updateSessionStore(storePath, async (store) => {
|
||||
const entry = store[key];
|
||||
entry.updatedAt = Date.now();
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[key]?.modelProvider).toBeUndefined();
|
||||
expect(store[key]?.model).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("appendAssistantMessageToSessionTranscript", () => {
|
||||
|
||||
@@ -22,7 +22,11 @@ import { loadConfig } from "../config.js";
|
||||
import type { SessionMaintenanceConfig, SessionMaintenanceMode } from "../types.base.js";
|
||||
import { enforceSessionDiskBudget, type SessionDiskBudgetSweepResult } from "./disk-budget.js";
|
||||
import { deriveSessionMetaPatch } from "./metadata.js";
|
||||
import { mergeSessionEntry, type SessionEntry } from "./types.js";
|
||||
import {
|
||||
mergeSessionEntry,
|
||||
normalizeSessionRuntimeModelFields,
|
||||
type SessionEntry,
|
||||
} from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("sessions/store");
|
||||
|
||||
@@ -157,7 +161,7 @@ function normalizeSessionStore(store: Record<string, SessionEntry>): void {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeSessionEntryDelivery(entry);
|
||||
const normalized = normalizeSessionEntryDelivery(normalizeSessionRuntimeModelFields(entry));
|
||||
if (normalized !== entry) {
|
||||
store[key] = normalized;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,65 @@ export type SessionEntry = {
|
||||
systemPromptReport?: SessionSystemPromptReport;
|
||||
};
|
||||
|
||||
function normalizeRuntimeField(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export function normalizeSessionRuntimeModelFields(entry: SessionEntry): SessionEntry {
|
||||
const normalizedModel = normalizeRuntimeField(entry.model);
|
||||
const normalizedProvider = normalizeRuntimeField(entry.modelProvider);
|
||||
let next = entry;
|
||||
|
||||
if (!normalizedModel) {
|
||||
if (entry.model !== undefined || entry.modelProvider !== undefined) {
|
||||
next = { ...next };
|
||||
delete next.model;
|
||||
delete next.modelProvider;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
if (entry.model !== normalizedModel) {
|
||||
if (next === entry) {
|
||||
next = { ...next };
|
||||
}
|
||||
next.model = normalizedModel;
|
||||
}
|
||||
|
||||
if (!normalizedProvider) {
|
||||
if (entry.modelProvider !== undefined) {
|
||||
if (next === entry) {
|
||||
next = { ...next };
|
||||
}
|
||||
delete next.modelProvider;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
if (entry.modelProvider !== normalizedProvider) {
|
||||
if (next === entry) {
|
||||
next = { ...next };
|
||||
}
|
||||
next.modelProvider = normalizedProvider;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function setSessionRuntimeModel(
|
||||
entry: SessionEntry,
|
||||
runtime: { provider: string; model: string },
|
||||
): boolean {
|
||||
const provider = runtime.provider.trim();
|
||||
const model = runtime.model.trim();
|
||||
if (!provider || !model) {
|
||||
return false;
|
||||
}
|
||||
entry.modelProvider = provider;
|
||||
entry.model = model;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function mergeSessionEntry(
|
||||
existing: SessionEntry | undefined,
|
||||
patch: Partial<SessionEntry>,
|
||||
@@ -121,9 +180,20 @@ export function mergeSessionEntry(
|
||||
const sessionId = patch.sessionId ?? existing?.sessionId ?? crypto.randomUUID();
|
||||
const updatedAt = Math.max(existing?.updatedAt ?? 0, patch.updatedAt ?? 0, Date.now());
|
||||
if (!existing) {
|
||||
return { ...patch, sessionId, updatedAt };
|
||||
return normalizeSessionRuntimeModelFields({ ...patch, sessionId, updatedAt });
|
||||
}
|
||||
return { ...existing, ...patch, sessionId, updatedAt };
|
||||
const next = { ...existing, ...patch, sessionId, updatedAt };
|
||||
|
||||
// Guard against stale provider carry-over when callers patch runtime model
|
||||
// without also patching runtime provider.
|
||||
if (Object.hasOwn(patch, "model") && !Object.hasOwn(patch, "modelProvider")) {
|
||||
const patchedModel = normalizeRuntimeField(patch.model);
|
||||
const existingModel = normalizeRuntimeField(existing.model);
|
||||
if (patchedModel && patchedModel !== existingModel) {
|
||||
delete next.modelProvider;
|
||||
}
|
||||
}
|
||||
return normalizeSessionRuntimeModelFields(next);
|
||||
}
|
||||
|
||||
export function resolveFreshSessionTotalTokens(
|
||||
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
} from "../../auto-reply/thinking.js";
|
||||
import type { CliDeps } from "../../cli/outbound-send-deps.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveSessionTranscriptPath, updateSessionStore } from "../../config/sessions.js";
|
||||
import {
|
||||
resolveSessionTranscriptPath,
|
||||
setSessionRuntimeModel,
|
||||
updateSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
import type { AgentDefaultsConfig } from "../../config/types.js";
|
||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { logWarn } from "../../logger.js";
|
||||
@@ -481,8 +485,10 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
const contextTokens =
|
||||
agentCfg?.contextTokens ?? lookupContextTokens(modelUsed) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
cronSession.sessionEntry.modelProvider = providerUsed;
|
||||
cronSession.sessionEntry.model = modelUsed;
|
||||
setSessionRuntimeModel(cronSession.sessionEntry, {
|
||||
provider: providerUsed,
|
||||
model: modelUsed,
|
||||
});
|
||||
cronSession.sessionEntry.contextTokens = contextTokens;
|
||||
if (isCliProvider(providerUsed, cfgWithAgentDefaults)) {
|
||||
const cliSessionId = runResult.meta?.agentMeta?.sessionId?.trim();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user