From f1a232c39588f3fe79c074e647e2e9cc875da029 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 27 Aug 2026 11:20:23 +0800 Subject: [PATCH] fix(agents): defer embedded alias normalization (#129700) --- .../model-resolution-consistency.test.ts | 78 ++++++++++++++++++- .../run/runtime-resolution.ts | 9 +++ src/agents/model-selection-config.ts | 2 + 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/agents/embedded-agent-runner/model-resolution-consistency.test.ts b/src/agents/embedded-agent-runner/model-resolution-consistency.test.ts index c95ab8461040..9239c05e6e81 100644 --- a/src/agents/embedded-agent-runner/model-resolution-consistency.test.ts +++ b/src/agents/embedded-agent-runner/model-resolution-consistency.test.ts @@ -4,6 +4,7 @@ import { resolvePreparedModelThinkingCompat, } from "../model-catalog-lookup.js"; import type { ModelCatalogEntry } from "../model-catalog.types.js"; +import { resolveModelCandidateChain } from "../model-fallback-candidates.js"; import { resolveInitialEmbeddedRunModel } from "./run/runtime-resolution.js"; const STATIC_MODEL_ID = "claude-haiku-4-5"; @@ -14,6 +15,8 @@ const resolveHookModelSelectionMock = vi.hoisted(() => modelId, })), ); +const loadManifestMetadataSnapshotMock = vi.hoisted(() => vi.fn()); +const normalizeProviderModelIdWithRuntimeMock = vi.hoisted(() => vi.fn(() => undefined)); const emptyModelRegistry = { find: vi.fn((_provider: string, _modelId: string) => null), @@ -69,7 +72,12 @@ vi.mock("./model.js", () => ({ })); vi.mock("../provider-model-normalization.runtime.js", () => ({ - normalizeProviderModelIdWithRuntime: () => undefined, + normalizeProviderModelIdWithRuntime: normalizeProviderModelIdWithRuntimeMock, +})); + +vi.mock("../../plugins/manifest-contract-eligibility.js", async (importOriginal) => ({ + ...(await importOriginal()), + loadManifestMetadataSnapshot: loadManifestMetadataSnapshotMock, })); vi.mock("../harness/runtime-plugin.js", () => ({ @@ -184,6 +192,8 @@ describe("embedded model resolution consistency", () => { provider, modelId, })); + loadManifestMetadataSnapshotMock.mockReset(); + normalizeProviderModelIdWithRuntimeMock.mockReset().mockReturnValue(undefined); }); it("resolves an explicit alias configured only on the selected agent", () => { @@ -208,6 +218,72 @@ describe("embedded model resolution consistency", () => { model: "worker-haiku", }), ).toEqual({ provider: "anthropic", modelId: "claude-haiku-4-5" }); + expect(loadManifestMetadataSnapshotMock).not.toHaveBeenCalled(); + expect(normalizeProviderModelIdWithRuntimeMock).not.toHaveBeenCalled(); + }); + + it("defers custom-provider normalization until prepared manifest policy is available", () => { + const config = { + agents: { + entries: { + worker: { + model: { primary: "worker-custom" }, + models: { + "custom-provider/legacy-model": { alias: "worker-custom" }, + }, + }, + }, + }, + }; + const initial = resolveInitialEmbeddedRunModel({ + config, + agentId: "worker", + }); + + expect(initial).toEqual({ + provider: "custom-provider", + modelId: "legacy-model", + }); + expect(loadManifestMetadataSnapshotMock).not.toHaveBeenCalled(); + expect(normalizeProviderModelIdWithRuntimeMock).not.toHaveBeenCalled(); + + const manifestPlugins = [ + { + modelIdNormalization: { + providers: { + "custom-provider": { + aliases: { "legacy-model": "modern-model" }, + }, + }, + }, + }, + ]; + expect( + resolveModelCandidateChain({ + cfg: config, + agentId: "worker", + provider: initial.provider, + model: initial.modelId, + requestedRouteResolution: "resolved", + fallbacksOverride: [], + manifestPlugins, + }), + ).toEqual([ + { + provider: "custom-provider", + model: "modern-model", + routeOrigin: "requested", + routeResolution: "resolved", + }, + ]); + expect(normalizeProviderModelIdWithRuntimeMock).toHaveBeenCalledWith({ + provider: "custom-provider", + plugins: manifestPlugins, + context: { + provider: "custom-provider", + modelId: "modern-model", + }, + }); }); it("resolves the same undated configured model for chat and manual compaction", async () => { diff --git a/src/agents/embedded-agent-runner/run/runtime-resolution.ts b/src/agents/embedded-agent-runner/run/runtime-resolution.ts index b78472174ce2..e840dc291036 100644 --- a/src/agents/embedded-agent-runner/run/runtime-resolution.ts +++ b/src/agents/embedded-agent-runner/run/runtime-resolution.ts @@ -90,9 +90,16 @@ export function resolveInitialEmbeddedRunModel(params: { model?: string; }): { provider: string; modelId: string } { const cfg = params.config ?? {}; + // Preliminary route identification stays static; prepared metadata owns + // plugin and workspace normalization once the runtime context exists. + const staticPreliminaryNormalization = { + allowManifestNormalization: false, + allowPluginNormalization: false, + } as const; const configuredDefault = resolveDefaultModelForAgent({ cfg, agentId: params.agentId, + ...staticPreliminaryNormalization, }); const explicitProvider = normalizeOptionalString(params.provider); const explicitModel = normalizeOptionalString(params.model); @@ -108,6 +115,7 @@ export function resolveInitialEmbeddedRunModel(params: { cfg, agentId: params.agentId, defaultProvider: provider, + ...staticPreliminaryNormalization, }); const resolved = resolveModelRefFromString({ cfg, @@ -115,6 +123,7 @@ export function resolveInitialEmbeddedRunModel(params: { raw: explicitModel, defaultProvider: provider, aliasIndex, + ...staticPreliminaryNormalization, }); return { provider: explicitProvider ?? resolved?.ref.provider ?? provider, diff --git a/src/agents/model-selection-config.ts b/src/agents/model-selection-config.ts index 71b4fba8f099..53f4d79ede9b 100644 --- a/src/agents/model-selection-config.ts +++ b/src/agents/model-selection-config.ts @@ -9,6 +9,7 @@ export function resolveDefaultModelForAgent( params: { cfg: OpenClawConfig; agentId?: string; + allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelRef { @@ -17,6 +18,7 @@ export function resolveDefaultModelForAgent( agentId: params.agentId, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, + allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, });