mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:30:44 +00:00
fix: clarify session runtime metadata
This commit is contained in:
@@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/model selection: resolve slash-form aliases before provider/model parsing and keep alias-resolved primary models subject to transient provider cooldowns, so cron and persisted sessions do not retry cooled-down raw aliases. Fixes #73573 and #73657. Thanks @akai-shuuichi and @hashslingers.
|
||||
- Agents/Claude CLI: reuse already-cached macOS Keychain credentials for no-prompt Claude credential reads, so doctor/runtime checks do not miss fresh interactive Claude auth. Fixes #73682. Thanks @RyanSandoval.
|
||||
- Agents/Claude CLI doctor: scope workspace and project-dir checks to agents that actually use the Claude CLI runtime, so non-default Claude agents no longer make the default agent look Claude-backed. Fixes #73903. Thanks @bobfreeman1989.
|
||||
- Gateway/sessions: expose effective agent runtime metadata on session rows, `sessions.patch`, and local `openclaw sessions --json`, while keeping Claude CLI-backed rows on the canonical model provider so runtime backend and model identity are no longer conflated. Fixes #73090. Thanks @vishutdhar.
|
||||
- Agents/runtime status: expose effective agent runtime metadata in `agents.list`, Control UI agent panels, and `/agents`, and avoid rendering stale or cumulative CLI token totals as live context usage. Fixes #73660, #73578, and #45268. Thanks @spartman, @DashLabsDev, and @xyooz.
|
||||
- Agents/transcripts: strip empty assistant text blocks while preserving valid text, images, and signatures, so Anthropic-style providers no longer reject sanitized transcript turns. Fixes #73640. Thanks @jowhee327.
|
||||
- Providers/Bedrock: omit deprecated `temperature` for Claude Opus 4.7 Bedrock model ids, named and application inference profiles, including dotted `opus-4.7` refs, and classify the nested validation response for failover. Fixes #73663. Thanks @bstanbury.
|
||||
|
||||
@@ -387,7 +387,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Session control">
|
||||
- `sessions.list` returns the current session index.
|
||||
- `sessions.list` returns the current session index, including per-row `agentRuntime` metadata when an agent runtime backend is configured.
|
||||
- `sessions.subscribe` and `sessions.unsubscribe` toggle session change event subscriptions for the current WS client.
|
||||
- `sessions.messages.subscribe` and `sessions.messages.unsubscribe` toggle transcript/message event subscriptions for one session.
|
||||
- `sessions.preview` returns bounded transcript previews for specific session keys.
|
||||
@@ -396,7 +396,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `sessions.send` sends a message into an existing session.
|
||||
- `sessions.steer` is the interrupt-and-steer variant for an active session.
|
||||
- `sessions.abort` aborts active work for a session.
|
||||
- `sessions.patch` updates session metadata/overrides.
|
||||
- `sessions.patch` updates session metadata/overrides and reports the resolved canonical model plus effective `agentRuntime`.
|
||||
- `sessions.reset`, `sessions.delete`, and `sessions.compact` perform session maintenance.
|
||||
- `sessions.get` returns the full stored session row.
|
||||
- Chat execution still uses `chat.history`, `chat.send`, `chat.abort`, and `chat.inject`. `chat.history` is display-normalized for UI clients: inline directive tags are stripped from visible text, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks) and leaked ASCII/full-width model control tokens are stripped, pure silent-token assistant rows such as exact `NO_REPLY` / `no_reply` are omitted, and oversized rows can be replaced with placeholders.
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import {
|
||||
inferUniqueProviderFromConfiguredModels,
|
||||
isCliProvider,
|
||||
} from "../agents/model-selection.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
@@ -14,7 +18,9 @@ type SessionDisplayDefaults = {
|
||||
model: string;
|
||||
};
|
||||
|
||||
function parseModelRef(raw: string, defaultProvider: string): { provider: string; model: string } {
|
||||
type SessionDisplayModelRef = { provider: string; model: string };
|
||||
|
||||
function parseModelRef(raw: string, defaultProvider: string): SessionDisplayModelRef {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return { provider: defaultProvider, model: DEFAULT_MODEL };
|
||||
@@ -59,10 +65,7 @@ function normalizeStoredOverrideModel(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDefaultModelRef(
|
||||
cfg: OpenClawConfig,
|
||||
agentId?: string,
|
||||
): { provider: string; model: string } {
|
||||
function resolveDefaultModelRef(cfg: OpenClawConfig, agentId?: string): SessionDisplayModelRef {
|
||||
const primary =
|
||||
resolveAgentPrimaryModel(cfg, agentId) ??
|
||||
resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model) ??
|
||||
@@ -79,10 +82,48 @@ export function resolveSessionDisplayDefaults(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCliRuntimeDisplayRef(
|
||||
cfg: OpenClawConfig,
|
||||
ref: SessionDisplayModelRef,
|
||||
defaultRef: SessionDisplayModelRef,
|
||||
): SessionDisplayModelRef {
|
||||
if (!isCliProvider(ref.provider, cfg)) {
|
||||
return ref;
|
||||
}
|
||||
if (ref.model.includes("/")) {
|
||||
const parsed = parseModelRef(ref.model, defaultRef.provider);
|
||||
if (!isCliProvider(parsed.provider, cfg)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
const inferredProvider = inferUniqueProviderFromConfiguredModels({
|
||||
cfg,
|
||||
model: ref.model,
|
||||
});
|
||||
if (inferredProvider && !isCliProvider(inferredProvider, cfg)) {
|
||||
return { provider: inferredProvider, model: ref.model };
|
||||
}
|
||||
const parsed = parseModelRef(ref.model, defaultRef.provider);
|
||||
if (!isCliProvider(parsed.provider, cfg)) {
|
||||
return parsed;
|
||||
}
|
||||
return {
|
||||
provider: defaultRef.provider || ref.provider,
|
||||
model: parsed.model || ref.model,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSessionDisplayModel(
|
||||
cfg: OpenClawConfig,
|
||||
row: SessionDisplayModelRow,
|
||||
): string {
|
||||
return resolveSessionDisplayModelRef(cfg, row).model;
|
||||
}
|
||||
|
||||
export function resolveSessionDisplayModelRef(
|
||||
cfg: OpenClawConfig,
|
||||
row: SessionDisplayModelRow,
|
||||
): SessionDisplayModelRef {
|
||||
const agentId = row.key.startsWith("agent:") ? row.key.split(":")[1] : undefined;
|
||||
const defaultRef = resolveDefaultModelRef(cfg, agentId);
|
||||
const normalizedOverride = normalizeStoredOverrideModel({
|
||||
@@ -94,10 +135,14 @@ export function resolveSessionDisplayModel(
|
||||
return parseModelRef(
|
||||
normalizedOverride.modelOverride,
|
||||
normalizedOverride.providerOverride ?? defaultRef.provider,
|
||||
).model;
|
||||
);
|
||||
}
|
||||
if (row.model) {
|
||||
return parseModelRef(row.model, row.modelProvider ?? defaultRef.provider).model;
|
||||
return normalizeCliRuntimeDisplayRef(
|
||||
cfg,
|
||||
parseModelRef(row.model, row.modelProvider ?? defaultRef.provider),
|
||||
defaultRef,
|
||||
);
|
||||
}
|
||||
return defaultRef.model;
|
||||
return defaultRef;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mockSessionsConfig, runSessionsJson, writeStore } from "./sessions.test-helpers.js";
|
||||
import {
|
||||
mockSessionsConfig,
|
||||
resetMockSessionsConfig,
|
||||
runSessionsJson,
|
||||
setMockSessionsConfig,
|
||||
writeStore,
|
||||
} from "./sessions.test-helpers.js";
|
||||
|
||||
mockSessionsConfig();
|
||||
|
||||
@@ -8,7 +14,9 @@ import { sessionsCommand } from "./sessions.js";
|
||||
type SessionsJsonPayload = {
|
||||
sessions?: Array<{
|
||||
key: string;
|
||||
modelProvider?: string | null;
|
||||
model?: string | null;
|
||||
agentRuntime?: { id: string; fallback?: string; source: string };
|
||||
}>;
|
||||
};
|
||||
|
||||
@@ -38,6 +46,7 @@ describe("sessionsCommand model resolution", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetMockSessionsConfig();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
@@ -60,4 +69,69 @@ describe("sessionsCommand model resolution", () => {
|
||||
);
|
||||
expect(model).toBe("gpt-5.4");
|
||||
});
|
||||
|
||||
it("separates Claude CLI runtime from canonical model provider in JSON output", async () => {
|
||||
setMockSessionsConfig(() => ({
|
||||
agents: {
|
||||
defaults: {
|
||||
agentRuntime: { id: "claude-cli", fallback: "none" },
|
||||
model: { primary: "anthropic/claude-opus-4-7" },
|
||||
models: { "anthropic/claude-opus-4-7": {} },
|
||||
contextTokens: 200_000,
|
||||
},
|
||||
},
|
||||
}));
|
||||
const store = writeStore(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "main-session",
|
||||
updatedAt: Date.now() - 60_000,
|
||||
modelProvider: "claude-cli",
|
||||
model: "claude-opus-4-7",
|
||||
},
|
||||
},
|
||||
"sessions-claude-runtime",
|
||||
);
|
||||
|
||||
const payload = await runSessionsJson<SessionsJsonPayload>(sessionsCommand, store);
|
||||
const session = payload.sessions?.find((row) => row.key === "agent:main:main");
|
||||
|
||||
expect(session?.modelProvider).toBe("anthropic");
|
||||
expect(session?.model).toBe("claude-opus-4-7");
|
||||
expect(session?.agentRuntime).toEqual({
|
||||
id: "claude-cli",
|
||||
fallback: "none",
|
||||
source: "defaults",
|
||||
});
|
||||
});
|
||||
|
||||
it("infers canonical provider for bare CLI models before default-provider fallback", async () => {
|
||||
setMockSessionsConfig(() => ({
|
||||
agents: {
|
||||
defaults: {
|
||||
agentRuntime: { id: "claude-cli", fallback: "none" },
|
||||
model: { primary: "openai/gpt-5.4" },
|
||||
models: { "anthropic/claude-opus-4-7": {} },
|
||||
contextTokens: 200_000,
|
||||
},
|
||||
},
|
||||
}));
|
||||
const store = writeStore(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "main-session",
|
||||
updatedAt: Date.now() - 60_000,
|
||||
modelProvider: "claude-cli",
|
||||
model: "claude-opus-4-7",
|
||||
},
|
||||
},
|
||||
"sessions-claude-runtime-openai-default",
|
||||
);
|
||||
|
||||
const payload = await runSessionsJson<SessionsJsonPayload>(sessionsCommand, store);
|
||||
const session = payload.sessions?.find((row) => row.key === "agent:main:main");
|
||||
|
||||
expect(session?.modelProvider).toBe("anthropic");
|
||||
expect(session?.model).toBe("claude-opus-4-7");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
||||
import { vi } from "vitest";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
const sessionsConfigState = vi.hoisted(() => ({
|
||||
const sessionsConfigState = vi.hoisted<{ loadConfig: () => Record<string, unknown> }>(() => ({
|
||||
loadConfig: () => ({
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -17,6 +17,8 @@ const sessionsConfigState = vi.hoisted(() => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultSessionsConfigLoader = sessionsConfigState.loadConfig;
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
getRuntimeConfig: () => sessionsConfigState.loadConfig(),
|
||||
loadConfig: () => sessionsConfigState.loadConfig(),
|
||||
@@ -28,6 +30,14 @@ export function mockSessionsConfig() {
|
||||
// warnings before importing `sessions.ts`.
|
||||
}
|
||||
|
||||
export function setMockSessionsConfig(loader: () => Record<string, unknown>) {
|
||||
sessionsConfigState.loadConfig = loader;
|
||||
}
|
||||
|
||||
export function resetMockSessionsConfig() {
|
||||
sessionsConfigState.loadConfig = defaultSessionsConfigLoader;
|
||||
}
|
||||
|
||||
export function makeRuntime(params?: { throwOnError?: boolean }): {
|
||||
runtime: RuntimeEnv;
|
||||
logs: string[];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import { loadSessionStore, resolveSessionTotalTokens } from "../config/sessions.js";
|
||||
@@ -7,6 +8,7 @@ import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
import { resolveSessionStoreTargetsOrExit } from "./session-store-targets.js";
|
||||
import {
|
||||
resolveSessionDisplayModelRef,
|
||||
resolveSessionDisplayDefaults,
|
||||
resolveSessionDisplayModel,
|
||||
} from "./sessions-display-model.js";
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
type SessionRow = SessionDisplayRow & {
|
||||
agentId: string;
|
||||
kind: "direct" | "group" | "global" | "unknown";
|
||||
agentRuntime: ReturnType<typeof resolveAgentRuntimeMetadata>;
|
||||
};
|
||||
|
||||
const AGENT_PAD = 10;
|
||||
@@ -146,12 +149,14 @@ export async function sessionsCommand(
|
||||
const rows = targets
|
||||
.flatMap((target) => {
|
||||
const store = loadSessionStore(target.storePath);
|
||||
return toSessionDisplayRows(store).map((row) =>
|
||||
Object.assign({}, row, {
|
||||
agentId: parseAgentSessionKey(row.key)?.agentId ?? target.agentId,
|
||||
return toSessionDisplayRows(store).map((row) => {
|
||||
const agentId = parseAgentSessionKey(row.key)?.agentId ?? target.agentId;
|
||||
return Object.assign({}, row, {
|
||||
agentId,
|
||||
agentRuntime: resolveAgentRuntimeMetadata(cfg, agentId),
|
||||
kind: classifySessionKey(row.key, store[row.key]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
})
|
||||
.filter((row) => {
|
||||
if (activeMinutes === undefined) {
|
||||
@@ -180,7 +185,7 @@ export async function sessionsCommand(
|
||||
activeMinutes: activeMinutes ?? null,
|
||||
sessions: await Promise.all(
|
||||
rows.map(async (r) => {
|
||||
const model = resolveSessionDisplayModel(cfg, r);
|
||||
const modelRef = resolveSessionDisplayModelRef(cfg, r);
|
||||
return {
|
||||
...r,
|
||||
totalTokens: resolveSessionTotalTokens(r) ?? null,
|
||||
@@ -189,10 +194,11 @@ export async function sessionsCommand(
|
||||
contextTokens:
|
||||
r.contextTokens ??
|
||||
configuredContextTokens ??
|
||||
(await lookupContextTokensForDisplay(model)) ??
|
||||
(await lookupContextTokensForDisplay(modelRef.model)) ??
|
||||
configContextTokens ??
|
||||
null,
|
||||
model,
|
||||
modelProvider: modelRef.provider,
|
||||
model: modelRef.model,
|
||||
};
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { resolveAgentRuntimeMetadata } from "../../agents/agent-runtime-metadata.js";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
@@ -79,6 +80,7 @@ import {
|
||||
resolveDeletedAgentIdFromSessionKey,
|
||||
resolveFreshestSessionEntryFromStoreKeys,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionDisplayModelIdentityRef,
|
||||
resolveSessionModelRef,
|
||||
resolveSessionTranscriptCandidates,
|
||||
type SessionsPatchResult,
|
||||
@@ -1364,14 +1366,22 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
const parsed = parseAgentSessionKey(target.canonicalKey ?? key);
|
||||
const agentId = normalizeAgentId(parsed?.agentId ?? resolveDefaultAgentId(cfg));
|
||||
const resolved = resolveSessionModelRef(cfg, applied.entry, agentId);
|
||||
const resolvedDisplayModel = resolveSessionDisplayModelIdentityRef({
|
||||
cfg,
|
||||
agentId,
|
||||
provider: resolved.provider,
|
||||
model: resolved.model,
|
||||
});
|
||||
const agentRuntime = resolveAgentRuntimeMetadata(cfg, agentId);
|
||||
const result: SessionsPatchResult = {
|
||||
ok: true,
|
||||
path: storePath,
|
||||
key: target.canonicalKey,
|
||||
entry: applied.entry,
|
||||
resolved: {
|
||||
modelProvider: resolved.provider,
|
||||
model: resolved.model,
|
||||
modelProvider: resolvedDisplayModel.provider,
|
||||
model: resolvedDisplayModel.model,
|
||||
agentRuntime,
|
||||
},
|
||||
};
|
||||
respond(true, result, undefined);
|
||||
|
||||
@@ -1391,7 +1391,11 @@ describe("gateway server sessions", () => {
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
};
|
||||
resolved?: { model?: string; modelProvider?: string };
|
||||
resolved?: {
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
agentRuntime?: { id: string; fallback?: string; source: string };
|
||||
};
|
||||
}>("sessions.patch", {
|
||||
key: "agent:main:main",
|
||||
model: "openai/gpt-test-a",
|
||||
@@ -1403,9 +1407,18 @@ describe("gateway server sessions", () => {
|
||||
expect(modelPatched.payload?.entry.modelProvider).toBeUndefined();
|
||||
expect(modelPatched.payload?.resolved?.modelProvider).toBe("openai");
|
||||
expect(modelPatched.payload?.resolved?.model).toBe("gpt-test-a");
|
||||
expect(modelPatched.payload?.resolved?.agentRuntime).toEqual({
|
||||
id: "pi",
|
||||
source: "implicit",
|
||||
});
|
||||
|
||||
const listAfterModelPatch = await directSessionReq<{
|
||||
sessions: Array<{ key: string; modelProvider?: string; model?: string }>;
|
||||
sessions: Array<{
|
||||
key: string;
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
agentRuntime?: { id: string; fallback?: string; source: string };
|
||||
}>;
|
||||
}>("sessions.list", {});
|
||||
expect(listAfterModelPatch.ok).toBe(true);
|
||||
const mainAfterModelPatch = listAfterModelPatch.payload?.sessions.find(
|
||||
@@ -1413,6 +1426,7 @@ describe("gateway server sessions", () => {
|
||||
);
|
||||
expect(mainAfterModelPatch?.modelProvider).toBe("openai");
|
||||
expect(mainAfterModelPatch?.model).toBe("gpt-test-a");
|
||||
expect(mainAfterModelPatch?.agentRuntime).toEqual({ id: "pi", source: "implicit" });
|
||||
|
||||
const compacted = await directSessionReq<{ ok: true; compacted: boolean }>("sessions.compact", {
|
||||
key: "agent:main:main",
|
||||
@@ -3723,7 +3737,11 @@ describe("gateway server sessions", () => {
|
||||
const patched = await rpcReq<{
|
||||
entry: { label?: string };
|
||||
key: string;
|
||||
resolved: { modelProvider: string; model: string };
|
||||
resolved: {
|
||||
modelProvider: string;
|
||||
model: string;
|
||||
agentRuntime: { id: string; fallback?: string; source: string };
|
||||
};
|
||||
}>(ws, "sessions.patch", {
|
||||
key: "agent:main:main",
|
||||
label: "cfg-isolation",
|
||||
@@ -3733,6 +3751,7 @@ describe("gateway server sessions", () => {
|
||||
expect(patched.payload?.resolved).toEqual({
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
agentRuntime: { id: "pi", source: "implicit" },
|
||||
});
|
||||
expect(patched.payload?.entry.label).toBe("cfg-isolation");
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
resolveDeletedAgentIdFromSessionKey,
|
||||
resolveGatewayModelSupportsImages,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionDisplayModelIdentityRef,
|
||||
resolveSessionModelIdentityRef,
|
||||
resolveSessionModelRef,
|
||||
resolveSessionStoreKey,
|
||||
@@ -57,12 +58,14 @@ function createSingleAgentAvatarConfig(workspace: string): OpenClawConfig {
|
||||
function createModelDefaultsConfig(params: {
|
||||
primary: string;
|
||||
models?: Record<string, Record<string, never>>;
|
||||
agentRuntime?: { id: string; fallback?: "pi" | "none" };
|
||||
}): OpenClawConfig {
|
||||
return {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: params.primary },
|
||||
models: params.models,
|
||||
agentRuntime: params.agentRuntime,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
@@ -1106,6 +1109,62 @@ describe("listSessionsFromStore selected model display", () => {
|
||||
expect(result.sessions[0]?.modelProvider).toBe("anthropic");
|
||||
expect(result.sessions[0]?.model).toBe("claude-opus-4-6");
|
||||
});
|
||||
|
||||
test("separates Claude CLI runtime metadata from canonical model identity", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "anthropic/claude-opus-4-7",
|
||||
agentRuntime: { id: "claude-cli", fallback: "none" },
|
||||
});
|
||||
|
||||
const result = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store: {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "claude-cli",
|
||||
model: "claude-opus-4-7",
|
||||
} as SessionEntry,
|
||||
},
|
||||
opts: {},
|
||||
});
|
||||
|
||||
expect(result.sessions[0]?.modelProvider).toBe("anthropic");
|
||||
expect(result.sessions[0]?.model).toBe("claude-opus-4-7");
|
||||
expect(result.sessions[0]?.agentRuntime).toEqual({
|
||||
id: "claude-cli",
|
||||
fallback: "none",
|
||||
source: "defaults",
|
||||
});
|
||||
});
|
||||
|
||||
test("infers canonical provider for bare CLI models before default-provider fallback", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "openai/gpt-5.4",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": {},
|
||||
},
|
||||
agentRuntime: { id: "claude-cli", fallback: "none" },
|
||||
});
|
||||
|
||||
const result = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store: {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "claude-cli",
|
||||
model: "claude-opus-4-7",
|
||||
} as SessionEntry,
|
||||
},
|
||||
opts: {},
|
||||
});
|
||||
|
||||
expect(result.sessions[0]?.modelProvider).toBe("anthropic");
|
||||
expect(result.sessions[0]?.model).toBe("claude-opus-4-7");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionModelIdentityRef", () => {
|
||||
@@ -1238,6 +1297,43 @@ describe("resolveSessionModelIdentityRef", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionDisplayModelIdentityRef", () => {
|
||||
test("canonicalizes CLI runtime provider to the selected model provider", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "anthropic/claude-opus-4-7",
|
||||
agentRuntime: { id: "claude-cli", fallback: "none" },
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveSessionDisplayModelIdentityRef({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
provider: "claude-cli",
|
||||
model: "claude-opus-4-7",
|
||||
}),
|
||||
).toEqual({ provider: "anthropic", model: "claude-opus-4-7" });
|
||||
});
|
||||
|
||||
test("prefers configured provider inference over default-provider parsing for bare CLI models", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "openai/gpt-5.4",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": {},
|
||||
},
|
||||
agentRuntime: { id: "claude-cli", fallback: "none" },
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveSessionDisplayModelIdentityRef({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
provider: "claude-cli",
|
||||
model: "claude-opus-4-7",
|
||||
}),
|
||||
).toEqual({ provider: "anthropic", model: "claude-opus-4-7" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveSessionTitle", () => {
|
||||
test("returns undefined for undefined entry", () => {
|
||||
expect(deriveSessionTitle(undefined)).toBeUndefined();
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "../agents/model-catalog.js";
|
||||
import {
|
||||
inferUniqueProviderFromConfiguredModels,
|
||||
isCliProvider,
|
||||
normalizeStoredOverrideModel,
|
||||
parseModelRef,
|
||||
resolveConfiguredModelRef,
|
||||
@@ -1249,6 +1250,45 @@ export function resolveSessionModelIdentityRef(
|
||||
return { provider: resolved.provider, model: resolved.model };
|
||||
}
|
||||
|
||||
export function resolveSessionDisplayModelIdentityRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}): { provider?: string; model?: string } {
|
||||
const provider = normalizeOptionalString(params.provider);
|
||||
const model = normalizeOptionalString(params.model);
|
||||
if (!provider || !model || !isCliProvider(provider, params.cfg)) {
|
||||
return { provider, model };
|
||||
}
|
||||
|
||||
const defaultRef = resolveDefaultModelForAgent({ cfg: params.cfg, agentId: params.agentId });
|
||||
if (model.includes("/")) {
|
||||
const parsedModel = parseModelRef(model, defaultRef.provider);
|
||||
if (parsedModel && !isCliProvider(parsedModel.provider, params.cfg)) {
|
||||
return parsedModel;
|
||||
}
|
||||
}
|
||||
|
||||
const inferredProvider = inferUniqueProviderFromConfiguredModels({
|
||||
cfg: params.cfg,
|
||||
model,
|
||||
});
|
||||
if (inferredProvider && !isCliProvider(inferredProvider, params.cfg)) {
|
||||
return { provider: inferredProvider, model };
|
||||
}
|
||||
|
||||
const parsedModel = parseModelRef(model, defaultRef.provider);
|
||||
if (parsedModel && !isCliProvider(parsedModel.provider, params.cfg)) {
|
||||
return parsedModel;
|
||||
}
|
||||
|
||||
return {
|
||||
provider: defaultRef.provider || provider,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildGatewaySessionRow(params: {
|
||||
cfg: OpenClawConfig;
|
||||
storePath: string;
|
||||
@@ -1395,11 +1435,22 @@ export function buildGatewaySessionRow(params: {
|
||||
: transcriptUsage?.totalTokensFresh === true;
|
||||
const childSessions = resolveChildSessionKeys(key, store, now);
|
||||
const latestCompactionCheckpoint = resolveLatestCompactionCheckpoint(entry);
|
||||
const agentRuntime = resolveAgentRuntimeMetadata(cfg, sessionAgentId);
|
||||
const selectedOrRuntimeModelProvider = selectedModel?.provider ?? modelProvider;
|
||||
const selectedOrRuntimeModel = selectedModel?.model ?? model;
|
||||
const rowModelIdentity = resolveSessionDisplayModelIdentityRef({
|
||||
cfg,
|
||||
agentId: sessionAgentId,
|
||||
provider: selectedOrRuntimeModelProvider,
|
||||
model: selectedOrRuntimeModel,
|
||||
});
|
||||
const rowModelProvider = rowModelIdentity.provider;
|
||||
const rowModel = rowModelIdentity.model;
|
||||
const estimatedCostUsd =
|
||||
resolveEstimatedSessionCostUsd({
|
||||
cfg,
|
||||
provider: modelProvider,
|
||||
model,
|
||||
provider: rowModelProvider,
|
||||
model: rowModel,
|
||||
entry,
|
||||
}) ?? resolveNonNegativeNumber(transcriptUsage?.estimatedCostUsd);
|
||||
const contextTokens =
|
||||
@@ -1408,8 +1459,8 @@ export function buildGatewaySessionRow(params: {
|
||||
resolvePositiveNumber(
|
||||
resolveContextTokensForModel({
|
||||
cfg,
|
||||
provider: modelProvider,
|
||||
model,
|
||||
provider: rowModelProvider,
|
||||
model: rowModel,
|
||||
// Gateway/session listing is read-only; don't start async model discovery.
|
||||
allowAsyncLoad: false,
|
||||
}),
|
||||
@@ -1432,8 +1483,6 @@ export function buildGatewaySessionRow(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const rowModelProvider = selectedModel?.provider ?? modelProvider;
|
||||
const rowModel = selectedModel?.model ?? model;
|
||||
const thinkingProvider = rowModelProvider ?? DEFAULT_PROVIDER;
|
||||
const thinkingModel = rowModel ?? DEFAULT_MODEL;
|
||||
const thinkingLevels = listThinkingLevelOptions(
|
||||
@@ -1500,6 +1549,7 @@ export function buildGatewaySessionRow(params: {
|
||||
responseUsage: entry?.responseUsage,
|
||||
modelProvider: rowModelProvider,
|
||||
model: rowModel,
|
||||
agentRuntime,
|
||||
contextTokens,
|
||||
deliveryContext: deliveryFields.deliveryContext,
|
||||
lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ChatType } from "../channels/chat-type.js";
|
||||
import type { SessionCompactionCheckpoint, SessionEntry } from "../config/sessions/types.js";
|
||||
import type { PluginSessionExtensionProjection } from "../plugins/host-hooks.js";
|
||||
import type {
|
||||
GatewayAgentRuntime,
|
||||
GatewayAgentRow as SharedGatewayAgentRow,
|
||||
SessionsListResultBase,
|
||||
SessionsPatchResultBase,
|
||||
@@ -75,6 +76,7 @@ export type GatewaySessionRow = {
|
||||
responseUsage?: "on" | "off" | "tokens" | "full";
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
agentRuntime?: GatewayAgentRuntime;
|
||||
contextTokens?: number;
|
||||
deliveryContext?: DeliveryContext;
|
||||
lastChannel?: SessionEntry["lastChannel"];
|
||||
@@ -111,5 +113,6 @@ export type SessionsPatchResult = SessionsPatchResultBase<SessionEntry> & {
|
||||
resolved?: {
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
agentRuntime?: GatewayAgentRuntime;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ export type UpdateAvailable = import("../../../src/infra/update-startup.js").Upd
|
||||
import type { CronJobBase } from "../../../src/cron/types-shared.js";
|
||||
import type { ConfigUiHints } from "../../../src/shared/config-ui-hints-types.js";
|
||||
import type {
|
||||
GatewayAgentRuntime,
|
||||
GatewayAgentRow as SharedGatewayAgentRow,
|
||||
SessionsListResultBase,
|
||||
SessionsPatchResultBase,
|
||||
@@ -443,6 +444,7 @@ export type GatewaySessionRow = {
|
||||
childSessions?: string[];
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
agentRuntime?: GatewayAgentRuntime;
|
||||
contextTokens?: number;
|
||||
compactionCheckpointCount?: number;
|
||||
latestCompactionCheckpoint?: SessionCompactionCheckpoint;
|
||||
@@ -497,6 +499,7 @@ export type SessionsPatchResult = SessionsPatchResultBase<{
|
||||
resolved?: {
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
agentRuntime?: GatewayAgentRuntime;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user