refactor(agents): consolidate model normalization (#112772)

* refactor(agents): consolidate model normalization

* docs(agents): clarify normalization boundary
This commit is contained in:
Peter Steinberger
2026-07-22 19:19:41 -04:00
committed by GitHub
parent dbd6662976
commit cdb8d32bcc
22 changed files with 119 additions and 130 deletions

View File

@@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { normalizePluginsConfig } from "../../plugins/config-state.js";
import { normalizeConfiguredProviderCatalogModelId } from "../model-ref-shared.js";
import type { ModelManifestNormalizationContext } from "../model-selection-normalize.js";
import type { ModelManifestNormalizationContext } from "../model-ref-shared.js";
import {
buildModelAliasIndex,
normalizeModelRef,

View File

@@ -32,7 +32,7 @@ import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js";
import { resolveAvailableAgentHarnessPolicy } from "../harness/selection.js";
import { loadManifestModelCatalog } from "../model-catalog.js";
import { splitTrailingAuthProfile } from "../model-ref-profile.js";
import type { ModelManifestNormalizationContext } from "../model-selection-normalize.js";
import type { ModelManifestNormalizationContext } from "../model-ref-shared.js";
import {
modelKey,
resolveDefaultModelForAgent,

View File

@@ -35,7 +35,7 @@ import {
} from "../agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
import type { ModelManifestNormalizationContext } from "../model-selection-normalize.js";
import type { ModelManifestNormalizationContext } from "../model-ref-shared.js";
import { buildConfiguredModelCatalog, resolveConfiguredModelRef } from "../model-selection.js";
import { normalizeSpawnedRunMetadata } from "../spawned-context.js";
import { resolveEffectiveAgentRuntime } from "../thinking-runtime.js";

View File

@@ -4,7 +4,7 @@ import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js";
import type { ModelFallbackStepFields } from "../model-fallback-observation.js";
import { runWithModelFallback, type ModelFallbackResultClassification } from "../model-fallback.js";
import type { FallbackAttempt } from "../model-fallback.types.js";
import type { ModelManifestNormalizationContext } from "../model-selection-normalize.js";
import type { ModelManifestNormalizationContext } from "../model-ref-shared.js";
import { resolveAgentRunAbortLifecycleFields } from "../run-termination.js";
import {
classifyEmbeddedAgentRunResultForModelFallback,

View File

@@ -15,7 +15,7 @@
*/
import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion";
import { modelKey } from "./model-selection-normalize.js";
import { modelKey } from "./model-ref-shared.js";
/**
* Default time-to-live for a skip marker. Disabled by default so existing

View File

@@ -66,14 +66,14 @@ import {
type ModelFallbackStepFields,
} from "./model-fallback-observation.js";
import type { FallbackAttempt, ModelCandidate } from "./model-fallback.types.js";
import { isCliRuntimeAlias } from "./model-runtime-aliases.js";
import { isCliProvider } from "./model-selection-cli.js";
import {
type ModelManifestNormalizationContext,
modelKey,
normalizeModelRef,
normalizeProviderId,
} from "./model-selection-normalize.js";
} from "./model-ref-shared.js";
import { isCliRuntimeAlias } from "./model-runtime-aliases.js";
import { isCliProvider } from "./model-selection-cli.js";
import {
buildConfiguredAllowlistKeys,
buildModelAliasIndex,

View File

@@ -3,24 +3,35 @@
* allowlists, and display paths. Manifest policies are optional so tests can
* isolate built-in normalization behavior.
*/
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import {
findNormalizedProviderKey as findNormalizedProviderKeyCore,
normalizeProviderId as normalizeProviderIdCore,
normalizeProviderIdForAuth as normalizeProviderIdForAuthCore,
} from "@openclaw/model-catalog-core/provider-id";
import {
collectManifestModelIdNormalizationPolicies,
normalizeBuiltInProviderModelId,
normalizeConfiguredProviderCatalogModelRef,
normalizeConfiguredProviderCatalogModelId as normalizeConfiguredProviderCatalogModelIdShared,
normalizeStaticProviderModelIdWithPolicies,
stripSelfProviderModelPrefix,
} from "@openclaw/model-catalog-core/provider-model-id-normalization";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { normalizeProviderModelIdWithManifest } from "../plugins/manifest-model-id-normalization.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import { modelKey } from "../shared/model-key.js";
import { normalizeProviderModelIdWithRuntime } from "./provider-model-normalization.runtime.js";
export { modelKey } from "../shared/model-key.js";
type StaticModelRef = {
export type ModelRef = {
provider: string;
model: string;
};
export type ModelManifestNormalizationContext = {
manifestPlugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
};
export type ProviderModelIdNormalizationOptions = {
allowManifestNormalization?: boolean;
manifestPlugins?: readonly ManifestModelIdNormalizationRecord[];
@@ -42,6 +53,24 @@ type ManifestModelIdNormalizationRecord = {
};
};
/** Normalize a provider ID using the shared catalog rules. */
export function normalizeProviderId(provider: string): string {
return normalizeProviderIdCore(provider);
}
/** Normalize a provider ID for auth lookup. */
export function normalizeProviderIdForAuth(provider: string): string {
return normalizeProviderIdForAuthCore(provider);
}
/** Find the original provider key matching a normalized provider ID. */
export function findNormalizedProviderKey(
entries: Record<string, unknown> | undefined,
provider: string,
): string | undefined {
return findNormalizedProviderKeyCore(entries, provider);
}
/** Normalize a static provider model ID with built-in and optional manifest policy. */
export function normalizeStaticProviderModelId(
provider: string,
@@ -91,7 +120,57 @@ export function normalizeConfiguredProviderCatalogModelId(
);
}
function parseStaticModelRef(raw: string, defaultProvider: string): StaticModelRef | null {
type ModelRefNormalizeOptions = ModelManifestNormalizationContext & {
allowManifestNormalization?: boolean;
allowPluginNormalization?: boolean;
};
function normalizeProviderModelId(
provider: string,
model: string,
options?: ModelRefNormalizeOptions,
): string {
const providerModel = stripSelfProviderModelPrefix(provider, model);
const staticModelId = normalizeStaticProviderModelId(provider, providerModel, options);
if (options?.allowPluginNormalization === false) {
return staticModelId;
}
return (
normalizeProviderModelIdWithRuntime({
provider,
...(options?.manifestPlugins ? { plugins: options.manifestPlugins } : {}),
context: {
provider,
modelId: staticModelId,
},
}) ?? staticModelId
);
}
/** Normalize a provider/model pair into a canonical model reference. */
export function normalizeModelRef(
provider: string,
model: string,
options?: ModelRefNormalizeOptions,
): ModelRef {
const normalizedProvider = normalizeProviderId(provider);
const normalizedModel = normalizeProviderModelId(normalizedProvider, model.trim(), options);
return { provider: normalizedProvider, model: normalizedModel };
}
/** Return the legacy raw key when it differs from the canonical key. */
export function legacyModelKey(provider: string, model: string): string | null {
const providerId = provider.trim();
const modelId = model.trim();
if (!providerId || !modelId) {
return null;
}
const rawKey = `${providerId}/${modelId}`;
const canonicalKey = modelKey(providerId, modelId);
return rawKey === canonicalKey ? null : rawKey;
}
function parseStaticModelRef(raw: string, defaultProvider: string): ModelRef | null {
const trimmed = raw.trim();
if (!trimmed) {
return null;

View File

@@ -4,7 +4,7 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js";
import { resolvePluginSetupCliBackendDescriptor } from "../plugins/setup-registry.runtime.js";
import { normalizeProviderId } from "./model-selection-normalize.js";
import { normalizeProviderId } from "./model-ref-shared.js";
/** Return true when a provider id resolves to a configured or plugin CLI backend. */
export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean {

View File

@@ -3,7 +3,7 @@ import { toAgentModelListLike } from "../config/model-input.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveAgentConfig, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
import type { ModelManifestNormalizationContext, ModelRef } from "./model-selection-normalize.js";
import type { ModelManifestNormalizationContext, ModelRef } from "./model-ref-shared.js";
import { normalizeModelSelection, resolveConfiguredModelRef } from "./model-selection-shared.js";
export function resolveDefaultModelForAgent(

View File

@@ -1,55 +1,21 @@
/**
* Normalizes provider/model references and configured model ids.
* Internal declaration anchor for parser and lookup exports consumed by the
* public Plugin SDK barrel. Provider/model normalization lives in model-ref-shared.
*/
import {
findNormalizedProviderKey as findNormalizedProviderKeyCore,
findNormalizedProviderValue as findNormalizedProviderValueCore,
normalizeProviderId as normalizeProviderIdCore,
normalizeProviderIdForAuth as normalizeProviderIdForAuthCore,
} from "@openclaw/model-catalog-core/provider-id";
import { stripSelfProviderModelPrefix } from "@openclaw/model-catalog-core/provider-model-id-normalization";
import { findNormalizedProviderValue as findNormalizedProviderValueCore } from "@openclaw/model-catalog-core/provider-id";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import { modelKey as sharedModelKey, normalizeStaticProviderModelId } from "./model-ref-shared.js";
import { normalizeProviderModelIdWithRuntime } from "./provider-model-normalization.runtime.js";
import {
type ModelManifestNormalizationContext,
type ModelRef,
normalizeModelRef,
} from "./model-ref-shared.js";
// Shared provider/model normalization facade for agent model selection. It
// combines catalog-core provider IDs, static aliases, and optional plugin hooks.
export type ModelRef = {
provider: string;
model: string;
type ModelRefNormalizeOptions = ModelManifestNormalizationContext & {
allowManifestNormalization?: boolean;
allowPluginNormalization?: boolean;
};
export type ModelManifestNormalizationContext = {
manifestPlugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
};
/** Build the canonical provider/model key for model selection. */
export function modelKey(provider: string, model: string) {
return sharedModelKey(provider, model);
}
/** Return the legacy raw key when it differs from the canonical key. */
export function legacyModelKey(provider: string, model: string): string | null {
const providerId = provider.trim();
const modelId = model.trim();
if (!providerId || !modelId) {
return null;
}
const rawKey = `${providerId}/${modelId}`;
const canonicalKey = modelKey(providerId, modelId);
return rawKey === canonicalKey ? null : rawKey;
}
/** Normalize a provider ID using the shared catalog rules. */
export function normalizeProviderId(provider: string): string {
return normalizeProviderIdCore(provider);
}
/** Normalize a provider ID for auth lookup. */
export function normalizeProviderIdForAuth(provider: string): string {
return normalizeProviderIdForAuthCore(provider);
}
const OPENROUTER_AUTO_COMPAT_ALIAS = "openrouter:auto";
/** Find a provider value by normalized provider ID. */
export function findNormalizedProviderValue<T>(
@@ -59,60 +25,6 @@ export function findNormalizedProviderValue<T>(
return findNormalizedProviderValueCore(entries, provider);
}
/** Find the original provider key matching a normalized provider ID. */
export function findNormalizedProviderKey(
entries: Record<string, unknown> | undefined,
provider: string,
): string | undefined {
return findNormalizedProviderKeyCore(entries, provider);
}
function normalizeProviderModelId(
provider: string,
model: string,
options?: ModelManifestNormalizationContext & {
allowManifestNormalization?: boolean;
allowPluginNormalization?: boolean;
},
): string {
const providerModel = stripSelfProviderModelPrefix(provider, model);
const staticModelId = normalizeStaticProviderModelId(provider, providerModel, {
allowManifestNormalization: options?.allowManifestNormalization,
manifestPlugins: options?.manifestPlugins,
});
if (options?.allowPluginNormalization === false) {
return staticModelId;
}
return (
normalizeProviderModelIdWithRuntime({
provider,
...(options?.manifestPlugins ? { plugins: options.manifestPlugins } : {}),
context: {
provider,
modelId: staticModelId,
},
}) ?? staticModelId
);
}
type ModelRefNormalizeOptions = ModelManifestNormalizationContext & {
allowManifestNormalization?: boolean;
allowPluginNormalization?: boolean;
};
/** Normalize a provider/model pair into a canonical model reference. */
export function normalizeModelRef(
provider: string,
model: string,
options?: ModelRefNormalizeOptions,
): ModelRef {
const normalizedProvider = normalizeProviderId(provider);
const normalizedModel = normalizeProviderModelId(normalizedProvider, model.trim(), options);
return { provider: normalizedProvider, model: normalizedModel };
}
const OPENROUTER_AUTO_COMPAT_ALIAS = "openrouter:auto";
/** Parse `provider/model` or bare model text using a default provider. */
export function parseModelRef(
raw: string,

View File

@@ -8,7 +8,7 @@ import { resolveAgentModelFallbackValues } from "../config/model-input.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveAgentModelFallbacksOverride } from "./agent-scope.js";
import type { ModelCatalogEntry } from "./model-catalog.types.js";
import type { ModelManifestNormalizationContext, ModelRef } from "./model-selection-normalize.js";
import type { ModelManifestNormalizationContext, ModelRef } from "./model-ref-shared.js";
import {
buildModelAliasIndex,
getModelRefStatusWithFallbackModels,

View File

@@ -31,12 +31,11 @@ import {
import {
type ModelManifestNormalizationContext,
type ModelRef,
findNormalizedProviderValue,
modelKey,
normalizeModelRef,
normalizeProviderId,
parseModelRef,
} from "./model-selection-normalize.js";
} from "./model-ref-shared.js";
import { findNormalizedProviderValue, 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.

View File

@@ -242,7 +242,7 @@ describe("model-selection plugin runtime normalization", () => {
},
},
];
const { normalizeModelRef } = await import("./model-selection-normalize.js");
const { normalizeModelRef } = await import("./model-ref-shared.js");
normalizeModelRef("custom", "my-model", { manifestPlugins: preparedPlugins });
expect(normalizeProviderModelIdWithPluginMock).toHaveBeenCalledWith(
expect.objectContaining({
@@ -254,7 +254,7 @@ describe("model-selection plugin runtime normalization", () => {
it("omits plugins from the runtime call when no manifestPlugins are prepared (preserves current behavior)", async () => {
normalizeProviderModelIdWithPluginMock.mockReturnValue(undefined);
const { normalizeModelRef } = await import("./model-selection-normalize.js");
const { normalizeModelRef } = await import("./model-ref-shared.js");
normalizeModelRef("custom", "my-model");
const callArgs = normalizeProviderModelIdWithPluginMock.mock.calls[0]?.[0] as
| { plugins?: unknown }

View File

@@ -27,14 +27,13 @@ import {
type ModelManifestNormalizationContext,
type ModelRef,
findNormalizedProviderKey,
findNormalizedProviderValue,
legacyModelKey,
modelKey,
normalizeModelRef,
normalizeProviderId,
normalizeProviderIdForAuth,
parseModelRef,
} from "./model-selection-normalize.js";
} from "./model-ref-shared.js";
import { findNormalizedProviderValue, parseModelRef } from "./model-selection-normalize.js";
import {
buildAllowedModelSetWithFallbacks,
buildConfiguredAllowlistKeys,

View File

@@ -11,7 +11,7 @@ import { resolveThinkingDefaultForModel } from "../auto-reply/thinking.js";
import type { ThinkLevel } from "../auto-reply/thinking.shared.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { ModelCatalogEntry } from "./model-catalog.types.js";
import { legacyModelKey, modelKey, normalizeProviderId } from "./model-selection-normalize.js";
import { legacyModelKey, modelKey, normalizeProviderId } from "./model-ref-shared.js";
import { normalizeModelSelection } from "./model-selection-resolve.js";
import { buildConfiguredModelCatalog } from "./model-selection-shared.js";

View File

@@ -5,7 +5,7 @@ import { resolveAgentModelFallbackValues } from "../config/model-input.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveAgentConfig, resolveAgentModelFallbacksOverride } from "./agent-scope.js";
import type { ModelCatalogEntry } from "./model-catalog.types.js";
import type { ModelManifestNormalizationContext } from "./model-selection-normalize.js";
import type { ModelManifestNormalizationContext } from "./model-ref-shared.js";
import {
createModelVisibilityPolicyWithFallbacks,
type ModelVisibilityPolicy,

View File

@@ -1,7 +1,7 @@
// Builds memory flush prompts when conversation context exceeds model budget.
import { resolveContextTokensForModel } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import { legacyModelKey, modelKey } from "../../agents/model-selection-normalize.js";
import { legacyModelKey, modelKey } from "../../agents/model-ref-shared.js";
import { parseNonNegativeByteSize } from "../../config/byte-size.js";
import { resolveFreshSessionTotalTokens, type SessionEntry } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";

View File

@@ -33,7 +33,7 @@ import {
import { loadAuthProfileStoreForRuntime } from "../../agents/auth-profiles/store.js";
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
import { clearAuthProfileCooldown } from "../../agents/auth-profiles/usage.js";
import { normalizeProviderId } from "../../agents/model-selection-normalize.js";
import { normalizeProviderId } from "../../agents/model-ref-shared.js";
import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js";
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
import { formatCliCommand } from "../../cli/command-format.js";

View File

@@ -1,6 +1,6 @@
/** Promotion decorations for `models list`: claim tags + passive discovery. */
import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js";
import { modelKey } from "../../agents/model-selection-normalize.js";
import { modelKey } from "../../agents/model-ref-shared.js";
import { formatCliCommand } from "../../cli/command-format.js";
import type { ClawHubPromotionsFeedEntry } from "../../infra/clawhub.js";
import {

View File

@@ -7,7 +7,7 @@ import { resolveDefaultAgentId, resolveAgentConfig } from "../agents/agent-scope
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { formatFastModeValue, resolveFastModeState } from "../agents/fast-mode.js";
import type { ModelCatalogEntry } from "../agents/model-catalog.types.js";
import { legacyModelKey, modelKey } from "../agents/model-selection-normalize.js";
import { legacyModelKey, modelKey } from "../agents/model-ref-shared.js";
import {
buildConfiguredModelCatalog,
resolveConfiguredModelRef,

View File

@@ -1,6 +1,6 @@
// Tests plugin activation boundaries during root package startup.
import { describe, expect, it, vi } from "vitest";
import { normalizeModelRef } from "./agents/model-selection-normalize.js";
import { normalizeModelRef } from "./agents/model-ref-shared.js";
import { isStaticallyChannelConfigured } from "./config/channel-configured-shared.js";
import { parseBrowserMajorVersion } from "./plugin-sdk/browser-host-inspection.js";

View File

@@ -1,6 +1,6 @@
// Audits configured model references for risky provider or model choices.
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
import { modelKey } from "../agents/model-selection-normalize.js";
import { modelKey } from "../agents/model-ref-shared.js";
import {
buildModelAliasIndex,
resolveModelRefFromString,