From 8d78451e8b4f0e10509e132773005882a40bafee Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 06:07:03 +0100 Subject: [PATCH] fix: clarify session runtime metadata --- CHANGELOG.md | 1 + docs/gateway/protocol.md | 4 +- src/commands/sessions-display-model.ts | 61 ++++++++++-- .../sessions.model-resolution.test.ts | 76 ++++++++++++++- src/commands/sessions.test-helpers.ts | 12 ++- src/commands/sessions.ts | 22 +++-- src/gateway/server-methods/sessions.ts | 14 ++- ...sessions.gateway-server-sessions-a.test.ts | 25 ++++- src/gateway/session-utils.test.ts | 96 +++++++++++++++++++ src/gateway/session-utils.ts | 62 ++++++++++-- src/gateway/session-utils.types.ts | 3 + ui/src/ui/types.ts | 3 + 12 files changed, 348 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c43920ad42..c47643d99fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index d863a6ad39a..aab45284835 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -387,7 +387,7 @@ enumeration of `src/gateway/server-methods/*.ts`. - - `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 `...`, `...`, `...`, `...`, 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. diff --git a/src/commands/sessions-display-model.ts b/src/commands/sessions-display-model.ts index da369ce4d71..c8703dcb2e4 100644 --- a/src/commands/sessions-display-model.ts +++ b/src/commands/sessions-display-model.ts @@ -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; } diff --git a/src/commands/sessions.model-resolution.test.ts b/src/commands/sessions.model-resolution.test.ts index cbb1ae0d41a..d689230bc69 100644 --- a/src/commands/sessions.model-resolution.test.ts +++ b/src/commands/sessions.model-resolution.test.ts @@ -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(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(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"); + }); }); diff --git a/src/commands/sessions.test-helpers.ts b/src/commands/sessions.test-helpers.ts index 65e47390597..bfdd4e33a22 100644 --- a/src/commands/sessions.test-helpers.ts +++ b/src/commands/sessions.test-helpers.ts @@ -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 }>(() => ({ 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) { + sessionsConfigState.loadConfig = loader; +} + +export function resetMockSessionsConfig() { + sessionsConfigState.loadConfig = defaultSessionsConfigLoader; +} + export function makeRuntime(params?: { throwOnError?: boolean }): { runtime: RuntimeEnv; logs: string[]; diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index dfb96a8974b..c2b9cdb87d7 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -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; }; 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, }; }), ), diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 9dd7a636534..dcf1ade212f 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -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); diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index c32c3e6f463..4f57498c7ca 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -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"); diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 342b0d979e5..4a30e6f3445 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -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>; + 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(); diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 787f7336b5c..62bcfc38d24 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -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, diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts index 530250bdd2d..ee9e9f75975 100644 --- a/src/gateway/session-utils.types.ts +++ b/src/gateway/session-utils.types.ts @@ -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 & { resolved?: { modelProvider?: string; model?: string; + agentRuntime?: GatewayAgentRuntime; }; }; diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 2447828e5b1..51f32e8cef1 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -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; }; };