mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 19:10:42 +00:00
422 lines
12 KiB
TypeScript
422 lines
12 KiB
TypeScript
import {
|
|
resolveAgentModelFallbackValues,
|
|
resolveAgentModelPrimaryValue,
|
|
toAgentModelListLike,
|
|
} from "../config/model-input.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import {
|
|
normalizeLowercaseStringOrEmpty,
|
|
normalizeOptionalString,
|
|
} 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";
|
|
|
|
function normalizePersistedDefaultProvider(value: unknown): string {
|
|
return normalizeOptionalString(value) ?? DEFAULT_PROVIDER;
|
|
}
|
|
|
|
export function resolvePersistedOverrideModelRef(params: {
|
|
defaultProvider?: unknown;
|
|
overrideProvider?: unknown;
|
|
overrideModel?: unknown;
|
|
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, {
|
|
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?: unknown;
|
|
runtimeProvider?: unknown;
|
|
runtimeModel?: unknown;
|
|
overrideProvider?: unknown;
|
|
overrideModel?: unknown;
|
|
allowPluginNormalization?: boolean;
|
|
}): ModelRef | null {
|
|
const defaultProvider = normalizePersistedDefaultProvider(params.defaultProvider);
|
|
const runtimeProvider = normalizeOptionalString(params.runtimeProvider);
|
|
const runtimeModel = normalizeOptionalString(params.runtimeModel);
|
|
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?: unknown;
|
|
runtimeProvider?: unknown;
|
|
runtimeModel?: unknown;
|
|
overrideProvider?: unknown;
|
|
overrideModel?: unknown;
|
|
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?: 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 function resolveAllowlistModelKey(
|
|
raw: string,
|
|
defaultProvider: string,
|
|
cfg?: OpenClawConfig,
|
|
): string | null {
|
|
return resolveAllowlistModelKeyFromShared({ cfg, raw, defaultProvider });
|
|
}
|
|
|
|
export function resolveDefaultModelForAgent(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId?: string;
|
|
allowPluginNormalization?: boolean;
|
|
}): 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,
|
|
allowPluginNormalization: params.allowPluginNormalization,
|
|
});
|
|
}
|
|
|
|
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";
|
|
}
|