mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +00:00
fix(sessions): fast-path qualified row model refs
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Plugin skills/Windows: publish plugin-provided skill directories as junctions on Windows so standard users without Developer Mode can register plugin skills without symlink EPERM failures. Fixes #77958. (#77971) Thanks @hclsys and @jarro.
|
||||
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.
|
||||
- Gateway/sessions: fast-path already-qualified model refs while building session-list rows so `openclaw sessions` and Control UI session lists avoid heavyweight model resolution on large stores. (#77902) Thanks @ragesaq.
|
||||
- PR triage: mark external pull requests with `proof: supplied` when Barnacle finds structured real behavior proof, keep stale negative proof labels in sync across CRLF-edited PR bodies, and let ClawSweeper own the stronger `proof: sufficient` judgement.
|
||||
- Sessions CLI: show the selected agent runtime in the `openclaw sessions` table so terminal output matches the runtime visibility already present in JSON/status surfaces. Thanks @vincentkoc.
|
||||
- Talk/voice: unify realtime relay, transcription relay, managed-room handoff, Voice Call, Google Meet, VoiceClaw, and native clients around a shared Talk session controller and add the Gateway-managed `talk.session.*` RPC surface.
|
||||
|
||||
@@ -1440,6 +1440,44 @@ describe("listSessionsFromStore selected model display", () => {
|
||||
expect(result.sessions[0]?.modelProvider).toBe("anthropic");
|
||||
expect(result.sessions[0]?.model).toBe("claude-opus-4-7");
|
||||
});
|
||||
|
||||
test("uses qualified selected defaults for rows without runtime model metadata", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: { model: { primary: "openai/gpt-5.4" } },
|
||||
list: [
|
||||
{ id: "main", model: { primary: "anthropic/claude-sonnet-4-6" } },
|
||||
{
|
||||
id: "review",
|
||||
model: { primary: "vercel-ai-gateway/anthropic/claude-haiku-4-5" },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store: {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: 2,
|
||||
} as SessionEntry,
|
||||
"agent:review:review": {
|
||||
sessionId: "sess-review",
|
||||
updatedAt: 1,
|
||||
} as SessionEntry,
|
||||
},
|
||||
opts: {},
|
||||
});
|
||||
|
||||
expect(
|
||||
result.sessions.map((session) => [session.key, session.modelProvider, session.model]),
|
||||
).toEqual([
|
||||
["agent:main:main", "anthropic", "claude-sonnet-4-6"],
|
||||
["agent:review:review", "vercel-ai-gateway", "anthropic/claude-haiku-4-5"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionModelIdentityRef", () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
inferUniqueProviderFromConfiguredModels,
|
||||
isCliProvider,
|
||||
normalizeProviderId,
|
||||
normalizeStoredOverrideModel,
|
||||
parseModelRef,
|
||||
resolveConfiguredModelRef,
|
||||
@@ -77,6 +78,7 @@ import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
normalizeOptionalLowercaseString,
|
||||
resolvePrimaryStringValue,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.shared.js";
|
||||
import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js";
|
||||
@@ -373,6 +375,7 @@ type SessionListRowContext = {
|
||||
subagentRuns: ReturnType<typeof buildSubagentRunReadIndex>;
|
||||
storeChildSessionsByKey: Map<string, string[]>;
|
||||
selectedModelByOverrideRef: Map<string, ReturnType<typeof resolveSessionModelRef>>;
|
||||
modelIdentityByEntryKey: Map<string, ReturnType<typeof resolveSessionModelIdentityRef>>;
|
||||
thinkingLevelsByModelRef: Map<string, ReturnType<typeof listThinkingLevelOptions>>;
|
||||
};
|
||||
|
||||
@@ -491,6 +494,7 @@ function buildSessionListRowContext(params: {
|
||||
subagentRuns,
|
||||
storeChildSessionsByKey: buildStoreChildSessionIndex(params.store, params.now, subagentRuns),
|
||||
selectedModelByOverrideRef: new Map(),
|
||||
modelIdentityByEntryKey: new Map(),
|
||||
thinkingLevelsByModelRef: new Map(),
|
||||
};
|
||||
}
|
||||
@@ -499,6 +503,116 @@ function createSessionRowModelCacheKey(provider: string | undefined, model: stri
|
||||
return `${normalizeLowercaseStringOrEmpty(provider)}\0${normalizeOptionalString(model) ?? ""}`;
|
||||
}
|
||||
|
||||
function parseQualifiedModelRefForSessionList(
|
||||
raw: string | undefined,
|
||||
): { provider: string; model: string } | undefined {
|
||||
const trimmed = normalizeOptionalString(raw);
|
||||
const slash = trimmed?.indexOf("/") ?? -1;
|
||||
if (!trimmed || slash <= 0 || slash === trimmed.length - 1) {
|
||||
return undefined;
|
||||
}
|
||||
const provider = normalizeProviderId(trimmed.slice(0, slash).trim());
|
||||
const model = normalizeOptionalString(trimmed.slice(slash + 1));
|
||||
return model ? { provider, model } : undefined;
|
||||
}
|
||||
|
||||
function createSessionDefaultModelCacheKey(cfg: OpenClawConfig, agentId?: string): string {
|
||||
const normalizedAgentId = normalizeAgentId(agentId);
|
||||
const primary = normalizedAgentId
|
||||
? resolveAgentEffectiveModelPrimary(cfg, normalizedAgentId)
|
||||
: resolvePrimaryStringValue(cfg.agents?.defaults?.model);
|
||||
return normalizeOptionalString(primary) ?? "";
|
||||
}
|
||||
|
||||
function createSessionEntryModelCacheKey(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
entry?:
|
||||
| SessionEntry
|
||||
| Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">;
|
||||
fallbackModelRef?: string;
|
||||
}) {
|
||||
return [
|
||||
createSessionDefaultModelCacheKey(params.cfg, params.agentId),
|
||||
normalizeOptionalString(params.entry?.providerOverride) ?? "",
|
||||
normalizeOptionalString(params.entry?.modelOverride) ?? "",
|
||||
normalizeOptionalString(params.entry?.modelProvider) ?? "",
|
||||
normalizeOptionalString(params.entry?.model) ?? "",
|
||||
normalizeOptionalString(params.fallbackModelRef) ?? "",
|
||||
].join("\0");
|
||||
}
|
||||
|
||||
function resolveSessionDefaultModelRefForRow(
|
||||
cfg: OpenClawConfig,
|
||||
agentId?: string,
|
||||
): { provider: string; model: string } {
|
||||
if (agentId) {
|
||||
const primary = resolveAgentEffectiveModelPrimary(cfg, agentId)?.trim();
|
||||
const parsed = parseQualifiedModelRefForSessionList(primary);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
return resolveDefaultModelForAgent({ cfg, agentId });
|
||||
}
|
||||
return resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveSessionRowModelIdentityRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
entry?:
|
||||
| SessionEntry
|
||||
| Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">;
|
||||
agentId: string;
|
||||
fallbackModelRef?: string;
|
||||
rowContext?: SessionListRowContext;
|
||||
}): ReturnType<typeof resolveSessionModelIdentityRef> {
|
||||
if (!params.rowContext) {
|
||||
return resolveSessionModelIdentityRef(
|
||||
params.cfg,
|
||||
params.entry,
|
||||
params.agentId,
|
||||
params.fallbackModelRef,
|
||||
);
|
||||
}
|
||||
const key = createSessionEntryModelCacheKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
entry: params.entry,
|
||||
fallbackModelRef: params.fallbackModelRef,
|
||||
});
|
||||
const cached = params.rowContext.modelIdentityByEntryKey.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const runtimeModel = normalizeOptionalString(params.entry?.model);
|
||||
const runtimeProvider = normalizeOptionalString(params.entry?.modelProvider);
|
||||
if (
|
||||
!runtimeModel &&
|
||||
!runtimeProvider &&
|
||||
!normalizeOptionalString(params.fallbackModelRef) &&
|
||||
!normalizeOptionalString(params.entry?.providerOverride) &&
|
||||
!normalizeOptionalString(params.entry?.modelOverride)
|
||||
) {
|
||||
const resolved = resolveSessionDefaultModelRefForRow(params.cfg, params.agentId);
|
||||
params.rowContext.modelIdentityByEntryKey.set(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const resolved = resolveSessionModelIdentityRef(
|
||||
params.cfg,
|
||||
params.entry,
|
||||
params.agentId,
|
||||
params.fallbackModelRef,
|
||||
);
|
||||
params.rowContext.modelIdentityByEntryKey.set(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveSessionSelectedModelRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
entry?: SessionEntry;
|
||||
@@ -1578,12 +1692,13 @@ export function buildGatewaySessionRow(params: {
|
||||
agentId: sessionAgentId,
|
||||
rowContext,
|
||||
});
|
||||
const resolvedModel = resolveSessionModelIdentityRef(
|
||||
const resolvedModel = resolveSessionRowModelIdentityRef({
|
||||
cfg,
|
||||
entry,
|
||||
sessionAgentId,
|
||||
subagentRun?.model,
|
||||
);
|
||||
agentId: sessionAgentId,
|
||||
fallbackModelRef: subagentRun?.model,
|
||||
rowContext,
|
||||
});
|
||||
const runtimeModelPresent =
|
||||
Boolean(entry?.model?.trim()) || Boolean(entry?.modelProvider?.trim());
|
||||
const needsTranscriptTotalTokens =
|
||||
|
||||
Reference in New Issue
Block a user