mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 16:34:45 +00:00
fix(status): treat CLI runtime aliases as selected route
This commit is contained in:
committed by
Peter Steinberger
parent
fa0506bd31
commit
2bdbec8246
@@ -286,6 +286,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Installer: when npm installs `openclaw` outside the parent shell PATH, print follow-up commands with the resolved binary path instead of telling users to run `openclaw` from a shell that will report `command not found`. Fixes #72382. Thanks @jbob762.
|
||||
- Plugins/runtime: share MIME and JSON Schema helpers across bundled plugins while preserving canonical media MIME inference, browser URL wildcard semantics, migration home-path resolution, QA request-limit responses, and extensionless text file previews.
|
||||
- Agents/memory flush: persist the pre-increment compaction counter after flush-triggered compaction so consecutive eligible compaction cycles run memoryFlush instead of alternating. Fixes #12590. Refs #12760, #26145, and #46513. Thanks @Kaspre, @lailoo, @drvoss, @Br1an67, and @dial481.
|
||||
- Status: treat CLI runtime aliases such as `claude-cli/<model>` as the canonical selected provider route in `/status`, avoiding spurious fallback/unknown-auth display and preserving fresh context usage from CLI usage snapshots. Fixes #79015. Thanks @ItsThierry.
|
||||
- Compute plugin callback authorization dynamically [AI]. (#78866) Thanks @pgondhi987.
|
||||
- Gateway/auth: allow `gateway.auth.mode: "none"` loopback backend RPC clients to skip device identity only for local non-browser backend connections, restoring subagent spawns and gateway tools without opening remote or browser-origin bypasses. Fixes #75780. Thanks @yozakura-ava.
|
||||
- Canvas plugin: keep legacy root `canvasHost` configs valid until `openclaw doctor --fix` migrates them into `plugins.entries.canvas.config.host`, move Canvas/A2UI clients to gateway protocol v4 plugin surfaces, and refresh the generated A2UI bundle hash so normal builds stay clean.
|
||||
|
||||
@@ -97,6 +97,28 @@ export function isCliRuntimeAlias(runtime: string | undefined): boolean {
|
||||
return normalized ? CLI_RUNTIME_ALIASES.has(normalizeProviderId(normalized)) : false;
|
||||
}
|
||||
|
||||
function canonicalizeRuntimeAliasProvider(provider: string): string {
|
||||
return resolveLegacyRuntimeModelProviderAlias(provider)?.provider ?? provider;
|
||||
}
|
||||
|
||||
function normalizeRuntimeModelRefForComparison(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
const slash = trimmed.indexOf("/");
|
||||
if (slash <= 0 || slash >= trimmed.length - 1) {
|
||||
return normalizeProviderId(canonicalizeRuntimeAliasProvider(trimmed));
|
||||
}
|
||||
const provider = trimmed.slice(0, slash).trim();
|
||||
const model = trimmed.slice(slash + 1).trim();
|
||||
const canonicalProvider = normalizeProviderId(canonicalizeRuntimeAliasProvider(provider));
|
||||
return model ? `${canonicalProvider}/${model}` : canonicalProvider;
|
||||
}
|
||||
|
||||
export function areRuntimeModelRefsEquivalent(left: string, right: string): boolean {
|
||||
return (
|
||||
normalizeRuntimeModelRefForComparison(left) === normalizeRuntimeModelRefForComparison(right)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveConfiguredRuntime(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildFallbackNotice,
|
||||
resolveActiveFallbackState,
|
||||
resolveFallbackTransition,
|
||||
type FallbackNoticeState,
|
||||
@@ -120,4 +121,37 @@ describe("fallback-state", () => {
|
||||
expect(resolved.nextState.activeModel).toBeUndefined();
|
||||
expect(resolved.nextState.reason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not treat a CLI runtime alias as a model fallback", () => {
|
||||
const resolved = resolveFallbackTransition({
|
||||
selectedProvider: "anthropic",
|
||||
selectedModel: "claude-opus-4-7",
|
||||
activeProvider: "claude-cli",
|
||||
activeModel: "claude-opus-4-7",
|
||||
attempts: [],
|
||||
state: {
|
||||
fallbackNoticeSelectedModel: "anthropic/claude-opus-4-7",
|
||||
fallbackNoticeActiveModel: "claude-cli/claude-opus-4-7",
|
||||
fallbackNoticeReason: "selected model unavailable",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.fallbackActive).toBe(false);
|
||||
expect(resolved.fallbackCleared).toBe(false);
|
||||
expect(resolved.stateChanged).toBe(true);
|
||||
expect(resolved.nextState.selectedModel).toBeUndefined();
|
||||
expect(resolved.nextState.activeModel).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not build a fallback notice for equivalent CLI runtime aliases", () => {
|
||||
expect(
|
||||
buildFallbackNotice({
|
||||
selectedProvider: "anthropic",
|
||||
selectedModel: "claude-opus-4-7",
|
||||
activeProvider: "claude-cli",
|
||||
activeModel: "claude-opus-4-7",
|
||||
attempts: [],
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js";
|
||||
import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import type { FallbackNoticeState } from "../status/fallback-notice-state.js";
|
||||
@@ -96,7 +97,7 @@ export function buildFallbackNotice(params: {
|
||||
}): string | null {
|
||||
const selected = formatProviderModelRef(params.selectedProvider, params.selectedModel);
|
||||
const active = formatProviderModelRef(params.activeProvider, params.activeModel);
|
||||
if (selected === active) {
|
||||
if (areRuntimeModelRefsEquivalent(selected, active)) {
|
||||
return null;
|
||||
}
|
||||
const reasonSummary = buildFallbackReasonSummary(params.attempts);
|
||||
@@ -152,13 +153,17 @@ export function resolveFallbackTransition(params: {
|
||||
activeModel: normalizeOptionalString(params.state?.fallbackNoticeActiveModel),
|
||||
reason: normalizeOptionalString(params.state?.fallbackNoticeReason),
|
||||
};
|
||||
const fallbackActive = selectedModelRef !== activeModelRef;
|
||||
const fallbackActive = !areRuntimeModelRefsEquivalent(selectedModelRef, activeModelRef);
|
||||
const fallbackTransitioned =
|
||||
fallbackActive &&
|
||||
(previousState.selectedModel !== selectedModelRef ||
|
||||
previousState.activeModel !== activeModelRef);
|
||||
const fallbackCleared =
|
||||
!fallbackActive && Boolean(previousState.selectedModel || previousState.activeModel);
|
||||
const previousStateWasRealFallback = Boolean(
|
||||
previousState.selectedModel &&
|
||||
previousState.activeModel &&
|
||||
!areRuntimeModelRefsEquivalent(previousState.selectedModel, previousState.activeModel),
|
||||
);
|
||||
const fallbackCleared = !fallbackActive && previousStateWasRealFallback;
|
||||
const reasonSummary = buildFallbackReasonSummary(params.attempts);
|
||||
const attemptSummaries = buildFallbackAttemptSummaries(params.attempts);
|
||||
const nextState = fallbackActive
|
||||
|
||||
@@ -1163,6 +1163,62 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not persist fallback state for an equivalent CLI runtime alias", async () => {
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
fallbackNoticeSelectedModel: "anthropic/claude-opus-4-7",
|
||||
fallbackNoticeActiveModel: "claude-cli/claude-opus-4-7",
|
||||
fallbackNoticeReason: "selected model unavailable",
|
||||
};
|
||||
const sessionStore = { main: sessionEntry };
|
||||
const dir = await mkdtemp(join(tmpdir(), "openclaw-agent-runner-cli-alias-"));
|
||||
const storePath = join(dir, "sessions.json");
|
||||
await writeFile(storePath, JSON.stringify({ main: sessionEntry }), "utf8");
|
||||
|
||||
state.runEmbeddedPiAgentMock.mockResolvedValue({
|
||||
payloads: [{ text: "final" }],
|
||||
meta: {
|
||||
agentMeta: {
|
||||
provider: "claude-cli",
|
||||
model: "claude-opus-4-7",
|
||||
usage: { input: 36_000, output: 19_000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { run } = createMinimalRun({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "main",
|
||||
storePath,
|
||||
runOverrides: {
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-7",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"claude-cli": { command: "claude" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await run();
|
||||
|
||||
const stored = JSON.parse(await readFile(storePath, "utf8")).main as SessionEntry;
|
||||
expect(sessionEntry.fallbackNoticeSelectedModel).toBeUndefined();
|
||||
expect(sessionEntry.fallbackNoticeActiveModel).toBeUndefined();
|
||||
expect(stored.fallbackNoticeSelectedModel).toBeUndefined();
|
||||
expect(stored.fallbackNoticeActiveModel).toBeUndefined();
|
||||
expect(stored.modelProvider).toBe("claude-cli");
|
||||
expect(stored.model).toBe("claude-opus-4-7");
|
||||
expect(stored.totalTokens).toBe(36_000);
|
||||
expect(stored.totalTokensFresh).toBe(true);
|
||||
});
|
||||
|
||||
it("surfaces overflow fallback when embedded run returns empty payloads", async () => {
|
||||
state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({
|
||||
payloads: [],
|
||||
|
||||
@@ -1416,10 +1416,11 @@ export async function runReplyAgent(params: {
|
||||
});
|
||||
}
|
||||
}
|
||||
const cliSessionId = isCliProvider(providerUsed, cfg)
|
||||
const usedCliProvider = isCliProvider(providerUsed, cfg);
|
||||
const cliSessionId = usedCliProvider
|
||||
? normalizeOptionalString(runResult.meta?.agentMeta?.sessionId)
|
||||
: undefined;
|
||||
const cliSessionBinding = isCliProvider(providerUsed, cfg)
|
||||
const cliSessionBinding = usedCliProvider
|
||||
? runResult.meta?.agentMeta?.cliSessionBinding
|
||||
: undefined;
|
||||
const runtimeContextTokens =
|
||||
@@ -1447,6 +1448,7 @@ export async function runReplyAgent(params: {
|
||||
usage,
|
||||
lastCallUsage: runResult.meta?.agentMeta?.lastCallUsage,
|
||||
promptTokens,
|
||||
usageIsContextSnapshot: usedCliProvider ? true : undefined,
|
||||
modelUsed,
|
||||
providerUsed,
|
||||
contextTokensUsed,
|
||||
|
||||
@@ -774,6 +774,43 @@ describe("buildStatusMessage", () => {
|
||||
expect(normalized).not.toContain("Context: 49k/1.0m");
|
||||
});
|
||||
|
||||
it("renders CLI runtime aliases as the selected model route", () => {
|
||||
const text = buildStatusMessage({
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
},
|
||||
sessionEntry: {
|
||||
sessionId: "claude-cli-runtime-alias",
|
||||
updatedAt: 0,
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-opus-4-7",
|
||||
modelProvider: "claude-cli",
|
||||
model: "claude-opus-4-7",
|
||||
fallbackNoticeSelectedModel: "anthropic/claude-opus-4-7",
|
||||
fallbackNoticeActiveModel: "claude-cli/claude-opus-4-7",
|
||||
fallbackNoticeReason: "selected model unavailable",
|
||||
inputTokens: 29,
|
||||
outputTokens: 19_000,
|
||||
cacheRead: 3_000_000,
|
||||
totalTokens: 36_000,
|
||||
totalTokensFresh: true,
|
||||
contextTokens: 1_000_000,
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
modelAuth: "unknown",
|
||||
activeModelAuth: "oauth (anthropic:claude-cli)",
|
||||
});
|
||||
|
||||
const normalized = normalizeTestText(text);
|
||||
expect(normalized).toContain("Model: anthropic/claude-opus-4-7");
|
||||
expect(normalized).toContain("oauth (anthropic:claude-cli)");
|
||||
expect(normalized).not.toContain("Fallback: claude-cli/claude-opus-4-7");
|
||||
expect(normalized).not.toContain("unknown");
|
||||
expect(normalized).toContain("Context: 36k/1.0m (4%)");
|
||||
});
|
||||
|
||||
it("keeps an explicit runtime context limit when fallback status already computed one", () => {
|
||||
const text = buildStatusMessage({
|
||||
config: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
|
||||
@@ -15,7 +16,7 @@ export function resolveActiveFallbackState(params: {
|
||||
const active = normalizeOptionalString(params.state?.fallbackNoticeActiveModel);
|
||||
const reason = normalizeOptionalString(params.state?.fallbackNoticeReason);
|
||||
const fallbackActive =
|
||||
params.selectedModelRef !== params.activeModelRef &&
|
||||
!areRuntimeModelRefsEquivalent(params.selectedModelRef, params.activeModelRef) &&
|
||||
selected === params.selectedModelRef &&
|
||||
active === params.activeModelRef;
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import { resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveModelAuthMode } from "../agents/model-auth.js";
|
||||
import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
resolveConfiguredModelRef,
|
||||
@@ -849,17 +850,25 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
];
|
||||
const activationLine = activationParts.filter(Boolean).join(" · ");
|
||||
|
||||
const selectedModelLabel = modelRefs.selected.label || "unknown";
|
||||
const runtimeAliasModelEquivalent = areRuntimeModelRefsEquivalent(
|
||||
selectedModelLabel,
|
||||
activeModelLabel,
|
||||
);
|
||||
const selectedAuthMode =
|
||||
normalizeAuthMode(args.modelAuth) ?? resolveModelAuthMode(selectedProvider, args.config);
|
||||
const selectedAuthLabelValue =
|
||||
args.modelAuth ??
|
||||
(selectedAuthMode && selectedAuthMode !== "unknown" ? selectedAuthMode : undefined);
|
||||
const rawSelectedAuthLabelValue =
|
||||
selectedAuthMode && selectedAuthMode !== "unknown"
|
||||
? (args.modelAuth ?? selectedAuthMode)
|
||||
: undefined;
|
||||
const activeAuthMode =
|
||||
normalizeAuthMode(args.activeModelAuth) ?? resolveModelAuthMode(activeProvider, args.config);
|
||||
const activeAuthLabelValue =
|
||||
args.activeModelAuth ??
|
||||
(activeAuthMode && activeAuthMode !== "unknown" ? activeAuthMode : undefined);
|
||||
const selectedModelLabel = modelRefs.selected.label || "unknown";
|
||||
activeAuthMode && activeAuthMode !== "unknown"
|
||||
? (args.activeModelAuth ?? activeAuthMode)
|
||||
: undefined;
|
||||
const selectedAuthLabelValue =
|
||||
rawSelectedAuthLabelValue ?? (runtimeAliasModelEquivalent ? activeAuthLabelValue : undefined);
|
||||
const fallbackState = resolveActiveFallbackState({
|
||||
selectedModelRef: selectedModelLabel,
|
||||
activeModelRef: activeModelLabel,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { resolveFastModeState } from "../agents/fast-mode.js";
|
||||
import { resolveModelAuthLabel } from "../agents/model-auth-label.js";
|
||||
import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js";
|
||||
import {
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
@@ -238,7 +239,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
provider: activeProvider,
|
||||
effectiveHarness,
|
||||
});
|
||||
const selectedModelAuth = Object.hasOwn(params, "modelAuthOverride")
|
||||
let selectedModelAuth = Object.hasOwn(params, "modelAuthOverride")
|
||||
? params.modelAuthOverride
|
||||
: resolveModelAuthLabel({
|
||||
provider: selectedStatusProvider,
|
||||
@@ -260,6 +261,18 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
includeExternalProfiles: false,
|
||||
})
|
||||
: selectedModelAuth;
|
||||
const runtimeAliasModelEquivalent = areRuntimeModelRefsEquivalent(
|
||||
modelRefs.selected.label,
|
||||
modelRefs.active.label,
|
||||
);
|
||||
if (
|
||||
runtimeAliasModelEquivalent &&
|
||||
normalizeOptionalLowercaseString(selectedModelAuth) === "unknown" &&
|
||||
activeModelAuth &&
|
||||
normalizeOptionalLowercaseString(activeModelAuth) !== "unknown"
|
||||
) {
|
||||
selectedModelAuth = activeModelAuth;
|
||||
}
|
||||
const usageAuthLabel = modelRefs.activeDiffers ? activeModelAuth : selectedModelAuth;
|
||||
const currentUsageProvider =
|
||||
resolveUsageProviderId(activeStatusProvider) ?? resolveUsageProviderId(activeProvider);
|
||||
|
||||
Reference in New Issue
Block a user