mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 19:01:44 +00:00
refactor(plugins): register provider model id hooks
This commit is contained in:
@@ -9,7 +9,11 @@ import {
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { createGoogleThinkingPayloadWrapper } from "openclaw/plugin-sdk/provider-stream";
|
||||
import { GOOGLE_GEMINI_DEFAULT_MODEL, applyGoogleGeminiModelDefault } from "./api.js";
|
||||
import {
|
||||
GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||
applyGoogleGeminiModelDefault,
|
||||
normalizeGoogleModelId,
|
||||
} from "./api.js";
|
||||
import { buildGoogleGeminiCliBackend } from "./cli-backend.js";
|
||||
import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js";
|
||||
import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js";
|
||||
@@ -131,6 +135,7 @@ function createLazyGoogleGeminiCliProvider(): ProviderPlugin {
|
||||
methodId: "oauth",
|
||||
},
|
||||
},
|
||||
normalizeModelId: ({ modelId }) => normalizeGoogleModelId(modelId),
|
||||
resolveDynamicModel: (ctx) =>
|
||||
resolveGoogle31ForwardCompatModel({ providerId: GOOGLE_GEMINI_CLI_PROVIDER_ID, ctx }),
|
||||
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
|
||||
@@ -204,6 +209,7 @@ export default definePluginEntry({
|
||||
id: "google",
|
||||
label: "Google AI Studio",
|
||||
docsPath: "/providers/models",
|
||||
aliases: ["google-vertex"],
|
||||
envVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
||||
auth: [
|
||||
createProviderApiKeyAuthMethod({
|
||||
@@ -227,6 +233,7 @@ export default definePluginEntry({
|
||||
},
|
||||
}),
|
||||
],
|
||||
normalizeModelId: ({ modelId }) => normalizeGoogleModelId(modelId),
|
||||
resolveDynamicModel: (ctx) =>
|
||||
resolveGoogle31ForwardCompatModel({ providerId: "google", ctx }),
|
||||
wrapStreamFn: (ctx) => createGoogleThinkingPayloadWrapper(ctx.streamFn, ctx.thinkingLevel),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
|
||||
import { createToolStreamWrapper } from "openclaw/plugin-sdk/provider-stream";
|
||||
import { applyXaiModelCompat, buildXaiProvider } from "./api.js";
|
||||
import { applyXaiModelCompat, buildXaiProvider, normalizeXaiModelId } from "./api.js";
|
||||
import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
|
||||
import {
|
||||
@@ -57,6 +57,7 @@ export default defineSingleProviderPluginEntry({
|
||||
return createToolStreamWrapper(streamFn, ctx.extraParams?.tool_stream !== false);
|
||||
},
|
||||
normalizeResolvedModel: ({ model }) => applyXaiModelCompat(model),
|
||||
normalizeModelId: ({ modelId }) => normalizeXaiModelId(modelId),
|
||||
resolveDynamicModel: (ctx) => resolveXaiForwardCompatModel({ providerId: PROVIDER_ID, ctx }),
|
||||
isModernModelRef: ({ modelId }) => isModernXaiModel(modelId),
|
||||
},
|
||||
|
||||
41
src/agents/model-selection.plugin-runtime.test.ts
Normal file
41
src/agents/model-selection.plugin-runtime.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const normalizeProviderModelIdWithPluginMock = vi.fn();
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
normalizeProviderModelIdWithPlugin: (params: unknown) =>
|
||||
normalizeProviderModelIdWithPluginMock(params),
|
||||
}));
|
||||
|
||||
describe("model-selection plugin runtime normalization", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
normalizeProviderModelIdWithPluginMock.mockReset();
|
||||
});
|
||||
|
||||
it("delegates provider-owned model id normalization to plugin runtime hooks", async () => {
|
||||
normalizeProviderModelIdWithPluginMock.mockImplementation(({ provider, context }) => {
|
||||
if (
|
||||
provider === "xai" &&
|
||||
(context as { modelId?: string }).modelId === "grok-4.20-experimental-beta-0304-reasoning"
|
||||
) {
|
||||
return "grok-4.20-beta-latest-reasoning";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { parseModelRef } = await import("./model-selection.js");
|
||||
|
||||
expect(parseModelRef("grok-4.20-experimental-beta-0304-reasoning", "xai")).toEqual({
|
||||
provider: "xai",
|
||||
model: "grok-4.20-beta-latest-reasoning",
|
||||
});
|
||||
expect(normalizeProviderModelIdWithPluginMock).toHaveBeenCalledWith({
|
||||
provider: "xai",
|
||||
context: {
|
||||
provider: "xai",
|
||||
modelId: "grok-4.20-experimental-beta-0304-reasoning",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,9 +6,8 @@ import {
|
||||
toAgentModelListLike,
|
||||
} from "../config/model-input.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizeGoogleModelId } from "../plugin-sdk/google.js";
|
||||
import { normalizeXaiModelId } from "../plugin-sdk/xai.js";
|
||||
import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js";
|
||||
import { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
@@ -125,12 +124,6 @@ function normalizeProviderModelId(provider: string, model: string): string {
|
||||
return `anthropic/${normalizedAnthropicModel}`;
|
||||
}
|
||||
}
|
||||
if (provider === "google" || provider === "google-vertex") {
|
||||
return normalizeGoogleModelId(model);
|
||||
}
|
||||
if (provider === "xai") {
|
||||
return normalizeXaiModelId(model);
|
||||
}
|
||||
// OpenRouter-native models (e.g. "openrouter/aurora-alpha") need the full
|
||||
// "openrouter/<name>" as the model ID sent to the API. Models from external
|
||||
// providers already contain a slash (e.g. "anthropic/claude-sonnet-4-5") and
|
||||
@@ -138,7 +131,15 @@ function normalizeProviderModelId(provider: string, model: string): string {
|
||||
if (provider === "openrouter" && !model.includes("/")) {
|
||||
return `openrouter/${model}`;
|
||||
}
|
||||
return model;
|
||||
return (
|
||||
normalizeProviderModelIdWithPlugin({
|
||||
provider,
|
||||
context: {
|
||||
provider,
|
||||
modelId: model,
|
||||
},
|
||||
}) ?? model
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeModelRef(provider: string, model: string): ModelRef {
|
||||
|
||||
@@ -11,5 +11,3 @@ export type {
|
||||
export { applyNativeStreamingUsageCompat } from "./models-config.providers.policy.js";
|
||||
export { enforceSourceManagedProviderSecrets } from "./models-config.providers.source-managed.js";
|
||||
export { resolveOllamaApiBase } from "../plugin-sdk/ollama-surface.js";
|
||||
export { normalizeGoogleModelId } from "../plugin-sdk/google.js";
|
||||
export { normalizeXaiModelId } from "../plugin-sdk/xai.js";
|
||||
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
} from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizeGoogleModelId } from "../plugin-sdk/google.js";
|
||||
import { normalizeXaiModelId } from "../plugin-sdk/xai.js";
|
||||
import { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js";
|
||||
|
||||
export type CachedModelPricing = {
|
||||
input: number;
|
||||
@@ -151,12 +150,14 @@ function canonicalizeOpenRouterLookupId(id: string): string {
|
||||
.replace(/^claude-(\d+)\.(\d+)-/u, "claude-$1-$2-")
|
||||
.replace(/^claude-([a-z]+)-(\d+)\.(\d+)$/u, "claude-$1-$2-$3");
|
||||
}
|
||||
if (provider === "google") {
|
||||
model = normalizeGoogleModelId(model);
|
||||
}
|
||||
if (provider === "x-ai") {
|
||||
model = normalizeXaiModelId(model);
|
||||
}
|
||||
model =
|
||||
normalizeProviderModelIdWithPlugin({
|
||||
provider,
|
||||
context: {
|
||||
provider,
|
||||
modelId: model,
|
||||
},
|
||||
}) ?? model;
|
||||
return `${provider}/${model}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
ProviderDiscoveryContext,
|
||||
ProviderFetchUsageSnapshotContext,
|
||||
ProviderModernModelPolicyContext,
|
||||
ProviderNormalizeModelIdContext,
|
||||
ProviderNormalizeResolvedModelContext,
|
||||
ProviderPrepareDynamicModelContext,
|
||||
ProviderPrepareExtraParamsContext,
|
||||
@@ -65,6 +66,7 @@ export type {
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
ProviderFetchUsageSnapshotContext,
|
||||
ProviderModernModelPolicyContext,
|
||||
ProviderNormalizeModelIdContext,
|
||||
ProviderPreparedRuntimeAuth,
|
||||
ProviderResolvedUsageAuth,
|
||||
ProviderPrepareExtraParamsContext,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user