refactor(plugins): register provider model id hooks

This commit is contained in:
Peter Steinberger
2026-03-28 05:41:40 +00:00
parent 49f693d06a
commit c7883fe892
10 changed files with 151 additions and 21 deletions

View File

@@ -25,6 +25,7 @@ let buildProviderAuthDoctorHintWithPlugin: typeof import("./provider-runtime.js"
let buildProviderMissingAuthMessageWithPlugin: typeof import("./provider-runtime.js").buildProviderMissingAuthMessageWithPlugin;
let buildProviderUnknownModelHintWithPlugin: typeof import("./provider-runtime.js").buildProviderUnknownModelHintWithPlugin;
let formatProviderAuthProfileApiKeyWithPlugin: typeof import("./provider-runtime.js").formatProviderAuthProfileApiKeyWithPlugin;
let normalizeProviderModelIdWithPlugin: typeof import("./provider-runtime.js").normalizeProviderModelIdWithPlugin;
let prepareProviderExtraParams: typeof import("./provider-runtime.js").prepareProviderExtraParams;
let resolveProviderStreamFn: typeof import("./provider-runtime.js").resolveProviderStreamFn;
let resolveProviderCacheTtlEligibility: typeof import("./provider-runtime.js").resolveProviderCacheTtlEligibility;
@@ -192,6 +193,7 @@ describe("provider-runtime", () => {
buildProviderMissingAuthMessageWithPlugin,
buildProviderUnknownModelHintWithPlugin,
formatProviderAuthProfileApiKeyWithPlugin,
normalizeProviderModelIdWithPlugin,
prepareProviderExtraParams,
resolveProviderStreamFn,
resolveProviderCacheTtlEligibility,
@@ -247,6 +249,34 @@ describe("provider-runtime", () => {
});
});
it("can normalize model ids through provider aliases without changing ownership", () => {
resolvePluginProvidersMock.mockReturnValue([
{
id: "google",
label: "Google",
aliases: ["google-vertex"],
auth: [],
normalizeModelId: ({ modelId }) => modelId.replace("flash-lite", "flash-lite-preview"),
},
]);
expect(
normalizeProviderModelIdWithPlugin({
provider: "google-vertex",
context: {
provider: "google-vertex",
modelId: "gemini-3.1-flash-lite",
},
}),
).toBe("gemini-3.1-flash-lite-preview");
expect(resolveOwningPluginIdsForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "google-vertex",
}),
);
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
});
it("invalidates cached runtime providers when config mutates in place", () => {
const config = {
plugins: {
@@ -342,6 +372,7 @@ describe("provider-runtime", () => {
id: DEMO_PROVIDER_ID,
label: "Demo",
auth: [],
normalizeModelId: ({ modelId }) => modelId.replace("-legacy", ""),
resolveDynamicModel: () => MODEL,
prepareDynamicModel,
capabilities: {
@@ -395,6 +426,16 @@ describe("provider-runtime", () => {
}),
).toMatchObject(MODEL);
expect(
normalizeProviderModelIdWithPlugin({
provider: DEMO_PROVIDER_ID,
context: {
provider: DEMO_PROVIDER_ID,
modelId: "demo-model-legacy",
},
}),
).toBe("demo-model");
await prepareProviderDynamicModel({
provider: DEMO_PROVIDER_ID,
context: createDemoRuntimeContext({

View File

@@ -19,6 +19,7 @@ import type {
ProviderCreateStreamFnContext,
ProviderDefaultThinkingPolicyContext,
ProviderFetchUsageSnapshotContext,
ProviderNormalizeModelIdContext,
ProviderModernModelPolicyContext,
ProviderPrepareExtraParamsContext,
ProviderPrepareDynamicModelContext,
@@ -217,6 +218,25 @@ export function normalizeProviderResolvedModelWithPlugin(params: {
);
}
export function normalizeProviderModelIdWithPlugin(params: {
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
context: ProviderNormalizeModelIdContext;
}): string | undefined {
const plugin =
resolveProviderRuntimePlugin(params) ??
resolveProviderPluginsForHooks({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
}).find((candidate) => matchesProviderId(candidate, params.provider));
const normalized = plugin?.normalizeModelId?.(params.context);
const trimmed = normalized?.trim();
return trimmed ? trimmed : undefined;
}
export function resolveProviderCapabilitiesWithPlugin(params: {
provider: string;
config?: OpenClawConfig;

View File

@@ -363,6 +363,17 @@ export type ProviderNormalizeResolvedModelContext = {
model: ProviderRuntimeModel;
};
/**
* Provider-owned model-id normalization before config/runtime lookup.
*
* Use this for provider-specific alias cleanup that should stay with the
* plugin rather than in core string tables.
*/
export type ProviderNormalizeModelIdContext = {
provider: string;
modelId: string;
};
/**
* Runtime auth input for providers that need an extra exchange step before
* inference. The incoming `apiKey` is the raw credential resolved from auth
@@ -829,6 +840,13 @@ export type ProviderPlugin = {
normalizeResolvedModel?: (
ctx: ProviderNormalizeResolvedModelContext,
) => ProviderRuntimeModel | null | undefined;
/**
* Provider-owned model-id normalization.
*
* Runs before model lookup/canonicalization. Use this for alias cleanup such
* as provider-owned preview/legacy model ids.
*/
normalizeModelId?: (ctx: ProviderNormalizeModelIdContext) => string | null | undefined;
/**
* Static provider capability overrides consumed by shared transcript/tooling
* logic.