mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 13:40:42 +00:00
* fix(agents): resolve model aliases in sessions_spawn normalizeModelSelection() only trims the input — it never resolves aliases through the model alias index. When a user passes an alias like 'opus' to sessions_spawn, the child session gets patched with the raw string, which the gateway cannot match to any provider. Add resolveModelThroughAliases() to check bare strings against the configured alias map before returning from resolveSubagentSpawnModelSelection(). Fixes #57532 Refs #50736 * refactor: address review feedback on alias resolution - Accept pre-built ModelAliasIndex instead of rebuilding per call - Narrow helper signature to (string, ModelAliasIndex) → string - Remove unreachable ?? raw fallback Co-Authored-By: greptile-apps[bot] * fix(agents): resolve sessions_spawn model aliases --------- Co-authored-by: HowdyDooToYou <HowdyDooToYou@users.noreply.github.com> Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
413 lines
12 KiB
TypeScript
413 lines
12 KiB
TypeScript
import {
|
|
resolveAgentModelFallbackValues,
|
|
resolveAgentModelPrimaryValue,
|
|
toAgentModelListLike,
|
|
} from "../config/model-input.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
|
import {
|
|
resolveAgentConfig,
|
|
resolveAgentEffectiveModelPrimary,
|
|
resolveAgentModelFallbacksOverride,
|
|
} from "./agent-scope.js";
|
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
|
import type { ModelCatalogEntry } from "./model-catalog.types.js";
|
|
export { resolveThinkingDefault } from "./model-thinking-default.js";
|
|
import {
|
|
type ModelRef,
|
|
findNormalizedProviderKey,
|
|
findNormalizedProviderValue,
|
|
legacyModelKey,
|
|
modelKey,
|
|
normalizeModelRef,
|
|
normalizeProviderId,
|
|
normalizeProviderIdForAuth,
|
|
parseModelRef,
|
|
} from "./model-selection-normalize.js";
|
|
import {
|
|
buildAllowedModelSetWithFallbacks,
|
|
buildConfiguredAllowlistKeys,
|
|
buildConfiguredModelCatalog,
|
|
buildModelAliasIndex,
|
|
getModelRefStatusWithFallbackModels,
|
|
inferUniqueProviderFromCatalog,
|
|
inferUniqueProviderFromConfiguredModels,
|
|
normalizeModelSelection,
|
|
resolveBareModelDefaultProvider,
|
|
resolveAllowedModelRefFromAliasIndex,
|
|
resolveAllowlistModelKey as resolveAllowlistModelKeyFromShared,
|
|
resolveConfiguredModelRef,
|
|
resolveConfiguredOpenRouterCompatAlias,
|
|
resolveHooksGmailModel,
|
|
resolveModelRefFromString,
|
|
type ModelAliasIndex,
|
|
type ModelRefStatus,
|
|
} from "./model-selection-shared.js";
|
|
|
|
export type { ModelAliasIndex, ModelRef, ModelRefStatus };
|
|
|
|
export type ThinkLevel =
|
|
| "off"
|
|
| "minimal"
|
|
| "low"
|
|
| "medium"
|
|
| "high"
|
|
| "xhigh"
|
|
| "adaptive"
|
|
| "max";
|
|
|
|
export {
|
|
buildConfiguredAllowlistKeys,
|
|
buildConfiguredModelCatalog,
|
|
buildModelAliasIndex,
|
|
findNormalizedProviderKey,
|
|
findNormalizedProviderValue,
|
|
inferUniqueProviderFromConfiguredModels,
|
|
inferUniqueProviderFromCatalog,
|
|
legacyModelKey,
|
|
modelKey,
|
|
normalizeModelRef,
|
|
normalizeModelSelection,
|
|
normalizeProviderId,
|
|
normalizeProviderIdForAuth,
|
|
parseModelRef,
|
|
resolveBareModelDefaultProvider,
|
|
resolveConfiguredModelRef,
|
|
resolveHooksGmailModel,
|
|
resolveModelRefFromString,
|
|
};
|
|
export { isCliProvider } from "./model-selection-cli.js";
|
|
|
|
export function resolvePersistedOverrideModelRef(params: {
|
|
defaultProvider: string;
|
|
overrideProvider?: string;
|
|
overrideModel?: string;
|
|
allowPluginNormalization?: boolean;
|
|
}): ModelRef | null {
|
|
const defaultProvider = params.defaultProvider.trim();
|
|
const overrideProvider = params.overrideProvider?.trim();
|
|
const overrideModel = params.overrideModel?.trim();
|
|
if (!overrideModel) {
|
|
return null;
|
|
}
|
|
const encodedOverride = overrideProvider ? `${overrideProvider}/${overrideModel}` : overrideModel;
|
|
return (
|
|
parseModelRef(encodedOverride, defaultProvider, {
|
|
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.
|
|
*/
|
|
export function resolvePersistedModelRef(params: {
|
|
defaultProvider: string;
|
|
runtimeProvider?: string;
|
|
runtimeModel?: string;
|
|
overrideProvider?: string;
|
|
overrideModel?: string;
|
|
allowPluginNormalization?: boolean;
|
|
}): ModelRef | null {
|
|
const defaultProvider = params.defaultProvider.trim();
|
|
const runtimeProvider = params.runtimeProvider?.trim();
|
|
const runtimeModel = params.runtimeModel?.trim();
|
|
if (runtimeModel) {
|
|
if (runtimeProvider) {
|
|
return { provider: runtimeProvider, model: runtimeModel };
|
|
}
|
|
return (
|
|
parseModelRef(runtimeModel, defaultProvider, {
|
|
allowPluginNormalization: params.allowPluginNormalization,
|
|
}) ?? {
|
|
provider: defaultProvider,
|
|
model: runtimeModel,
|
|
}
|
|
);
|
|
}
|
|
return resolvePersistedOverrideModelRef({
|
|
defaultProvider,
|
|
overrideProvider: params.overrideProvider,
|
|
overrideModel: params.overrideModel,
|
|
allowPluginNormalization: params.allowPluginNormalization,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Selected-model resolver for persisted model metadata.
|
|
* Use this for control/status/UI surfaces that should honor explicit session
|
|
* overrides before falling back to runtime identity.
|
|
*/
|
|
export function resolvePersistedSelectedModelRef(params: {
|
|
defaultProvider: string;
|
|
runtimeProvider?: string;
|
|
runtimeModel?: string;
|
|
overrideProvider?: string;
|
|
overrideModel?: string;
|
|
allowPluginNormalization?: boolean;
|
|
}): ModelRef | null {
|
|
const override = resolvePersistedOverrideModelRef({
|
|
defaultProvider: params.defaultProvider,
|
|
overrideProvider: params.overrideProvider,
|
|
overrideModel: params.overrideModel,
|
|
allowPluginNormalization: params.allowPluginNormalization,
|
|
});
|
|
if (override) {
|
|
return override;
|
|
}
|
|
return resolvePersistedModelRef({
|
|
defaultProvider: params.defaultProvider,
|
|
runtimeProvider: params.runtimeProvider,
|
|
runtimeModel: params.runtimeModel,
|
|
allowPluginNormalization: params.allowPluginNormalization,
|
|
});
|
|
}
|
|
|
|
export function normalizeStoredOverrideModel(params: {
|
|
providerOverride?: string | null;
|
|
modelOverride?: string | null;
|
|
}): { providerOverride?: string; modelOverride?: string } {
|
|
const providerOverride = params.providerOverride?.trim();
|
|
const modelOverride = params.modelOverride?.trim();
|
|
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 function resolveAllowlistModelKey(
|
|
raw: string,
|
|
defaultProvider: string,
|
|
cfg?: OpenClawConfig,
|
|
): string | null {
|
|
return resolveAllowlistModelKeyFromShared({ cfg, raw, defaultProvider });
|
|
}
|
|
|
|
export function resolveDefaultModelForAgent(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId?: string;
|
|
}): ModelRef {
|
|
const agentModelOverride = params.agentId
|
|
? resolveAgentEffectiveModelPrimary(params.cfg, params.agentId)
|
|
: undefined;
|
|
const cfg =
|
|
agentModelOverride && agentModelOverride.length > 0
|
|
? {
|
|
...params.cfg,
|
|
agents: {
|
|
...params.cfg.agents,
|
|
defaults: {
|
|
...params.cfg.agents?.defaults,
|
|
model: {
|
|
...toAgentModelListLike(params.cfg.agents?.defaults?.model),
|
|
primary: agentModelOverride,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
: params.cfg;
|
|
return resolveConfiguredModelRef({
|
|
cfg,
|
|
defaultProvider: DEFAULT_PROVIDER,
|
|
defaultModel: DEFAULT_MODEL,
|
|
});
|
|
}
|
|
|
|
function resolveAllowedFallbacks(params: { cfg: OpenClawConfig; agentId?: string }): string[] {
|
|
if (params.agentId) {
|
|
const override = resolveAgentModelFallbacksOverride(params.cfg, params.agentId);
|
|
if (override !== undefined) {
|
|
return override;
|
|
}
|
|
}
|
|
return resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);
|
|
}
|
|
|
|
export function resolveSubagentConfiguredModelSelection(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
}): string | undefined {
|
|
const agentConfig = resolveAgentConfig(params.cfg, params.agentId);
|
|
return (
|
|
normalizeModelSelection(agentConfig?.subagents?.model) ??
|
|
normalizeModelSelection(agentConfig?.model) ??
|
|
normalizeModelSelection(params.cfg.agents?.defaults?.subagents?.model)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolve a normalized model string through a pre-built alias index, returning
|
|
* a fully qualified `provider/model` string. If the value is already qualified
|
|
* or not a known alias, returns it unchanged.
|
|
*/
|
|
function resolveModelThroughAliases(value: string, aliasIndex: ModelAliasIndex): string {
|
|
// Already a provider/model ref — no alias resolution needed.
|
|
if (value.includes("/")) {
|
|
return value;
|
|
}
|
|
// Check if the value is a known alias; if so, resolve to provider/model.
|
|
// Unknown bare strings are returned as-is (don't guess the provider).
|
|
const aliasKey = normalizeLowercaseStringOrEmpty(value);
|
|
const aliasMatch = aliasIndex.byAlias.get(aliasKey);
|
|
if (aliasMatch) {
|
|
return `${aliasMatch.ref.provider}/${aliasMatch.ref.model}`;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function resolveSubagentSpawnModelSelection(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
modelOverride?: unknown;
|
|
}): string {
|
|
const runtimeDefault = resolveDefaultModelForAgent({
|
|
cfg: params.cfg,
|
|
agentId: params.agentId,
|
|
});
|
|
const raw =
|
|
normalizeModelSelection(params.modelOverride) ??
|
|
resolveSubagentConfiguredModelSelection({
|
|
cfg: params.cfg,
|
|
agentId: params.agentId,
|
|
}) ??
|
|
normalizeModelSelection(resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)) ??
|
|
`${runtimeDefault.provider}/${runtimeDefault.model}`;
|
|
const aliasIndex = buildModelAliasIndex({
|
|
cfg: params.cfg,
|
|
defaultProvider: runtimeDefault.provider,
|
|
});
|
|
return resolveModelThroughAliases(raw, aliasIndex);
|
|
}
|
|
|
|
export function buildAllowedModelSet(params: {
|
|
cfg: OpenClawConfig;
|
|
catalog: ModelCatalogEntry[];
|
|
defaultProvider: string;
|
|
defaultModel?: string;
|
|
agentId?: string;
|
|
}): {
|
|
allowAny: boolean;
|
|
allowedCatalog: ModelCatalogEntry[];
|
|
allowedKeys: Set<string>;
|
|
} {
|
|
return buildAllowedModelSetWithFallbacks({
|
|
cfg: params.cfg,
|
|
catalog: params.catalog,
|
|
defaultProvider: params.defaultProvider,
|
|
defaultModel: params.defaultModel,
|
|
fallbackModels: resolveAllowedFallbacks({
|
|
cfg: params.cfg,
|
|
agentId: params.agentId,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export function getModelRefStatus(params: {
|
|
cfg: OpenClawConfig;
|
|
catalog: ModelCatalogEntry[];
|
|
ref: ModelRef;
|
|
defaultProvider: string;
|
|
defaultModel?: string;
|
|
}): ModelRefStatus {
|
|
return getModelRefStatusWithFallbackModels({
|
|
cfg: params.cfg,
|
|
catalog: params.catalog,
|
|
ref: params.ref,
|
|
defaultProvider: params.defaultProvider,
|
|
defaultModel: params.defaultModel,
|
|
fallbackModels: resolveAllowedFallbacks({
|
|
cfg: params.cfg,
|
|
}),
|
|
});
|
|
}
|
|
|
|
function getModelRefStatusForResolve(
|
|
params: {
|
|
cfg: OpenClawConfig;
|
|
catalog: ModelCatalogEntry[];
|
|
defaultProvider: string;
|
|
defaultModel?: string;
|
|
},
|
|
ref: ModelRef,
|
|
): ModelRefStatus {
|
|
return getModelRefStatus({
|
|
cfg: params.cfg,
|
|
catalog: params.catalog,
|
|
ref,
|
|
defaultProvider: params.defaultProvider,
|
|
defaultModel: params.defaultModel,
|
|
});
|
|
}
|
|
|
|
export function resolveAllowedModelRef(params: {
|
|
cfg: OpenClawConfig;
|
|
catalog: ModelCatalogEntry[];
|
|
raw: string;
|
|
defaultProvider: string;
|
|
defaultModel?: string;
|
|
}):
|
|
| { ref: ModelRef; key: string }
|
|
| {
|
|
error: string;
|
|
} {
|
|
const trimmed = params.raw.trim();
|
|
if (!trimmed) {
|
|
return { error: "invalid model: empty" };
|
|
}
|
|
|
|
const aliasIndex = buildModelAliasIndex({
|
|
cfg: params.cfg,
|
|
defaultProvider: params.defaultProvider,
|
|
});
|
|
|
|
const openrouterCompatRef = resolveConfiguredOpenRouterCompatAlias({
|
|
cfg: params.cfg,
|
|
raw: trimmed,
|
|
defaultProvider: params.defaultProvider,
|
|
});
|
|
if (openrouterCompatRef) {
|
|
const status = getModelRefStatusForResolve(params, openrouterCompatRef);
|
|
if (!status.allowed) {
|
|
return { error: `model not allowed: ${status.key}` };
|
|
}
|
|
return { ref: openrouterCompatRef, key: status.key };
|
|
}
|
|
|
|
return resolveAllowedModelRefFromAliasIndex({
|
|
cfg: params.cfg,
|
|
raw: params.raw,
|
|
defaultProvider: params.defaultProvider,
|
|
aliasIndex,
|
|
getStatus: (ref) => getModelRefStatusForResolve(params, ref),
|
|
});
|
|
}
|
|
|
|
/** Default reasoning level when session/directive do not set it: "on" if model supports reasoning, else "off". */
|
|
export function resolveReasoningDefault(params: {
|
|
provider: string;
|
|
model: string;
|
|
catalog?: ModelCatalogEntry[];
|
|
}): "on" | "off" {
|
|
const key = modelKey(params.provider, params.model);
|
|
const candidate = params.catalog?.find(
|
|
(entry) =>
|
|
(entry.provider === params.provider && entry.id === params.model) ||
|
|
(entry.provider === key && entry.id === params.model),
|
|
);
|
|
return candidate?.reasoning === true ? "on" : "off";
|
|
}
|