feat(providers): keep registered setup connection-only (#142202)

Registered provider setup now saves a connection without pinning a copy of the built-in model catalog. Add shared connection-only presets and move the Cloudflare AI Gateway, Featherless, Hugging Face, LongCat, Meta, Mistral, NVIDIA, Synthetic, Together, regular Xiaomi, and Z.AI registrations to them.

Ordinary setup preserves authored rows, defaults, fallbacks, and aliases. Explicit replace mode keeps the existing catalog-merge and required-default rules. Published helper APIs keep their catalog-seeding behavior. Shared descriptors remove duplicate provider decisions, and generated replace rows belong to the resulting config.

The new SDK imports require a paired host/plugin release. The existing version-sync and package builder update the plugin API and peer ranges with that release. This change does not claim compatibility between the new plugin imports and an older released host.

Validation:

- 128 focused SDK and provider checks pass. Four connection-only checks fail when the mode condition is deliberately removed; the correct source is restored.
- Real registered NVIDIA setup saves seven generated rows on the pinned baseline and zero on the candidate. Authored merge/replace settings and the available default remain intact.
- The required CI artifact passes compiled Mistral setup for fresh, authored merge, and authored replace configurations. A real Gateway lists the configured default and returns one synthetic provider reply with matching requested, effective, and response models. Catalog and inference leave saved configuration unchanged.
- Required CI passed on the exact candidate. Runtime proof uses its recorded CI merge build and matching compiled host/plugin, with existing dependencies and isolated synthetic state. No real vendor request or package publication occurred.
- Independent acceptance passed all five clauses for the retained NVIDIA/Mistral observations. It did not replay runtime or claim coverage beyond those representative scenarios.

The earlier fixture failure from comparing resolved configuration with raw saved JSON remains recorded. Its completed setup was preserved and was not repeated. The artifact's unrelated standalone GoogleChat dependency-link failure is also retained; the exercised Mistral dependency path completed successfully.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
Ayaan Zaidi 2026-09-08 20:22:25 +05:30 • committed by GitHub
parent 37bddb2b44
commit 0f3db48abb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 457 additions and 103 deletions

View file

@ -340,10 +340,33 @@ agent/workspace paths.
If your auth flow also needs to patch `models.providers.*`, aliases, and
the agent default model during onboarding, use the preset helpers from
`openclaw/plugin-sdk/provider-onboard`. The narrowest helpers are
`createDefaultModelPresetAppliers(...)`,
`openclaw/plugin-sdk/provider-onboard`. For registered connection-only setup,
use `createProviderConnectionPresetAppliers(...)` with a lazy `catalogModels`
supplier. Ordinary setup writes connection facts, aliases, and a missing
primary without copying the built-in catalog into saved configuration.
Explicit `models.mode: "replace"` evaluates the supplier and merges generated
rows behind authored rows. Each setup result owns its generated model data.
Use `createDefaultModelsConnectionPresetAppliers(...)` when replace mode must
retain the existing required-default rule: add the supplied `defaultModels`
only when the configured provider lacks the selected `defaultModelId`.
`applyProviderConnectionConfig(...)` provides the catalog variant for auth
flows that resolve their endpoint or primary per invocation. These helpers
preserve authored rows, existing aliases and fallbacks. The auth flow still
owns any explicit default selection.
The existing public `createDefaultModelPresetAppliers(...)`,
`createDefaultModelsPresetAppliers(...)`, and
`createModelCatalogPresetAppliers(...)`.
`createModelCatalogPresetAppliers(...)` retain their catalog-seeding behavior
in ordinary mode. The latter two also accept lazy model suppliers. A provider
with published config helpers can share one preset descriptor between its
existing helper and its new registered setup helper; migrate the registration
without silently changing the published helper's contract.
Independently published plugins must require the host release containing these
helpers in `openclaw.compat.pluginApi`. The core release version-sync tool
updates that range with the paired release; these imports do not work on an
older host that lacks the helpers.
When a provider's native endpoint supports streamed usage blocks on the
normal `openai-completions` transport, prefer the shared catalog helpers in

View file

@ -17,7 +17,10 @@ import { upsertAuthProfileWithLockOrThrow } from "openclaw/plugin-sdk/provider-a
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { buildCloudflareAiGatewayCatalogProvider } from "./catalog-provider.js";
import { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF } from "./models.js";
import { applyCloudflareAiGatewayConfig, buildCloudflareAiGatewayConfigPatch } from "./onboard.js";
import {
applyCloudflareAiGatewayConnectionConfig,
applyCloudflareAiGatewayProviderConnectionConfig,
} from "./onboard.js";
import { wrapCloudflareAiGatewayProviderStream } from "./stream-wrappers.js";
const PROVIDER_ID = "cloudflare-ai-gateway";
@ -132,7 +135,7 @@ export default definePluginEntry({
),
},
],
configPatch: buildCloudflareAiGatewayConfigPatch(metadata),
configPatch: applyCloudflareAiGatewayProviderConnectionConfig(ctx.config, metadata),
defaultModel: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
};
},
@ -195,7 +198,7 @@ export default definePluginEntry({
provider: PROVIDER_ID,
mode: "api_key",
});
return applyCloudflareAiGatewayConfig(next, { accountId, gatewayId });
return applyCloudflareAiGatewayConnectionConfig(next, { accountId, gatewayId });
},
},
],

View file

@ -5,6 +5,7 @@
import {
applyAgentDefaultModelPrimary,
applyProviderConfigWithDefaultModel,
applyProviderConnectionConfig,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import {
@ -102,3 +103,29 @@ export function applyCloudflareAiGatewayConfig(
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
);
}
/** Registered setup keeps authored rows and seeds the default only in replace mode. */
export function applyCloudflareAiGatewayProviderConnectionConfig(
cfg: OpenClawConfig,
params: { accountId: string; gatewayId: string },
): OpenClawConfig {
return applyProviderConnectionConfig(cfg, {
providerId: "cloudflare-ai-gateway",
api: "anthropic-messages",
baseUrl: resolveCloudflareAiGatewayBaseUrl(params),
catalogModels: () => [buildCloudflareAiGatewayModelDefinition()],
aliases: [
{ modelRef: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, alias: "Cloudflare AI Gateway" },
],
});
}
export function applyCloudflareAiGatewayConnectionConfig(
cfg: OpenClawConfig,
params: { accountId: string; gatewayId: string },
): OpenClawConfig {
return applyAgentDefaultModelPrimary(
applyCloudflareAiGatewayProviderConnectionConfig(cfg, params),
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
);
}

View file

@ -11,7 +11,7 @@ import {
normalizeModelCompat,
} from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { applyFeatherlessConfig, FEATHERLESS_DEFAULT_MODEL_REF } from "./onboard.js";
import { applyFeatherlessConnectionConfig, FEATHERLESS_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import {
FEATHERLESS_BASE_URL,
@ -81,7 +81,7 @@ export default defineSingleProviderPluginEntry({
docsPath: "/providers/featherless",
manifestAuth: {
defaultModel: FEATHERLESS_DEFAULT_MODEL_REF,
applyConfig: applyFeatherlessConfig,
applyConfig: applyFeatherlessConnectionConfig,
noteTitle: "Featherless AI",
noteMessage: [
"Featherless AI serves open models through an OpenAI-compatible API.",

View file

@ -1,5 +1,8 @@
// Featherless onboarding applies the curated model catalog and default.
import { createModelCatalogPresetAppliers } from "openclaw/plugin-sdk/provider-onboard";
import {
createModelCatalogPresetAppliers,
createProviderConnectionPresetAppliers,
} from "openclaw/plugin-sdk/provider-onboard";
import {
buildFeatherlessCatalogModels,
FEATHERLESS_BASE_URL,
@ -8,13 +11,18 @@ import {
export { FEATHERLESS_DEFAULT_MODEL_REF } from "./models.js";
export const { applyConfig: applyFeatherlessConfig } = createModelCatalogPresetAppliers<[]>({
const featherlessPreset = {
primaryModelRef: FEATHERLESS_DEFAULT_MODEL_REF,
resolveParams: () => ({
providerId: "featherless",
api: "openai-completions",
baseUrl: FEATHERLESS_BASE_URL,
catalogModels: buildFeatherlessCatalogModels(),
catalogModels: buildFeatherlessCatalogModels,
aliases: [{ modelRef: FEATHERLESS_DEFAULT_MODEL_REF, alias: "Qwen3 32B" }],
}),
});
} satisfies Parameters<typeof createProviderConnectionPresetAppliers<[]>>[0];
export const { applyConfig: applyFeatherlessConfig } =
createModelCatalogPresetAppliers(featherlessPreset);
export const { applyConfig: applyFeatherlessConnectionConfig } =
createProviderConnectionPresetAppliers(featherlessPreset);

View file

@ -1,6 +1,6 @@
import { runLiveProviderCatalog } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyHuggingfaceConfig, HUGGINGFACE_DEFAULT_MODEL_REF } from "./onboard.js";
import { applyHuggingfaceConnectionConfig, HUGGINGFACE_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildHuggingfaceProvider } from "./provider-catalog.js";
@ -23,7 +23,7 @@ export default defineSingleProviderPluginEntry({
envVars: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
manifestAuth: {
defaultModel: HUGGINGFACE_DEFAULT_MODEL_REF,
applyConfig: applyHuggingfaceConfig,
applyConfig: applyHuggingfaceConnectionConfig,
},
catalog: {
run: async (ctx) => {

View file

@ -1,16 +1,24 @@
// Huggingface setup module handles plugin onboarding behavior.
import { createModelCatalogPresetAppliers } from "openclaw/plugin-sdk/provider-onboard";
import {
createModelCatalogPresetAppliers,
createProviderConnectionPresetAppliers,
} from "openclaw/plugin-sdk/provider-onboard";
import { HUGGINGFACE_BASE_URL, HUGGINGFACE_MODEL_CATALOG } from "./models.js";
export const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R1";
export const { applyConfig: applyHuggingfaceConfig } = createModelCatalogPresetAppliers<[]>({
const huggingfacePreset = {
primaryModelRef: HUGGINGFACE_DEFAULT_MODEL_REF,
resolveParams: () => ({
providerId: "huggingface",
api: "openai-completions",
baseUrl: HUGGINGFACE_BASE_URL,
catalogModels: HUGGINGFACE_MODEL_CATALOG.map((model) => Object.assign({}, model)),
catalogModels: () => HUGGINGFACE_MODEL_CATALOG.map((model) => Object.assign({}, model)),
aliases: [{ modelRef: HUGGINGFACE_DEFAULT_MODEL_REF, alias: "Hugging Face" }],
}),
});
} satisfies Parameters<typeof createProviderConnectionPresetAppliers<[]>>[0];
export const { applyConfig: applyHuggingfaceConfig } =
createModelCatalogPresetAppliers(huggingfacePreset);
export const { applyConfig: applyHuggingfaceConnectionConfig } =
createProviderConnectionPresetAppliers(huggingfacePreset);

View file

@ -3,7 +3,7 @@ import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-en
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { LONGCAT_DEFAULT_MODEL_REF } from "./models.js";
import { applyLongCatConfig } from "./onboard.js";
import { applyLongCatConnectionConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { createLongCatThinkingWrapper } from "./stream.js";
@ -20,7 +20,7 @@ export default defineSingleProviderPluginEntry({
aliases: ["meituan-longcat"],
manifestAuth: {
defaultModel: LONGCAT_DEFAULT_MODEL_REF,
applyConfig: applyLongCatConfig,
applyConfig: applyLongCatConnectionConfig,
noteTitle: "LongCat",
noteMessage: "Manage API keys at https://longcat.chat/platform/api_keys",
},

View file

@ -1,14 +1,21 @@
// LongCat setup module handles plugin onboarding behavior.
import { createModelCatalogPresetAppliers } from "openclaw/plugin-sdk/provider-onboard";
import {
createModelCatalogPresetAppliers,
createProviderConnectionPresetAppliers,
} from "openclaw/plugin-sdk/provider-onboard";
import { LONGCAT_BASE_URL, LONGCAT_DEFAULT_MODEL_REF, LONGCAT_MODEL_CATALOG } from "./models.js";
export const { applyConfig: applyLongCatConfig } = createModelCatalogPresetAppliers<[]>({
const longCatPreset = {
primaryModelRef: LONGCAT_DEFAULT_MODEL_REF,
resolveParams: () => ({
providerId: "longcat",
api: "openai-completions",
baseUrl: LONGCAT_BASE_URL,
catalogModels: LONGCAT_MODEL_CATALOG,
catalogModels: () => LONGCAT_MODEL_CATALOG,
aliases: [{ modelRef: LONGCAT_DEFAULT_MODEL_REF, alias: "LongCat 2.0" }],
}),
});
} satisfies Parameters<typeof createProviderConnectionPresetAppliers<[]>>[0];
export const { applyConfig: applyLongCatConfig } = createModelCatalogPresetAppliers(longCatPreset);
export const { applyConfig: applyLongCatConnectionConfig } =
createProviderConnectionPresetAppliers(longCatPreset);

View file

@ -3,7 +3,7 @@
*/
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { applyMetaConfig } from "./onboard.js";
import { applyMetaConnectionConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildMetaProvider } from "./provider-catalog.js";
import { wrapMetaProviderStream } from "./stream.js";
@ -20,7 +20,7 @@ export default defineSingleProviderPluginEntry({
label: "Meta",
docsPath: "/providers/meta",
manifestAuth: {
applyConfig: applyMetaConfig,
applyConfig: applyMetaConnectionConfig,
noteMessage: "Meta provides Responses API inference.",
noteTitle: "Meta",
},

View file

@ -2,7 +2,10 @@
* Meta onboarding config helpers.
*/
import { readManifestProviderDefaultModelRef } from "openclaw/plugin-sdk/provider-catalog-shared";
import { createModelCatalogPresetAppliers } from "openclaw/plugin-sdk/provider-onboard";
import {
createModelCatalogPresetAppliers,
createProviderConnectionPresetAppliers,
} from "openclaw/plugin-sdk/provider-onboard";
import { buildMetaCatalogModels, META_BASE_URL } from "./models.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
@ -10,13 +13,17 @@ import manifest from "./openclaw.plugin.json" with { type: "json" };
export const META_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(manifest, "meta")!;
/** Applies Meta provider/catalog config and default model aliases. */
export const { applyConfig: applyMetaConfig } = createModelCatalogPresetAppliers<[]>({
const metaPreset = {
primaryModelRef: META_DEFAULT_MODEL_REF,
resolveParams: () => ({
providerId: "meta",
api: "openai-responses",
baseUrl: META_BASE_URL,
catalogModels: buildMetaCatalogModels(),
catalogModels: buildMetaCatalogModels,
aliases: [{ modelRef: META_DEFAULT_MODEL_REF, alias: "Muse Spark 1.3" }],
}),
});
} satisfies Parameters<typeof createProviderConnectionPresetAppliers<[]>>[0];
export const { applyConfig: applyMetaConfig } = createModelCatalogPresetAppliers(metaPreset);
export const { applyConfig: applyMetaConnectionConfig } =
createProviderConnectionPresetAppliers(metaPreset);

View file

@ -7,7 +7,7 @@ import {
} from "./api.js";
import { mistralMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { mistralMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
import { applyMistralConfig } from "./onboard.js";
import { applyMistralConnectionConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildMistralRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
@ -27,7 +27,7 @@ export default defineSingleProviderPluginEntry({
provider: {
label: "Mistral",
docsPath: "/providers/models",
manifestAuth: { applyConfig: applyMistralConfig },
manifestAuth: { applyConfig: applyMistralConnectionConfig },
catalog: {
discoveryMode: "strict",
allowExplicitBaseUrl: true,

View file

@ -1,4 +1,7 @@
import { createDefaultModelPresetAppliers } from "openclaw/plugin-sdk/provider-onboard";
import {
createDefaultModelsPresetAppliers,
createDefaultModelsConnectionPresetAppliers,
} from "openclaw/plugin-sdk/provider-onboard";
import {
buildMistralModelDefinition,
MISTRAL_BASE_URL,
@ -6,15 +9,19 @@ import {
MISTRAL_DEFAULT_MODEL_REF,
} from "./model-definitions.js";
const mistralPreset = {
primaryModelRef: MISTRAL_DEFAULT_MODEL_REF,
resolveParams: () => ({
providerId: "mistral",
api: "openai-completions",
baseUrl: MISTRAL_BASE_URL,
defaultModels: () => [buildMistralModelDefinition()],
defaultModelId: MISTRAL_DEFAULT_MODEL_ID,
aliases: [{ modelRef: MISTRAL_DEFAULT_MODEL_REF, alias: "Mistral" }],
}),
} satisfies Parameters<typeof createDefaultModelsConnectionPresetAppliers<[]>>[0];
export const { applyConfig: applyMistralConfig, applyProviderConfig: applyMistralProviderConfig } =
createDefaultModelPresetAppliers<[]>({
primaryModelRef: MISTRAL_DEFAULT_MODEL_REF,
resolveParams: () => ({
providerId: "mistral",
api: "openai-completions",
baseUrl: MISTRAL_BASE_URL,
defaultModel: buildMistralModelDefinition(),
defaultModelId: MISTRAL_DEFAULT_MODEL_ID,
aliases: [{ modelRef: MISTRAL_DEFAULT_MODEL_REF, alias: "Mistral" }],
}),
});
createDefaultModelsPresetAppliers(mistralPreset);
export const { applyConfig: applyMistralConnectionConfig } =
createDefaultModelsConnectionPresetAppliers(mistralPreset);

View file

@ -1,6 +1,6 @@
// Nvidia plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyNvidiaConfig, NVIDIA_DEFAULT_MODEL_REF } from "./onboard.js";
import { applyNvidiaConnectionConfig, NVIDIA_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildLiveNvidiaProvider, buildSelectableNvidiaProvider } from "./provider-catalog.js";
@ -17,7 +17,7 @@ export default defineSingleProviderPluginEntry({
preserveLiteralProviderPrefix: true,
manifestAuth: {
defaultModel: NVIDIA_DEFAULT_MODEL_REF,
applyConfig: applyNvidiaConfig,
applyConfig: applyNvidiaConnectionConfig,
},
catalog: {
discoveryMode: "strict",

View file

@ -1,21 +1,28 @@
// Nvidia setup module handles plugin onboarding behavior.
import { createDefaultModelsPresetAppliers } from "openclaw/plugin-sdk/provider-onboard";
import {
createDefaultModelsPresetAppliers,
createDefaultModelsConnectionPresetAppliers,
} from "openclaw/plugin-sdk/provider-onboard";
import { buildSelectableNvidiaProvider, NVIDIA_DEFAULT_MODEL_ID } from "./provider-catalog.js";
export const NVIDIA_DEFAULT_MODEL_REF = NVIDIA_DEFAULT_MODEL_ID;
const nvidiaPreset = {
primaryModelRef: NVIDIA_DEFAULT_MODEL_REF,
resolveParams: () => {
const defaultProvider = buildSelectableNvidiaProvider();
return {
providerId: "nvidia",
api: defaultProvider.api ?? "openai-completions",
baseUrl: defaultProvider.baseUrl,
defaultModels: () => defaultProvider.models ?? [],
defaultModelId: NVIDIA_DEFAULT_MODEL_ID,
aliases: [{ modelRef: NVIDIA_DEFAULT_MODEL_REF, alias: "NVIDIA" }],
};
},
} satisfies Parameters<typeof createDefaultModelsConnectionPresetAppliers<[]>>[0];
export const { applyConfig: applyNvidiaConfig, applyProviderConfig: applyNvidiaProviderConfig } =
createDefaultModelsPresetAppliers<[]>({
primaryModelRef: NVIDIA_DEFAULT_MODEL_REF,
resolveParams: () => {
const defaultProvider = buildSelectableNvidiaProvider();
return {
providerId: "nvidia",
api: defaultProvider.api ?? "openai-completions",
baseUrl: defaultProvider.baseUrl,
defaultModels: defaultProvider.models ?? [],
defaultModelId: NVIDIA_DEFAULT_MODEL_ID,
aliases: [{ modelRef: NVIDIA_DEFAULT_MODEL_REF, alias: "NVIDIA" }],
};
},
});
createDefaultModelsPresetAppliers(nvidiaPreset);
export const { applyConfig: applyNvidiaConnectionConfig } =
createDefaultModelsConnectionPresetAppliers(nvidiaPreset);

View file

@ -1,6 +1,6 @@
// Synthetic plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applySyntheticConfig, SYNTHETIC_DEFAULT_MODEL_REF } from "./onboard.js";
import { applySyntheticConnectionConfig, SYNTHETIC_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildSyntheticProvider, SYNTHETIC_MODEL_DISCOVERY } from "./provider-catalog.js";
@ -16,7 +16,7 @@ export default defineSingleProviderPluginEntry({
docsPath: "/providers/synthetic",
manifestAuth: {
defaultModel: SYNTHETIC_DEFAULT_MODEL_REF,
applyConfig: applySyntheticConfig,
applyConfig: applySyntheticConnectionConfig,
},
catalog: {
discoveryMode: "strict",

View file

@ -1,5 +1,8 @@
// Synthetic setup module handles plugin onboarding behavior.
import { createModelCatalogPresetAppliers } from "openclaw/plugin-sdk/provider-onboard";
import {
createModelCatalogPresetAppliers,
createProviderConnectionPresetAppliers,
} from "openclaw/plugin-sdk/provider-onboard";
import {
buildSyntheticModelDefinition,
SYNTHETIC_BASE_URL,
@ -9,16 +12,20 @@ import {
export { SYNTHETIC_DEFAULT_MODEL_REF };
export const {
applyConfig: applySyntheticConfig,
applyProviderConfig: applySyntheticProviderConfig,
} = createModelCatalogPresetAppliers<[]>({
const syntheticPreset = {
primaryModelRef: SYNTHETIC_DEFAULT_MODEL_REF,
resolveParams: () => ({
providerId: "synthetic",
api: "anthropic-messages",
baseUrl: SYNTHETIC_BASE_URL,
catalogModels: SYNTHETIC_MODEL_CATALOG.map(buildSyntheticModelDefinition),
catalogModels: () => SYNTHETIC_MODEL_CATALOG.map(buildSyntheticModelDefinition),
aliases: [{ modelRef: SYNTHETIC_DEFAULT_MODEL_REF, alias: "MiniMax M3" }],
}),
});
} satisfies Parameters<typeof createProviderConnectionPresetAppliers<[]>>[0];
export const {
applyConfig: applySyntheticConfig,
applyProviderConfig: applySyntheticProviderConfig,
} = createModelCatalogPresetAppliers(syntheticPreset);
export const { applyConfig: applySyntheticConnectionConfig } =
createProviderConnectionPresetAppliers(syntheticPreset);

View file

@ -1,6 +1,6 @@
// Together plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyTogetherConfig } from "./onboard.js";
import { applyTogetherConnectionConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildTogetherVideoGenerationProvider } from "./video-generation-provider.js";
@ -14,7 +14,7 @@ export default defineSingleProviderPluginEntry({
provider: {
label: "Together",
docsPath: "/providers/together",
manifestAuth: { applyConfig: applyTogetherConfig },
manifestAuth: { applyConfig: applyTogetherConnectionConfig },
catalog: { liveModelDiscovery: true, discoveryMode: "strict" },
classifyFailoverReason: ({ errorMessage }) =>
/\bconcurrency limit\b.*\b(?:breached|reached)\b/i.test(errorMessage)

View file

@ -1,6 +1,9 @@
// Together setup module handles plugin onboarding behavior.
import { readManifestProviderDefaultModelRef } from "openclaw/plugin-sdk/provider-catalog-shared";
import { createModelCatalogPresetAppliers } from "openclaw/plugin-sdk/provider-onboard";
import {
createModelCatalogPresetAppliers,
createProviderConnectionPresetAppliers,
} from "openclaw/plugin-sdk/provider-onboard";
import { TOGETHER_BASE_URL, TOGETHER_MODEL_CATALOG } from "./models.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
@ -9,13 +12,18 @@ export const TOGETHER_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(
"together",
)!;
export const { applyConfig: applyTogetherConfig } = createModelCatalogPresetAppliers<[]>({
const togetherPreset = {
primaryModelRef: TOGETHER_DEFAULT_MODEL_REF,
resolveParams: () => ({
providerId: "together",
api: "openai-completions",
baseUrl: TOGETHER_BASE_URL,
catalogModels: structuredClone(TOGETHER_MODEL_CATALOG),
catalogModels: () => structuredClone(TOGETHER_MODEL_CATALOG),
aliases: [{ modelRef: TOGETHER_DEFAULT_MODEL_REF, alias: "Together AI" }],
}),
});
} satisfies Parameters<typeof createProviderConnectionPresetAppliers<[]>>[0];
export const { applyConfig: applyTogetherConfig } =
createModelCatalogPresetAppliers(togetherPreset);
export const { applyConfig: applyTogetherConnectionConfig } =
createProviderConnectionPresetAppliers(togetherPreset);

View file

@ -26,7 +26,7 @@ import {
} from "openclaw/plugin-sdk/provider-model-shared";
import { PROVIDER_LABELS } from "openclaw/plugin-sdk/provider-usage";
import {
applyXiaomiConfig,
applyXiaomiConnectionConfig,
applyXiaomiTokenPlanConfig,
XIAOMI_DEFAULT_MODEL_REF,
XIAOMI_TOKEN_PLAN_DEFAULT_MODEL_REF,
@ -299,7 +299,7 @@ function createPaygAuthMethod(): ProviderAuthMethod {
promptMessage: "Enter Xiaomi MiMo API key (pay-as-you-go, sk-...)",
expectedKind: "payg",
defaultModel: XIAOMI_DEFAULT_MODEL_REF,
applyConfig: applyXiaomiConfig,
applyConfig: applyXiaomiConnectionConfig,
}),
runNonInteractive: async (ctx) =>
await runXiaomiApiKeyAuthNonInteractive(ctx, {
@ -308,7 +308,7 @@ function createPaygAuthMethod(): ProviderAuthMethod {
flagName: PAYG_FLAG_NAME,
envVar: PAYG_ENV_VAR,
expectedKind: "payg",
applyConfig: applyXiaomiConfig,
applyConfig: applyXiaomiConnectionConfig,
}),
};
}

View file

@ -1,6 +1,7 @@
// Xiaomi setup module handles plugin onboarding behavior.
import {
createDefaultModelsPresetAppliers,
createDefaultModelsConnectionPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import {
@ -17,21 +18,25 @@ import {
export const XIAOMI_DEFAULT_MODEL_REF = `${XIAOMI_PROVIDER_ID}/${XIAOMI_DEFAULT_MODEL_ID}`;
export const XIAOMI_TOKEN_PLAN_DEFAULT_MODEL_REF = `${XIAOMI_TOKEN_PLAN_PROVIDER_ID}/${XIAOMI_TOKEN_PLAN_DEFAULT_MODEL_ID}`;
const xiaomiPreset = {
primaryModelRef: XIAOMI_DEFAULT_MODEL_REF,
resolveParams: () => {
const defaultProvider = buildXiaomiProvider();
return {
providerId: XIAOMI_PROVIDER_ID,
api: defaultProvider.api ?? "openai-completions",
baseUrl: defaultProvider.baseUrl,
defaultModels: () => defaultProvider.models ?? [],
defaultModelId: XIAOMI_DEFAULT_MODEL_ID,
aliases: [{ modelRef: XIAOMI_DEFAULT_MODEL_REF, alias: "Xiaomi" }],
};
},
} satisfies Parameters<typeof createDefaultModelsConnectionPresetAppliers<[]>>[0];
export const { applyConfig: applyXiaomiConfig, applyProviderConfig: applyXiaomiProviderConfig } =
createDefaultModelsPresetAppliers<[]>({
primaryModelRef: XIAOMI_DEFAULT_MODEL_REF,
resolveParams: () => {
const defaultProvider = buildXiaomiProvider();
return {
providerId: XIAOMI_PROVIDER_ID,
api: defaultProvider.api ?? "openai-completions",
baseUrl: defaultProvider.baseUrl,
defaultModels: defaultProvider.models ?? [],
defaultModelId: XIAOMI_DEFAULT_MODEL_ID,
aliases: [{ modelRef: XIAOMI_DEFAULT_MODEL_REF, alias: "Xiaomi" }],
};
},
});
createDefaultModelsPresetAppliers(xiaomiPreset);
export const { applyConfig: applyXiaomiConnectionConfig } =
createDefaultModelsConnectionPresetAppliers(xiaomiPreset);
const xiaomiTokenPlanPresetAppliers = createDefaultModelsPresetAppliers<[]>({
primaryModelRef: XIAOMI_TOKEN_PLAN_DEFAULT_MODEL_REF,

View file

@ -34,7 +34,11 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coer
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
import { zaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { buildZaiModelDefinition, resolveZaiBaseUrl } from "./model-definitions.js";
import { applyZaiConfig, applyZaiProviderConfig, resolveZaiModelId } from "./onboard.js";
import {
applyZaiConnectionConfig,
applyZaiProviderConnectionConfig,
resolveZaiModelId,
} from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { resolveThinkingProfile, resolveZaiReasoningEffort } from "./provider-policy-api.js";
@ -169,7 +173,7 @@ async function runZaiApiKeyAuth(
endpoint?: ZaiEndpointId,
): Promise<{
profiles: Array<{ profileId: string; credential: ReturnType<typeof buildApiKeyCredential> }>;
configPatch: ReturnType<typeof applyZaiProviderConfig>;
configPatch: ReturnType<typeof applyZaiProviderConnectionConfig>;
defaultModel: string;
notes?: string[];
}> {
@ -226,7 +230,7 @@ async function runZaiApiKeyAuth(
),
},
],
configPatch: applyZaiProviderConfig(ctx.config, preset),
configPatch: applyZaiProviderConnectionConfig(ctx.config, preset),
defaultModel: `zai/${resolveZaiModelId(preset)}`,
...(detected?.note ? { notes: [detected.note] } : {}),
};
@ -272,7 +276,7 @@ async function runZaiApiKeyAuthNonInteractive(
provider: PROVIDER_ID,
mode: "api_key",
});
return applyZaiConfig(next, {
return applyZaiConnectionConfig(next, {
...(nextEndpoint ? { endpoint: nextEndpoint } : {}),
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
});

View file

@ -1,6 +1,7 @@
// Zai setup module handles plugin onboarding behavior.
import {
applyProviderConfigWithModelCatalogPreset,
applyProviderConnectionConfig,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
@ -42,15 +43,16 @@ function applyZaiPreset(
cfg: OpenClawConfig,
params?: { endpoint?: string; modelId?: string },
primaryModelRef?: string,
applyPreset = applyProviderConfigWithModelCatalogPreset,
): OpenClawConfig {
const baseUrl = resolveZaiPresetBaseUrl(cfg, params?.endpoint);
const modelId = resolveZaiModelId({ ...params, baseUrl });
const modelRef = `zai/${modelId}`;
return applyProviderConfigWithModelCatalogPreset(cfg, {
return applyPreset(cfg, {
providerId: "zai",
api: "openai-completions",
baseUrl,
catalogModels: buildZaiCatalogModels(),
catalogModels: buildZaiCatalogModels,
aliases: [{ modelRef, alias: "GLM" }],
primaryModelRef,
});
@ -72,3 +74,19 @@ export function applyZaiConfig(
const modelRef = modelId === ZAI_DEFAULT_MODEL_ID ? ZAI_DEFAULT_MODEL_REF : `zai/${modelId}`;
return applyZaiPreset(cfg, params, modelRef);
}
export function applyZaiProviderConnectionConfig(
cfg: OpenClawConfig,
params?: { endpoint?: string; modelId?: string },
): OpenClawConfig {
return applyZaiPreset(cfg, params, undefined, applyProviderConnectionConfig);
}
export function applyZaiConnectionConfig(
cfg: OpenClawConfig,
params?: { endpoint?: string; modelId?: string },
): OpenClawConfig {
const baseUrl = resolveZaiPresetBaseUrl(cfg, params?.endpoint);
const modelId = resolveZaiModelId({ ...params, baseUrl });
return applyZaiPreset(cfg, params, `zai/${modelId}`, applyProviderConnectionConfig);
}

View file

@ -0,0 +1,170 @@
import { describe, expect, it } from "vitest";
import {
createDefaultModelsConnectionPresetAppliers,
createDefaultModelsPresetAppliers,
createModelCatalogPresetAppliers,
createProviderConnectionPresetAppliers,
type ModelDefinitionConfig,
type OpenClawConfig,
} from "./provider-onboard.js";
function model(id: string): ModelDefinitionConfig {
return {
id,
name: id,
reasoning: false,
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
contextWindow: 8192,
maxTokens: 1024,
};
}
function preset(models: () => ModelDefinitionConfig[]) {
return {
primaryModelRef: "fixture/default",
resolveParams: () => ({
providerId: "fixture",
api: "openai-completions" as const,
baseUrl: "https://fixture.invalid/v1",
catalogModels: models,
defaultModels: models,
defaultModelId: "default",
aliases: [{ modelRef: "fixture/default", alias: "Default" }],
}),
};
}
describe.each([
["catalog", createProviderConnectionPresetAppliers<[]>],
["default models", createDefaultModelsConnectionPresetAppliers<[]>],
] as const)("connection-only %s setup", (_name, create) => {
it.each([undefined, "merge"] as const)(
"writes a connection without needing catalog data in %s mode",
(mode) => {
const appliers = create(
preset(() => {
throw new Error("Catalog data is unavailable");
}),
);
const result = appliers.applyConfig(mode ? { models: { mode } } : {});
expect(result.models?.providers?.fixture).toEqual({
api: "openai-completions",
baseUrl: "https://fixture.invalid/v1",
models: [],
});
expect(result.agents?.defaults?.model).toEqual({ primary: "fixture/default" });
expect(result.agents?.defaults?.models).toEqual({
"fixture/default": { alias: "Default" },
});
},
);
it("keeps authored native-named and unique rows, defaults, fallbacks and aliases", () => {
const authoredDefault = { ...model("default"), name: "Authored", contextWindow: 32768 };
const authoredUnique = model("private-choice");
const config: OpenClawConfig = {
models: {
mode: "merge",
providers: {
fixture: {
baseUrl: "https://fixture.invalid/v1",
models: [authoredDefault, authoredUnique],
},
},
},
agents: {
defaults: {
model: { primary: "other/chosen", fallbacks: ["other/backup", "fixture/private-choice"] },
models: { "fixture/default": { alias: "Authored alias", params: { temperature: 0.3 } } },
},
},
};
const result = create(preset(() => [model("default"), model("extra")])).applyConfig(config);
expect(result.models?.providers?.fixture?.models).toEqual([authoredDefault, authoredUnique]);
expect(result.agents?.defaults?.model).toEqual(config.agents?.defaults?.model);
expect(result.agents?.defaults?.models).toEqual(config.agents?.defaults?.models);
expect(config.models?.providers?.fixture?.models).toEqual([authoredDefault, authoredUnique]);
});
it("keeps provider-only setup from changing the primary", () => {
const result = create(preset(() => [model("default")])).applyProviderConfig({});
expect(result.models?.providers?.fixture?.models).toEqual([]);
expect(result.agents?.defaults?.model).toBeUndefined();
});
it("owns generated replace rows independently from the catalog and other setup calls", () => {
const catalog = [model("default")];
const appliers = create(preset(() => catalog));
const first = appliers.applyConfig({ models: { mode: "replace" } });
const firstModel = first.models!.providers!.fixture!.models[0]!;
firstModel.cost.input = 91;
firstModel.input.push("image");
const second = appliers.applyConfig({ models: { mode: "replace" } });
expect(catalog[0]!.cost.input).toBe(1);
expect(catalog[0]!.input).toEqual(["text"]);
expect(second.models?.providers?.fixture?.models[0]?.cost.input).toBe(1);
expect(second.models?.providers?.fixture?.models[0]?.input).toEqual(["text"]);
});
it("preserves a resolver's intentional no-op", () => {
const config: OpenClawConfig = { agents: { defaults: { model: { fallbacks: [] } } } };
const result = create({
primaryModelRef: "fixture/default",
resolveParams: () => null,
}).applyConfig(config);
expect(result).toBe(config);
});
});
it("keeps catalog and default-model membership rules distinct in replace mode", () => {
const authored = { ...model("default"), name: "Authored" };
const config: OpenClawConfig = {
models: {
mode: "replace",
providers: { fixture: { baseUrl: "https://fixture.invalid/v1", models: [authored] } },
},
};
const params = preset(() => [model("default"), model("extra")]);
const catalog = createProviderConnectionPresetAppliers(params).applyConfig(config);
const defaults = createDefaultModelsConnectionPresetAppliers(params).applyConfig(config);
expect(catalog.models?.providers?.fixture?.models).toEqual([authored, model("extra")]);
expect(defaults.models?.providers?.fixture?.models).toEqual([authored]);
});
it("still adds required defaults in replace mode when the authored rows omit the default", () => {
const authored = model("private-choice");
const result = createDefaultModelsConnectionPresetAppliers(
preset(() => [model("default"), model("extra")]),
).applyConfig({
models: {
mode: "replace",
providers: { fixture: { baseUrl: "https://fixture.invalid/v1", models: [authored] } },
},
});
expect(result.models?.providers?.fixture?.models).toEqual([
authored,
model("default"),
model("extra"),
]);
});
it.each([createModelCatalogPresetAppliers<[]>, createDefaultModelsPresetAppliers<[]>])(
"preserves the existing public catalog-seeding helper behavior",
(create) => {
const result = create(preset(() => [model("default")])).applyConfig({});
expect(result.models?.providers?.fixture?.models).toEqual([model("default")]);
},
);

View file

@ -49,6 +49,19 @@ export type ProviderOnboardPresetAppliers<TArgs extends unknown[]> = {
applyConfig: (cfg: OpenClawConfig, ...args: TArgs) => OpenClawConfig;
};
type ProviderPresetModels = ModelDefinitionConfig[] | (() => ModelDefinitionConfig[]);
function resolvePresetModels(models: ProviderPresetModels): ModelDefinitionConfig[] {
return typeof models === "function" ? models() : models;
}
function resolveConnectionModels(
cfg: OpenClawConfig,
models: ProviderPresetModels,
): ModelDefinitionConfig[] {
return cfg.models?.mode === "replace" ? structuredClone(resolvePresetModels(models)) : [];
}
function extractAgentDefaultModelFallbacks(model: unknown): string[] | undefined {
if (!model || typeof model !== "object") {
return undefined;
@ -547,7 +560,7 @@ export function applyProviderConfigWithDefaultModelsPreset(
providerId: string;
api: ModelApi;
baseUrl: string;
defaultModels: ModelDefinitionConfig[];
defaultModels: ProviderPresetModels;
defaultModelId?: string;
aliases?: readonly AgentModelAliasEntry[];
primaryModelRef?: string;
@ -558,7 +571,7 @@ export function applyProviderConfigWithDefaultModelsPreset(
providerId: params.providerId,
api: params.api,
baseUrl: params.baseUrl,
defaultModels: params.defaultModels,
defaultModels: resolvePresetModels(params.defaultModels),
defaultModelId: params.defaultModelId,
});
return completeProviderPreset(cfg, next, params.primaryModelRef);
@ -582,6 +595,20 @@ export function createDefaultModelsPresetAppliers<TArgs extends unknown[]>(param
});
}
/** Build connection-only setup appliers while retaining the default-model merge rule in replace mode. */
export function createDefaultModelsConnectionPresetAppliers<TArgs extends unknown[]>(
params: Parameters<typeof createDefaultModelsPresetAppliers<TArgs>>[0],
): ProviderOnboardPresetAppliers<TArgs> {
return createProviderPresetAppliers({
...params,
applyPreset: (cfg, preset: Parameters<typeof applyProviderConfigWithDefaultModelsPreset>[1]) =>
applyProviderConfigWithDefaultModelsPreset(cfg, {
...preset,
defaultModels: resolveConnectionModels(cfg, preset.defaultModels),
}),
});
}
/** Merge a provider config with a catalog while preserving existing model entries first. */
export function applyProviderConfigWithModelCatalog(
cfg: OpenClawConfig,
@ -621,7 +648,7 @@ export function applyProviderConfigWithModelCatalogPreset(
providerId: string;
api: ModelApi;
baseUrl: string;
catalogModels: ModelDefinitionConfig[];
catalogModels: ProviderPresetModels;
aliases?: readonly AgentModelAliasEntry[];
primaryModelRef?: string;
},
@ -631,7 +658,7 @@ export function applyProviderConfigWithModelCatalogPreset(
providerId: params.providerId,
api: params.api,
baseUrl: params.baseUrl,
catalogModels: params.catalogModels,
catalogModels: resolvePresetModels(params.catalogModels),
});
return completeProviderPreset(cfg, next, params.primaryModelRef);
}
@ -654,6 +681,24 @@ export function createModelCatalogPresetAppliers<TArgs extends unknown[]>(params
});
}
/** Apply connection facts and aliases, seeding the supplied catalog only in explicit replace mode. */
export function applyProviderConnectionConfig(
cfg: OpenClawConfig,
params: Parameters<typeof applyProviderConfigWithModelCatalogPreset>[1],
): OpenClawConfig {
return applyProviderConfigWithModelCatalogPreset(cfg, {
...params,
catalogModels: resolveConnectionModels(cfg, params.catalogModels),
});
}
/** Build registered setup appliers without changing the catalog-seeding public helper contract. */
export function createProviderConnectionPresetAppliers<TArgs extends unknown[]>(
params: Parameters<typeof createModelCatalogPresetAppliers<TArgs>>[0],
): ProviderOnboardPresetAppliers<TArgs> {
return createProviderPresetAppliers({ ...params, applyPreset: applyProviderConnectionConfig });
}
/** Ensure static per-model config includes a provider model ref after onboarding. */
export function ensureModelAllowlistEntry(params: {
cfg: OpenClawConfig;