Files
openclaw/src/plugins/provider-validation.ts
2026-04-07 09:44:53 +01:00

393 lines
12 KiB
TypeScript

import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeTrimmedStringList } from "../shared/string-normalization.js";
import type { PluginDiagnostic, ProviderAuthMethod, ProviderPlugin } from "./types.js";
type ProviderWizardSetup = NonNullable<NonNullable<ProviderPlugin["wizard"]>["setup"]>;
type ProviderWizardModelPicker = NonNullable<NonNullable<ProviderPlugin["wizard"]>["modelPicker"]>;
type ProviderWizardModelAllowlist = NonNullable<ProviderWizardSetup["modelAllowlist"]>;
function pushProviderDiagnostic(params: {
level: PluginDiagnostic["level"];
pluginId: string;
source: string;
message: string;
pushDiagnostic: (diag: PluginDiagnostic) => void;
}) {
params.pushDiagnostic({
level: params.level,
pluginId: params.pluginId,
source: params.source,
message: params.message,
});
}
function normalizeTextList(values: string[] | undefined): string[] | undefined {
const normalized = Array.from(new Set(normalizeTrimmedStringList(values)));
return normalized.length > 0 ? normalized : undefined;
}
function normalizeOnboardingScopes(
values: Array<"text-inference" | "image-generation"> | undefined,
): Array<"text-inference" | "image-generation"> | undefined {
const normalized = Array.from(
new Set(
(values ?? []).filter(
(value): value is "text-inference" | "image-generation" =>
value === "text-inference" || value === "image-generation",
),
),
);
return normalized.length > 0 ? normalized : undefined;
}
function normalizeProviderOAuthProfileIdRepairs(
values: ProviderPlugin["oauthProfileIdRepairs"],
): ProviderPlugin["oauthProfileIdRepairs"] {
if (!Array.isArray(values)) {
return undefined;
}
const normalized = values
.map((value) => {
const legacyProfileId = normalizeOptionalString(value?.legacyProfileId);
const promptLabel = normalizeOptionalString(value?.promptLabel);
if (!legacyProfileId && !promptLabel) {
return null;
}
return {
...(legacyProfileId ? { legacyProfileId } : {}),
...(promptLabel ? { promptLabel } : {}),
};
})
.filter((value): value is NonNullable<typeof value> => value !== null);
return normalized.length > 0 ? normalized : undefined;
}
function resolveWizardMethodId(params: {
providerId: string;
pluginId: string;
source: string;
auth: ProviderAuthMethod[];
methodId: string | undefined;
metadataKind: "setup" | "model-picker";
pushDiagnostic: (diag: PluginDiagnostic) => void;
}): string | undefined {
if (!params.methodId) {
return undefined;
}
if (params.auth.some((method) => method.id === params.methodId)) {
return params.methodId;
}
pushProviderDiagnostic({
level: "warn",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" ${params.metadataKind} method "${params.methodId}" not found; falling back to available methods`,
pushDiagnostic: params.pushDiagnostic,
});
return undefined;
}
function buildNormalizedModelAllowlist(
modelAllowlist: ProviderWizardModelAllowlist | undefined,
): ProviderWizardModelAllowlist | undefined {
if (!modelAllowlist) {
return undefined;
}
const allowedKeys = normalizeTextList(modelAllowlist.allowedKeys);
const initialSelections = normalizeTextList(modelAllowlist.initialSelections);
const message = normalizeOptionalString(modelAllowlist.message);
if (!allowedKeys && !initialSelections && !message) {
return undefined;
}
return {
...(allowedKeys ? { allowedKeys } : {}),
...(initialSelections ? { initialSelections } : {}),
...(message ? { message } : {}),
};
}
function buildNormalizedWizardSetup(params: {
setup: ProviderWizardSetup;
methodId: string | undefined;
}): ProviderWizardSetup {
const choiceId = normalizeOptionalString(params.setup.choiceId);
const choiceLabel = normalizeOptionalString(params.setup.choiceLabel);
const choiceHint = normalizeOptionalString(params.setup.choiceHint);
const groupId = normalizeOptionalString(params.setup.groupId);
const groupLabel = normalizeOptionalString(params.setup.groupLabel);
const groupHint = normalizeOptionalString(params.setup.groupHint);
const onboardingScopes = normalizeOnboardingScopes(params.setup.onboardingScopes);
const modelAllowlist = buildNormalizedModelAllowlist(params.setup.modelAllowlist);
return {
...(choiceId ? { choiceId } : {}),
...(choiceLabel ? { choiceLabel } : {}),
...(choiceHint ? { choiceHint } : {}),
...(typeof params.setup.assistantPriority === "number" &&
Number.isFinite(params.setup.assistantPriority)
? { assistantPriority: params.setup.assistantPriority }
: {}),
...(params.setup.assistantVisibility === "manual-only" ||
params.setup.assistantVisibility === "visible"
? { assistantVisibility: params.setup.assistantVisibility }
: {}),
...(groupId ? { groupId } : {}),
...(groupLabel ? { groupLabel } : {}),
...(groupHint ? { groupHint } : {}),
...(params.methodId ? { methodId: params.methodId } : {}),
...(onboardingScopes ? { onboardingScopes } : {}),
...(modelAllowlist ? { modelAllowlist } : {}),
};
}
function buildNormalizedModelPicker(
modelPicker: ProviderWizardModelPicker,
methodId: string | undefined,
): ProviderWizardModelPicker {
const label = normalizeOptionalString(modelPicker.label);
const hint = normalizeOptionalString(modelPicker.hint);
return {
...(label ? { label } : {}),
...(hint ? { hint } : {}),
...(methodId ? { methodId } : {}),
};
}
function normalizeProviderWizardSetup(params: {
providerId: string;
pluginId: string;
source: string;
auth: ProviderAuthMethod[];
setup: ProviderWizardSetup;
pushDiagnostic: (diag: PluginDiagnostic) => void;
}): ProviderWizardSetup | undefined {
const hasAuthMethods = params.auth.length > 0;
if (!params.setup) {
return undefined;
}
if (!hasAuthMethods) {
pushProviderDiagnostic({
level: "warn",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" setup metadata ignored because it has no auth methods`,
pushDiagnostic: params.pushDiagnostic,
});
return undefined;
}
const methodId = resolveWizardMethodId({
providerId: params.providerId,
pluginId: params.pluginId,
source: params.source,
auth: params.auth,
methodId: normalizeOptionalString(params.setup.methodId),
metadataKind: "setup",
pushDiagnostic: params.pushDiagnostic,
});
return buildNormalizedWizardSetup({
setup: params.setup,
methodId,
});
}
function normalizeProviderAuthMethods(params: {
providerId: string;
pluginId: string;
source: string;
auth: ProviderAuthMethod[];
pushDiagnostic: (diag: PluginDiagnostic) => void;
}): ProviderAuthMethod[] {
const seenMethodIds = new Set<string>();
const normalized: ProviderAuthMethod[] = [];
for (const method of params.auth) {
const methodId = normalizeOptionalString(method.id);
if (!methodId) {
pushProviderDiagnostic({
level: "error",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" auth method missing id`,
pushDiagnostic: params.pushDiagnostic,
});
continue;
}
if (seenMethodIds.has(methodId)) {
pushProviderDiagnostic({
level: "error",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" auth method duplicated id "${methodId}"`,
pushDiagnostic: params.pushDiagnostic,
});
continue;
}
seenMethodIds.add(methodId);
const wizardSetup = method.wizard;
const wizard = wizardSetup
? normalizeProviderWizardSetup({
providerId: params.providerId,
pluginId: params.pluginId,
source: params.source,
auth: [{ ...method, id: methodId }],
setup: wizardSetup,
pushDiagnostic: params.pushDiagnostic,
})
: undefined;
normalized.push({
...method,
id: methodId,
label: normalizeOptionalString(method.label) ?? methodId,
...(normalizeOptionalString(method.hint)
? { hint: normalizeOptionalString(method.hint) }
: {}),
...(wizard ? { wizard } : {}),
});
}
return normalized;
}
function normalizeProviderWizard(params: {
providerId: string;
pluginId: string;
source: string;
auth: ProviderAuthMethod[];
wizard: ProviderPlugin["wizard"];
pushDiagnostic: (diag: PluginDiagnostic) => void;
}): ProviderPlugin["wizard"] {
if (!params.wizard) {
return undefined;
}
const hasAuthMethods = params.auth.length > 0;
const normalizeSetup = () => {
const setup = params.wizard?.setup;
if (!setup) {
return undefined;
}
return normalizeProviderWizardSetup({
providerId: params.providerId,
pluginId: params.pluginId,
source: params.source,
auth: params.auth,
setup,
pushDiagnostic: params.pushDiagnostic,
});
};
const normalizeModelPicker = () => {
const modelPicker = params.wizard?.modelPicker;
if (!modelPicker) {
return undefined;
}
if (!hasAuthMethods) {
pushProviderDiagnostic({
level: "warn",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" model-picker metadata ignored because it has no auth methods`,
pushDiagnostic: params.pushDiagnostic,
});
return undefined;
}
return buildNormalizedModelPicker(
modelPicker,
resolveWizardMethodId({
providerId: params.providerId,
pluginId: params.pluginId,
source: params.source,
auth: params.auth,
methodId: normalizeOptionalString(modelPicker.methodId),
metadataKind: "model-picker",
pushDiagnostic: params.pushDiagnostic,
}),
);
};
const setup = normalizeSetup();
const modelPicker = normalizeModelPicker();
if (!setup && !modelPicker) {
return undefined;
}
return {
...(setup ? { setup } : {}),
...(modelPicker ? { modelPicker } : {}),
};
}
export function normalizeRegisteredProvider(params: {
pluginId: string;
source: string;
provider: ProviderPlugin;
pushDiagnostic: (diag: PluginDiagnostic) => void;
}): ProviderPlugin | null {
const id = normalizeOptionalString(params.provider.id);
if (!id) {
pushProviderDiagnostic({
level: "error",
pluginId: params.pluginId,
source: params.source,
message: "provider registration missing id",
pushDiagnostic: params.pushDiagnostic,
});
return null;
}
const auth = normalizeProviderAuthMethods({
providerId: id,
pluginId: params.pluginId,
source: params.source,
auth: params.provider.auth ?? [],
pushDiagnostic: params.pushDiagnostic,
});
const docsPath = normalizeOptionalString(params.provider.docsPath);
const aliases = normalizeTextList(params.provider.aliases);
const deprecatedProfileIds = normalizeTextList(params.provider.deprecatedProfileIds);
const oauthProfileIdRepairs = normalizeProviderOAuthProfileIdRepairs(
params.provider.oauthProfileIdRepairs,
);
const envVars = normalizeTextList(params.provider.envVars);
const wizard = normalizeProviderWizard({
providerId: id,
pluginId: params.pluginId,
source: params.source,
auth,
wizard: params.provider.wizard,
pushDiagnostic: params.pushDiagnostic,
});
const catalog = params.provider.catalog;
const discovery = params.provider.discovery;
if (catalog && discovery) {
pushProviderDiagnostic({
level: "warn",
pluginId: params.pluginId,
source: params.source,
message: `provider "${id}" registered both catalog and discovery; using catalog`,
pushDiagnostic: params.pushDiagnostic,
});
}
const {
wizard: _ignoredWizard,
docsPath: _ignoredDocsPath,
aliases: _ignoredAliases,
envVars: _ignoredEnvVars,
catalog: _ignoredCatalog,
discovery: _ignoredDiscovery,
...restProvider
} = params.provider;
return {
...restProvider,
id,
label: normalizeOptionalString(params.provider.label) ?? id,
...(docsPath ? { docsPath } : {}),
...(aliases ? { aliases } : {}),
...(deprecatedProfileIds ? { deprecatedProfileIds } : {}),
...(oauthProfileIdRepairs ? { oauthProfileIdRepairs } : {}),
...(envVars ? { envVars } : {}),
auth,
...(catalog ? { catalog } : {}),
...(!catalog && discovery ? { discovery } : {}),
...(wizard ? { wizard } : {}),
};
}