mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix: child sessions honor inherited model pins over channel defaults (#125880)
* fix: honor inherited model pins over channel defaults Child sessions now keep parent model pins consistently across reply, status, harness prediction, and agentCommand. * test: align agent command model selection fixtures Keep concurrent override and threaded parent-selection coverage aligned with canonical model normalization and runtime snapshot access.
This commit is contained in:
committed by
GitHub
parent
e68e973c05
commit
e011e9db61
@@ -378,6 +378,7 @@ vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../config/runtime-snapshot.js", () => ({
|
||||
getRuntimeConfigSnapshot: () => state.runtimeConfigMock ?? state.defaultRuntimeConfig,
|
||||
registerRuntimeConfigSnapshotPreparer: vi.fn(),
|
||||
setRuntimeConfigSnapshot: vi.fn(),
|
||||
}));
|
||||
@@ -2039,7 +2040,7 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
models: {
|
||||
"anthropic/default-model": {},
|
||||
"anthropic/stale-fallback-model": {},
|
||||
"google/gemini-3-pro": {},
|
||||
"google/user-model": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2064,17 +2065,17 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
...sessionEntry,
|
||||
updatedAt: 2,
|
||||
providerOverride: "google",
|
||||
modelOverride: "gemini-3-pro",
|
||||
modelOverride: "user-model",
|
||||
modelOverrideSource: "user",
|
||||
};
|
||||
});
|
||||
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("google", "gemini-3-pro"));
|
||||
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("google", "user-model"));
|
||||
|
||||
await runBasicAgentCommand();
|
||||
|
||||
const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
|
||||
expect(fallbackParams.provider).toBe("google");
|
||||
expect(fallbackParams.model).toBe("gemini-3-pro");
|
||||
expect(fallbackParams.model).toBe("user-model");
|
||||
});
|
||||
|
||||
it("probes the channel primary when a session is pinned to an auto fallback", async () => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type ThinkLevel,
|
||||
} from "../../auto-reply/thinking.js";
|
||||
import { resolveChannelModelOverride } from "../../channels/model-overrides.js";
|
||||
import { resolveSessionModelOverrideRouteResolution } from "../../config/sessions/model-override-provenance.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { requireActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
isModelSelectionLocked,
|
||||
repairProviderWrappedModelOverride,
|
||||
} from "../../sessions/model-overrides.js";
|
||||
import { resolveStoredModelOverride } from "../../sessions/stored-model-overrides.js";
|
||||
import {
|
||||
sessionDeliveryChannel,
|
||||
sessionDeliveryOrigin,
|
||||
@@ -116,9 +116,6 @@ export async function resolveEmbeddedModelSelection(params: {
|
||||
let sessionEntry = params.sessionEntry;
|
||||
const hasStoredOverride = Boolean(sessionEntry?.modelOverride || sessionEntry?.providerOverride);
|
||||
let storedModelOverrideSource = hasStoredOverride ? sessionEntry?.modelOverrideSource : undefined;
|
||||
let storedModelOverrideRouteResolution = hasStoredOverride
|
||||
? resolveSessionModelOverrideRouteResolution(sessionEntry)
|
||||
: undefined;
|
||||
let hasStoredAutoFallbackProvenance =
|
||||
hasStoredOverride && hasSessionAutoModelFallbackProvenance(sessionEntry);
|
||||
let hasLegacyAutoFallbackOverrideWithoutOrigin =
|
||||
@@ -232,9 +229,6 @@ export async function resolveEmbeddedModelSelection(params: {
|
||||
storedModelOverrideSource = adoptedHasStoredOverride
|
||||
? sessionEntry?.modelOverrideSource
|
||||
: undefined;
|
||||
storedModelOverrideRouteResolution = adoptedHasStoredOverride
|
||||
? resolveSessionModelOverrideRouteResolution(sessionEntry)
|
||||
: undefined;
|
||||
hasStoredAutoFallbackProvenance =
|
||||
adoptedHasStoredOverride && hasSessionAutoModelFallbackProvenance(sessionEntry);
|
||||
hasLegacyAutoFallbackOverrideWithoutOrigin =
|
||||
@@ -246,12 +240,26 @@ export async function resolveEmbeddedModelSelection(params: {
|
||||
hasLegacyAutoFallbackOverrideWithoutOrigin = false;
|
||||
}
|
||||
|
||||
const effectiveStoredOverride = hasLegacyAutoFallbackOverrideWithoutOrigin
|
||||
? null
|
||||
: resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
sessionStore: params.sessionStore,
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: sessionEntry?.parentSessionKey,
|
||||
defaultProvider,
|
||||
});
|
||||
if (effectiveStoredOverride?.source === "parent") {
|
||||
storedModelOverrideSource = undefined;
|
||||
hasStoredAutoFallbackProvenance = false;
|
||||
}
|
||||
const storedProviderOverride = hasLegacyAutoFallbackOverrideWithoutOrigin
|
||||
? undefined
|
||||
: sessionEntry?.providerOverride?.trim();
|
||||
: (effectiveStoredOverride?.provider ?? sessionEntry?.providerOverride?.trim());
|
||||
const storedModelOverride = hasLegacyAutoFallbackOverrideWithoutOrigin
|
||||
? undefined
|
||||
: sessionEntry?.modelOverride?.trim();
|
||||
: (effectiveStoredOverride?.model ?? sessionEntry?.modelOverride?.trim());
|
||||
const storedModelOverrideRouteResolution = effectiveStoredOverride?.routeResolution;
|
||||
const currentRunModelChannel = [
|
||||
params.runContext.messageChannel,
|
||||
params.opts.replyChannel,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Persisted model metadata normalization without loading the broader selection runtime.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import type { ModelRef } from "./model-ref-shared.js";
|
||||
import { parseModelRef } from "./model-selection-normalize.js";
|
||||
|
||||
function normalizePersistedDefaultProvider(value: unknown): string {
|
||||
return normalizeOptionalString(value) ?? DEFAULT_PROVIDER;
|
||||
}
|
||||
|
||||
export function resolvePersistedOverrideModelRef(params: {
|
||||
defaultProvider?: unknown;
|
||||
overrideProvider?: unknown;
|
||||
overrideModel?: unknown;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const defaultProvider = normalizePersistedDefaultProvider(params.defaultProvider);
|
||||
const overrideProvider = normalizeOptionalString(params.overrideProvider);
|
||||
const overrideModel = normalizeOptionalString(params.overrideModel);
|
||||
if (!overrideModel) {
|
||||
return null;
|
||||
}
|
||||
const encodedOverride = overrideProvider ? `${overrideProvider}/${overrideModel}` : overrideModel;
|
||||
return (
|
||||
parseModelRef(encodedOverride, defaultProvider, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
}) ?? {
|
||||
provider: overrideProvider || defaultProvider,
|
||||
model: overrideModel,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeStoredOverrideModel(params: {
|
||||
providerOverride?: unknown;
|
||||
modelOverride?: unknown;
|
||||
}): { providerOverride?: string; modelOverride?: string } {
|
||||
const providerOverride = normalizeOptionalString(params.providerOverride);
|
||||
const modelOverride = normalizeOptionalString(params.modelOverride);
|
||||
if (!providerOverride || !modelOverride) {
|
||||
return {
|
||||
providerOverride,
|
||||
modelOverride,
|
||||
};
|
||||
}
|
||||
|
||||
const providerPrefix = `${providerOverride.toLowerCase()}/`;
|
||||
return {
|
||||
providerOverride,
|
||||
modelOverride: modelOverride.toLowerCase().startsWith(providerPrefix)
|
||||
? modelOverride.slice(providerOverride.length + 1).trim() || modelOverride
|
||||
: modelOverride,
|
||||
};
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
resolveSubagentConfiguredModelSelection,
|
||||
} from "./model-selection-config.js";
|
||||
import { findNormalizedProviderValue, parseModelRef } from "./model-selection-normalize.js";
|
||||
import { resolvePersistedOverrideModelRef } from "./model-selection-persisted.js";
|
||||
import {
|
||||
resolveAllowedModelRefCore as resolveAllowedModelRefInternal,
|
||||
resolveConfiguredModelFallbacks,
|
||||
@@ -52,6 +53,11 @@ export type { ModelAliasIndex, ModelManifestNormalizationContext, ModelRef };
|
||||
|
||||
export { resolveDefaultModelForAgent, resolveSubagentConfiguredModelSelection };
|
||||
|
||||
export {
|
||||
normalizeStoredOverrideModel,
|
||||
resolvePersistedOverrideModelRef,
|
||||
} from "./model-selection-persisted.js";
|
||||
|
||||
export {
|
||||
buildConfiguredModelCatalog,
|
||||
buildModelAliasIndex,
|
||||
@@ -83,31 +89,6 @@ function normalizePersistedDefaultProvider(value: unknown): string {
|
||||
return normalizeOptionalString(value) ?? DEFAULT_PROVIDER;
|
||||
}
|
||||
|
||||
export function resolvePersistedOverrideModelRef(params: {
|
||||
defaultProvider?: unknown;
|
||||
overrideProvider?: unknown;
|
||||
overrideModel?: unknown;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const defaultProvider = normalizePersistedDefaultProvider(params.defaultProvider);
|
||||
const overrideProvider = normalizeOptionalString(params.overrideProvider);
|
||||
const overrideModel = normalizeOptionalString(params.overrideModel);
|
||||
if (!overrideModel) {
|
||||
return null;
|
||||
}
|
||||
const encodedOverride = overrideProvider ? `${overrideProvider}/${overrideModel}` : overrideModel;
|
||||
return (
|
||||
parseModelRef(encodedOverride, defaultProvider, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
}) ?? {
|
||||
provider: overrideProvider || defaultProvider,
|
||||
model: overrideModel,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime-first resolver for persisted model metadata.
|
||||
* Use this when callers intentionally want the last executed model identity.
|
||||
@@ -180,28 +161,6 @@ export function resolvePersistedSelectedModelRef(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeStoredOverrideModel(params: {
|
||||
providerOverride?: unknown;
|
||||
modelOverride?: unknown;
|
||||
}): { providerOverride?: string; modelOverride?: string } {
|
||||
const providerOverride = normalizeOptionalString(params.providerOverride);
|
||||
const modelOverride = normalizeOptionalString(params.modelOverride);
|
||||
if (!providerOverride || !modelOverride) {
|
||||
return {
|
||||
providerOverride,
|
||||
modelOverride,
|
||||
};
|
||||
}
|
||||
|
||||
const providerPrefix = `${providerOverride.toLowerCase()}/`;
|
||||
return {
|
||||
providerOverride,
|
||||
modelOverride: modelOverride.toLowerCase().startsWith(providerPrefix)
|
||||
? modelOverride.slice(providerOverride.length + 1).trim() || modelOverride
|
||||
: modelOverride,
|
||||
};
|
||||
}
|
||||
|
||||
export async function canonicalizeCaseOnlyCatalogModelRef(params: {
|
||||
raw: string | undefined;
|
||||
cfg?: OpenClawConfig;
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { resolveStoredModelOverride } from "../../sessions/stored-model-overrides.js";
|
||||
import {
|
||||
sessionDeliveryChannel,
|
||||
sessionDeliveryOrigin,
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
loadSessionStoreEntry,
|
||||
resolveSessionStorePathCore,
|
||||
} from "./dispatch-from-config.runtime.js";
|
||||
import { resolveStoredModelOverride } from "./stored-model-override.js";
|
||||
|
||||
type HarnessSourceVisibleRepliesDefault = "automatic" | "message_tool";
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { resolveChannelModelOverride } from "../../channels/model-overrides.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { isModelSelectionLocked } from "../../sessions/model-overrides.js";
|
||||
import { recordSessionCreated } from "../../sessions/session-state-events.js";
|
||||
import { resolveStoredModelOverride } from "../../sessions/stored-model-overrides.js";
|
||||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import type { SkillCommandSpec } from "../../skills/types.js";
|
||||
import {
|
||||
@@ -202,13 +203,27 @@ export async function maybeResolveNativeSlashCommandFastReply(params: {
|
||||
if (command.commandBodyNormalized === "/status") {
|
||||
const targetSessionEntry =
|
||||
sessionState.sessionStore[sessionState.sessionKey] ?? sessionState.sessionEntry;
|
||||
const canApplyStoredModel =
|
||||
params.provider === params.defaultProvider && params.model === params.defaultModel;
|
||||
const storedModelOverride = canApplyStoredModel
|
||||
? resolveStoredModelOverride({
|
||||
sessionEntry: targetSessionEntry,
|
||||
sessionStore: sessionState.sessionStore,
|
||||
sessionKey: sessionState.sessionKey,
|
||||
parentSessionKey:
|
||||
targetSessionEntry?.parentSessionKey ??
|
||||
params.ctx.ModelParentSessionKey ??
|
||||
params.ctx.ParentSessionKey,
|
||||
defaultProvider: params.defaultProvider,
|
||||
})
|
||||
: null;
|
||||
const canApplyChannelModel =
|
||||
params.cfg.channels?.modelByChannel &&
|
||||
!isModelSelectionLocked(targetSessionEntry) &&
|
||||
!storedModelOverride &&
|
||||
!normalizeOptionalString(targetSessionEntry?.modelOverride) &&
|
||||
!normalizeOptionalString(targetSessionEntry?.providerOverride) &&
|
||||
params.provider === params.defaultProvider &&
|
||||
params.model === params.defaultModel;
|
||||
canApplyStoredModel;
|
||||
const deliveryChannel = normalizeMessageChannel(sessionDeliveryChannel(targetSessionEntry));
|
||||
// Shared sessions can retain another channel's peer; never let that stale
|
||||
// identity outrank the authorized current command's live sender.
|
||||
@@ -245,10 +260,23 @@ export async function maybeResolveNativeSlashCommandFastReply(params: {
|
||||
aliasIndex: params.aliasIndex,
|
||||
})
|
||||
: null;
|
||||
const resolvedInheritedModel =
|
||||
storedModelOverride?.source === "parent"
|
||||
? (resolveModelRefFromString({
|
||||
raw: `${storedModelOverride.provider ?? params.defaultProvider}/${storedModelOverride.model}`,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex: params.aliasIndex,
|
||||
})?.ref ?? {
|
||||
provider: storedModelOverride.provider ?? params.defaultProvider,
|
||||
model: storedModelOverride.model,
|
||||
})
|
||||
: null;
|
||||
// Native status returns before normal channel routing; select once before
|
||||
// preparing model-bound thinking, runtime, auth, context, or fast-mode facts.
|
||||
const statusProvider = resolvedChannelModel?.ref.provider ?? params.provider;
|
||||
const statusModel = resolvedChannelModel?.ref.model ?? params.model;
|
||||
const statusProvider =
|
||||
resolvedInheritedModel?.provider ?? resolvedChannelModel?.ref.provider ?? params.provider;
|
||||
const statusModel =
|
||||
resolvedInheritedModel?.model ?? resolvedChannelModel?.ref.model ?? params.model;
|
||||
let resolvedDefaultThinkingLevel: ThinkLevel | undefined;
|
||||
const resolveDefaultThinkingLevel = async () => {
|
||||
resolvedDefaultThinkingLevel ??= await resolveNativeSlashDefaultThinkingLevel({
|
||||
|
||||
@@ -296,7 +296,7 @@ vi.mock("./session-system-events.js", () => ({
|
||||
drainFormattedSystemEvents: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("./stored-model-override.js", () => ({
|
||||
vi.mock("../../sessions/stored-model-overrides.js", () => ({
|
||||
resolveStoredModelOverride: vi.fn(
|
||||
(params: {
|
||||
sessionEntry?: { providerOverride?: string; modelOverride?: string };
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
ModelSelectionLockedError,
|
||||
} from "../../sessions/model-overrides.js";
|
||||
import { ensureSessionDiffBaseline } from "../../sessions/session-diff-baseline.js";
|
||||
import { resolveStoredModelOverride } from "../../sessions/stored-model-overrides.js";
|
||||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import {
|
||||
sessionDeliveryChannel,
|
||||
@@ -88,10 +89,7 @@ import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js"
|
||||
import { initSessionState, resolveReplySessionPreprocessingState } from "./session.js";
|
||||
import { mergeSkillFilters } from "./skill-filter.js";
|
||||
import { stageRemoteInboundMediaIfNeeded } from "./stage-remote-inbound-media.js";
|
||||
import {
|
||||
isStaleHeartbeatAutoFallbackOverride,
|
||||
resolveStoredModelOverride,
|
||||
} from "./stored-model-override.js";
|
||||
import { isStaleHeartbeatAutoFallbackOverride } from "./stored-model-override.js";
|
||||
import { createTypingController } from "./typing.js";
|
||||
|
||||
type ResetCommandAction = "new" | "reset";
|
||||
@@ -840,13 +838,13 @@ export async function getReplyFromConfig(
|
||||
primaryModel,
|
||||
})
|
||||
: undefined;
|
||||
const hasEffectiveSessionModelOverride =
|
||||
hasSessionModelOverride &&
|
||||
const hasEffectiveStoredModelOverride =
|
||||
Boolean(storedModelOverride || hasSessionModelOverride) &&
|
||||
!staleHeartbeatAutoFallbackOverride &&
|
||||
!staleLegacyAutoFallbackWithoutOrigin;
|
||||
if (
|
||||
!hasResolvedHeartbeatModelOverride &&
|
||||
!hasEffectiveSessionModelOverride &&
|
||||
!hasEffectiveStoredModelOverride &&
|
||||
resolvedChannelModelOverride
|
||||
) {
|
||||
provider = resolvedChannelModelOverride.ref.provider;
|
||||
|
||||
@@ -39,14 +39,13 @@ import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { isDiagnosticFlagEnabled } from "../../infra/diagnostic-flags.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import * as storedModelOverrides from "../../sessions/stored-model-overrides.js";
|
||||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import { normalizeThinkLevel, type ThinkLevel } from "../thinking.shared.js";
|
||||
import { normalizeRuntimeRef, resolveRuntimeNormalization } from "./model-runtime-normalization.js";
|
||||
import {
|
||||
isStaleHeartbeatAutoFallbackOverride,
|
||||
normalizeStoredRuntimeModelRef,
|
||||
resolveDirectStoredModelOverride,
|
||||
resolveStoredModelOverride,
|
||||
} from "./stored-model-override.js";
|
||||
export {
|
||||
resolveModelDirectiveSelection,
|
||||
@@ -244,7 +243,7 @@ export async function createModelSelectionState(params: {
|
||||
let resetModelOverride = false;
|
||||
let resetModelOverrideRef: string | undefined;
|
||||
let resetModelOverrideReason: "disallowed" | "stale" | "temporarily-unavailable" | undefined;
|
||||
const directStoredModelOverride = resolveDirectStoredModelOverride({
|
||||
const directStoredModelOverride = storedModelOverrides.resolveDirectStoredModelOverride({
|
||||
sessionEntry,
|
||||
defaultProvider,
|
||||
});
|
||||
@@ -424,7 +423,7 @@ export async function createModelSelectionState(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const storedOverride = resolveStoredModelOverride({
|
||||
const storedOverride = storedModelOverrides.resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
|
||||
@@ -1,52 +1,18 @@
|
||||
// Persists and resolves per-session model override choices.
|
||||
// Normalizes stored reply models and detects stale heartbeat fallback pins.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { hasSessionAutoModelFallbackProvenance } from "../../agents/agent-scope.js";
|
||||
import { resolveCliRuntimeCanonicalProvider } from "../../agents/cli-backends.js";
|
||||
import type { ModelFallbackRouteResolution } from "../../agents/model-fallback.types.js";
|
||||
import {
|
||||
modelKey,
|
||||
normalizeModelRef,
|
||||
normalizeStoredOverrideModel,
|
||||
resolvePersistedOverrideModelRef,
|
||||
} from "../../agents/model-selection.js";
|
||||
} from "../../agents/model-selection-persisted.js";
|
||||
import { modelKey, normalizeModelRef } from "../../agents/model-selection.js";
|
||||
import { RUNTIME_MODEL_VISIBILITY_NORMALIZATION } from "../../agents/model-visibility-policy.js";
|
||||
import { resolveSessionParentSessionKey } from "../../channels/plugins/session-conversation.js";
|
||||
import { resolveSessionModelOverrideRouteResolution } from "../../config/sessions/model-override-provenance.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { StoredModelOverride } from "../../sessions/stored-model-overrides.js";
|
||||
import type { RuntimeModelNormalization } from "./model-runtime-normalization.js";
|
||||
|
||||
/** Model override loaded from the current session or its parent session. */
|
||||
export type StoredModelOverride = {
|
||||
provider?: string;
|
||||
model: string;
|
||||
source: "session" | "parent";
|
||||
routeResolution: ModelFallbackRouteResolution;
|
||||
};
|
||||
|
||||
/** Resolves only the current session's persisted model override. */
|
||||
export function resolveDirectStoredModelOverride(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
defaultProvider: string;
|
||||
}): StoredModelOverride | null {
|
||||
const normalized = normalizeStoredOverrideModel({
|
||||
providerOverride: params.sessionEntry?.providerOverride,
|
||||
modelOverride: params.sessionEntry?.modelOverride,
|
||||
});
|
||||
const direct = resolvePersistedOverrideModelRef({
|
||||
defaultProvider: params.defaultProvider,
|
||||
overrideProvider: normalized.providerOverride,
|
||||
overrideModel: normalized.modelOverride,
|
||||
});
|
||||
return direct
|
||||
? {
|
||||
...direct,
|
||||
source: "session",
|
||||
routeResolution: resolveSessionModelOverrideRouteResolution(params.sessionEntry),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Normalizes a stored model ref, resolving runtime aliases only for CLI-bound sessions. */
|
||||
export function normalizeStoredRuntimeModelRef(
|
||||
provider: string,
|
||||
@@ -69,64 +35,6 @@ export function normalizeStoredRuntimeModelRef(
|
||||
return canonicalProvider ? { ...normalized, provider: canonicalProvider } : normalized;
|
||||
}
|
||||
|
||||
function resolveParentSessionKeyCandidate(params: {
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
}): string | null {
|
||||
const explicit = normalizeOptionalString(params.parentSessionKey);
|
||||
if (explicit && explicit !== params.sessionKey) {
|
||||
return explicit;
|
||||
}
|
||||
const derived = resolveSessionParentSessionKey(params.sessionKey);
|
||||
if (derived && derived !== params.sessionKey) {
|
||||
return derived;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolves the persisted model override visible to the current session. */
|
||||
export function resolveStoredModelOverride(params: {
|
||||
loadSessionEntry?: (sessionKey: string) => SessionEntry | undefined;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
defaultProvider: string;
|
||||
}): StoredModelOverride | null {
|
||||
const direct = resolveDirectStoredModelOverride({
|
||||
sessionEntry: params.sessionEntry,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
const parentKey = resolveParentSessionKeyCandidate({
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: params.parentSessionKey,
|
||||
});
|
||||
if (!parentKey) {
|
||||
return null;
|
||||
}
|
||||
const parentEntry = params.loadSessionEntry?.(parentKey) ?? params.sessionStore?.[parentKey];
|
||||
const normalizedParentOverride = normalizeStoredOverrideModel({
|
||||
providerOverride: parentEntry?.providerOverride,
|
||||
modelOverride: parentEntry?.modelOverride,
|
||||
});
|
||||
const parentOverride = resolvePersistedOverrideModelRef({
|
||||
defaultProvider: params.defaultProvider,
|
||||
overrideProvider: normalizedParentOverride.providerOverride,
|
||||
overrideModel: normalizedParentOverride.modelOverride,
|
||||
});
|
||||
if (!parentOverride) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...parentOverride,
|
||||
source: "parent",
|
||||
routeResolution: resolveSessionModelOverrideRouteResolution(parentEntry),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveModelRefKey(params: {
|
||||
defaultProvider: string;
|
||||
overrideProvider?: string;
|
||||
|
||||
@@ -36,7 +36,7 @@ export {
|
||||
resolveCommandAuthorization,
|
||||
type CommandAuthorization,
|
||||
} from "../auto-reply/command-auth.js";
|
||||
export { resolveStoredModelOverride } from "../auto-reply/reply/stored-model-override.js";
|
||||
export { resolveStoredModelOverride } from "../sessions/stored-model-overrides.js";
|
||||
export { resolveEffectiveAgentRuntime } from "../agents/thinking-runtime.js";
|
||||
export {
|
||||
formatFastModeCommandOptions,
|
||||
|
||||
@@ -101,8 +101,8 @@ export {
|
||||
resolveModelsCommandReply,
|
||||
} from "../auto-reply/reply/commands-models.js";
|
||||
export type { ModelsProviderData } from "../auto-reply/reply/commands-models.js";
|
||||
export { resolveStoredModelOverride } from "../auto-reply/reply/stored-model-override.js";
|
||||
export type { StoredModelOverride } from "../auto-reply/reply/stored-model-override.js";
|
||||
export { resolveStoredModelOverride } from "../sessions/stored-model-overrides.js";
|
||||
export type { StoredModelOverride } from "../sessions/stored-model-overrides.js";
|
||||
|
||||
/**
|
||||
* Inputs for legacy sender command authorization.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveStoredModelOverride } from "./stored-model-override.js";
|
||||
import { resolveStoredModelOverride } from "./stored-model-overrides.js";
|
||||
|
||||
describe("resolveStoredModelOverride", () => {
|
||||
it("recovers resolved provenance for legacy auto-fallback overrides", () => {
|
||||
@@ -0,0 +1,98 @@
|
||||
// Resolves persisted per-session model choices across child and parent sessions.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { ModelFallbackRouteResolution } from "../agents/model-fallback.types.js";
|
||||
import {
|
||||
normalizeStoredOverrideModel,
|
||||
resolvePersistedOverrideModelRef,
|
||||
} from "../agents/model-selection-persisted.js";
|
||||
import { resolveSessionParentSessionKey } from "../channels/plugins/session-conversation.js";
|
||||
import { resolveSessionModelOverrideRouteResolution } from "../config/sessions/model-override-provenance.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
|
||||
/** Model override loaded from the current session or its parent session. */
|
||||
export type StoredModelOverride = {
|
||||
provider?: string;
|
||||
model: string;
|
||||
source: "session" | "parent";
|
||||
routeResolution: ModelFallbackRouteResolution;
|
||||
};
|
||||
|
||||
function resolveStoredOverrideFromEntry(params: {
|
||||
entry?: SessionEntry;
|
||||
defaultProvider: string;
|
||||
source: StoredModelOverride["source"];
|
||||
}): StoredModelOverride | null {
|
||||
const normalized = normalizeStoredOverrideModel({
|
||||
providerOverride: params.entry?.providerOverride,
|
||||
modelOverride: params.entry?.modelOverride,
|
||||
});
|
||||
const ref = resolvePersistedOverrideModelRef({
|
||||
defaultProvider: params.defaultProvider,
|
||||
overrideProvider: normalized.providerOverride,
|
||||
overrideModel: normalized.modelOverride,
|
||||
});
|
||||
return ref
|
||||
? {
|
||||
...ref,
|
||||
source: params.source,
|
||||
routeResolution: resolveSessionModelOverrideRouteResolution(params.entry),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Resolves only the current session's persisted model override. */
|
||||
export function resolveDirectStoredModelOverride(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
defaultProvider: string;
|
||||
}): StoredModelOverride | null {
|
||||
return resolveStoredOverrideFromEntry({
|
||||
entry: params.sessionEntry,
|
||||
defaultProvider: params.defaultProvider,
|
||||
source: "session",
|
||||
});
|
||||
}
|
||||
|
||||
function resolveParentSessionKeyCandidate(params: {
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
}): string | null {
|
||||
const explicit = normalizeOptionalString(params.parentSessionKey);
|
||||
if (explicit && explicit !== params.sessionKey) {
|
||||
return explicit;
|
||||
}
|
||||
const derived = resolveSessionParentSessionKey(params.sessionKey);
|
||||
if (derived && derived !== params.sessionKey) {
|
||||
return derived;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolves the persisted model override visible to the current session. */
|
||||
export function resolveStoredModelOverride(params: {
|
||||
loadSessionEntry?: (sessionKey: string) => SessionEntry | undefined;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
defaultProvider: string;
|
||||
}): StoredModelOverride | null {
|
||||
const direct = resolveDirectStoredModelOverride({
|
||||
sessionEntry: params.sessionEntry,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
const parentKey = resolveParentSessionKeyCandidate({
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: params.parentSessionKey,
|
||||
});
|
||||
if (!parentKey) {
|
||||
return null;
|
||||
}
|
||||
return resolveStoredOverrideFromEntry({
|
||||
entry: params.loadSessionEntry?.(parentKey) ?? params.sessionStore?.[parentKey],
|
||||
defaultProvider: params.defaultProvider,
|
||||
source: "parent",
|
||||
});
|
||||
}
|
||||
@@ -260,13 +260,11 @@ export const TURN_MODEL_DIFFERENTIAL_FIXTURES: TurnModelDifferentialFixture[] =
|
||||
entry: createTurnModelEntry({ sessionId: "parent-session", override: TURN_MODEL_PARENT_REF }),
|
||||
},
|
||||
modelByChannel: { telegram: { "*": turnModelRefLabel(TURN_MODEL_CHANNEL_REF) } },
|
||||
// OBSERVED, not endorsed: reply's child-only stored gate lets channel replace the
|
||||
// resolved parent. Harness keeps parent; command does not inherit it. Product decision is separate.
|
||||
expected: {
|
||||
reply: turnModelVerdict(TURN_MODEL_CHANNEL_REF),
|
||||
status: turnModelVerdict(TURN_MODEL_CHANNEL_REF),
|
||||
reply: turnModelVerdict(TURN_MODEL_PARENT_REF),
|
||||
status: turnModelVerdict(TURN_MODEL_PARENT_REF),
|
||||
harness: turnModelVerdict(TURN_MODEL_PARENT_REF),
|
||||
command: turnModelVerdict(TURN_MODEL_CHANNEL_REF),
|
||||
command: turnModelVerdict(TURN_MODEL_PARENT_REF),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user