docs: document model selection reconciliation

This commit is contained in:
Peter Steinberger
2026-06-04 01:18:43 -04:00
parent ba445e0e3f
commit 9bc7712c40
3 changed files with 33 additions and 0 deletions
+1
View File
@@ -3,6 +3,7 @@ import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js";
import { resolvePluginSetupCliBackendDescriptor } from "../plugins/setup-registry.runtime.js";
import { normalizeProviderId } from "./model-selection-normalize.js";
/** Return true when a provider id resolves to a configured or plugin CLI backend. */
export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean {
const normalized = normalizeProviderId(provider);
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
+25
View File
@@ -28,6 +28,8 @@ import {
parseModelRef,
} from "./model-selection-normalize.js";
// Shared model-selection helpers for config aliases, allowlists, provider
// inference, and configured catalog rows used by CLI and runtime selectors.
let log: ReturnType<typeof createSubsystemLogger> | null = null;
function getLog(): ReturnType<typeof createSubsystemLogger> {
@@ -106,6 +108,8 @@ function createModelManifestPluginContext(params: {
return {
peek: () => manifestPlugins,
get: () => {
// Manifest metadata can touch plugin registries. Defer that work until a
// path actually needs plugin/provider normalization.
if (!resolved) {
manifestPlugins = resolveManifestPluginsForModelIdNormalization(params);
resolved = true;
@@ -173,6 +177,7 @@ function mergeModelCatalogEntries(params: {
return merged;
}
/** Infer a unique provider for a bare model from configured model rows. */
export function inferUniqueProviderFromConfiguredModels(
params: {
cfg: OpenClawConfig;
@@ -252,6 +257,7 @@ export function inferUniqueProviderFromConfiguredModels(
return providers.values().next().value;
}
/** Infer a unique provider for a bare model from a provider catalog. */
export function inferUniqueProviderFromCatalog(params: {
catalog: readonly ModelCatalogEntry[];
model: string;
@@ -281,6 +287,7 @@ export function inferUniqueProviderFromCatalog(params: {
return providers.size === 1 ? providers.values().next().value : undefined;
}
/** Resolve the provider used when a model string omits provider/id syntax. */
export function resolveBareModelDefaultProvider(
params: {
cfg: OpenClawConfig;
@@ -346,6 +353,7 @@ function resolveConfiguredOpenRouterCompatFreeRef(
return null;
}
/** Resolve OpenRouter compatibility aliases such as openrouter:auto/free. */
export function resolveConfiguredOpenRouterCompatAlias(
params: {
cfg?: OpenClawConfig;
@@ -475,6 +483,7 @@ function resolveExactConfiguredProviderRef(
return normalizeExactConfiguredProviderRef(exactConfigured, params);
}
/** Normalize a configured allowlist entry into the canonical provider/model key. */
export function resolveAllowlistModelKey(
params: {
cfg?: OpenClawConfig;
@@ -498,6 +507,7 @@ export function resolveAllowlistModelKey(
return modelKey(parsed.provider, parsed.model);
}
/** Build the exact configured model keys that constrain model visibility. */
export function buildConfiguredAllowlistKeys(
params: {
cfg: OpenClawConfig | undefined;
@@ -549,6 +559,8 @@ function buildModelAliasIndexWithManifestContext(
const manifestPlugins = params.manifestPluginContext.get();
for (const { keyRaw, alias } of aliasCandidates) {
// Aliases point at configured model keys, not arbitrary model ids, so profile
// suffixes and configured-provider normalization happen on the target key.
const parsed = parseModelRefWithCompatAlias({
cfg: params.cfg,
raw: keyRaw,
@@ -571,6 +583,7 @@ function buildModelAliasIndexWithManifestContext(
return { byAlias, byKey };
}
/** Build lookup maps from user-facing aliases to normalized model refs. */
export function buildModelAliasIndex(params: BuildModelAliasIndexParams): ModelAliasIndex {
return buildModelAliasIndexWithManifestContext({
cfg: params.cfg,
@@ -719,6 +732,7 @@ export function resolveModelRefFromString(
return { ref: parsed };
}
/** Resolve the default configured model ref, including aliases and fallback provider rows. */
export function resolveConfiguredModelRef(
params: {
cfg: OpenClawConfig;
@@ -742,6 +756,8 @@ export function resolveConfiguredModelRef(
? (exactAliasCandidate ?? strippedAliasCandidate)
: undefined;
if (profileAliasCandidate) {
// Auth-profile suffixes are not part of alias matching; resolve the alias
// target while preserving the provider/model semantics of the key.
const aliasRef = parseModelRefWithCompatAlias({
cfg: params.cfg,
raw: profileAliasCandidate.keyRaw,
@@ -828,6 +844,8 @@ export function resolveConfiguredModelRef(
(!inferredProvider || inferredProvider !== "openai") &&
hasConfiguredRowsNeedingManifestLookup(params.cfg, params.defaultProvider)
) {
// Non-default provider rows may normalize through plugin manifests. Avoid
// that heavier lookup unless the cheap configured pass was ambiguous.
inferredProviderManifestPlugins = manifestPluginContext.get();
inferredProvider =
inferUniqueProviderFromConfiguredModels({
@@ -883,6 +901,7 @@ export function resolveConfiguredModelRef(
return { provider: params.defaultProvider, model: params.defaultModel };
}
/** Build allowed model keys/catalog entries after provider wildcards and fallbacks. */
export function buildAllowedModelSetWithFallbacks(
params: {
cfg: OpenClawConfig;
@@ -1006,6 +1025,8 @@ export function buildAllowedModelSetWithFallbacks(
!findModelCatalogEntry(catalog, { provider: parsed.provider, modelId: parsed.model }) &&
!syntheticCatalogEntries.has(key)
) {
// Config can allow a model before it appears in live provider catalogs.
// Synthetic entries keep UI/model switchers aligned with that allowlist.
syntheticCatalogEntries.set(key, buildSyntheticAllowedCatalogEntry({ parsed, metadata }));
}
};
@@ -1059,6 +1080,7 @@ export function buildAllowedModelSetWithFallbacks(
return { allowAny: false, allowedCatalog, allowedKeys };
}
/** Status of a candidate model against catalog and configured allowlist state. */
export type ModelRefStatus = {
key: string;
inCatalog: boolean;
@@ -1119,6 +1141,7 @@ export function getModelRefStatusWithFallbackModels(
});
}
/** Resolve a requested model string only if it is allowed by the supplied status check. */
export function resolveAllowedModelRefFromAliasIndex(
params: {
cfg: OpenClawConfig;
@@ -1160,6 +1183,7 @@ export function resolveAllowedModelRefFromAliasIndex(
return { ref: resolved.ref, key: status.key };
}
/** True when config contains provider model rows that should seed catalogs. */
export function hasConfiguredProviderModelRows(cfg: OpenClawConfig): boolean {
const providers = cfg.models?.providers;
if (!providers || typeof providers !== "object") {
@@ -1239,6 +1263,7 @@ function resolveConfiguredModelManifestPlugins(params: {
}).plugins;
}
/** Build catalog entries from configured provider model rows. */
export function buildConfiguredModelCatalog(params: {
cfg: OpenClawConfig;
workspaceDir?: string;
@@ -15,8 +15,11 @@ import {
type SubagentLifecycleEndedReason,
} from "./subagent-lifecycle-events.js";
// Reconcile subagent lifecycle events with persisted session store state. This
// lets parent sessions announce completion even if registry updates arrive late.
export type SubagentSessionStoreCache = Map<string, Record<string, SessionEntry>>;
/** Completion inferred from the child session store. */
export type SubagentSessionCompletion = {
startedAt?: number;
endedAt: number;
@@ -68,6 +71,7 @@ function findSessionEntryByKey(store: Record<string, SessionEntry>, sessionKey:
return undefined;
}
/** Load a child session entry using the agent-specific session store path. */
export function loadSubagentSessionEntry(params: {
childSessionKey: string;
storeCache?: SubagentSessionStoreCache;
@@ -88,6 +92,7 @@ export function loadSubagentSessionEntry(params: {
return findSessionEntryByKey(store, key);
}
/** Convert persisted session status into a subagent completion outcome. */
export function resolveCompletionFromSessionEntry(
sessionEntry: SessionEntry | undefined,
fallbackEndedAt: number,
@@ -158,6 +163,7 @@ export function resolveCompletionFromSessionEntry(
return null;
}
/** Resolve child completion by reading its persisted session entry. */
export function resolveSubagentSessionCompletion(params: {
childSessionKey: string;
fallbackEndedAt: number;
@@ -176,6 +182,7 @@ export function resolveSubagentSessionCompletion(params: {
);
}
/** Resolve a fresh child session start time for lifecycle reconciliation. */
export function resolveSubagentSessionStartedAt(params: {
childSessionKey: string;
notBeforeMs?: number;