From 0f3db48abbf065d3d997ede4ff6600bef45cebf6 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 8 Sep 2026 20:22:25 +0530 Subject: [PATCH] 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 --- .../sdk-provider-plugins/model-catalogs.md | 29 ++- extensions/cloudflare-ai-gateway/index.ts | 9 +- extensions/cloudflare-ai-gateway/onboard.ts | 27 +++ extensions/featherless/index.ts | 4 +- extensions/featherless/onboard.ts | 16 +- extensions/huggingface/index.ts | 4 +- extensions/huggingface/onboard.ts | 16 +- extensions/longcat/index.ts | 4 +- extensions/longcat/onboard.ts | 15 +- extensions/meta/index.ts | 4 +- extensions/meta/onboard.ts | 15 +- extensions/mistral/index.ts | 4 +- extensions/mistral/onboard.ts | 31 ++-- extensions/nvidia/index.ts | 4 +- extensions/nvidia/onboard.ts | 37 ++-- extensions/synthetic/index.ts | 4 +- extensions/synthetic/onboard.ts | 21 ++- extensions/together/index.ts | 4 +- extensions/together/onboard.ts | 16 +- extensions/xiaomi/index.ts | 6 +- extensions/xiaomi/onboard.ts | 33 ++-- extensions/zai/index.ts | 12 +- extensions/zai/onboard.ts | 22 ++- .../provider-onboard.connection.test.ts | 170 ++++++++++++++++++ src/plugin-sdk/provider-onboard.ts | 53 +++++- 25 files changed, 457 insertions(+), 103 deletions(-) create mode 100644 src/plugin-sdk/provider-onboard.connection.test.ts diff --git a/docs/plugins/sdk-provider-plugins/model-catalogs.md b/docs/plugins/sdk-provider-plugins/model-catalogs.md index b92033a8e344..374648f2e08a 100644 --- a/docs/plugins/sdk-provider-plugins/model-catalogs.md +++ b/docs/plugins/sdk-provider-plugins/model-catalogs.md @@ -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 diff --git a/extensions/cloudflare-ai-gateway/index.ts b/extensions/cloudflare-ai-gateway/index.ts index 5a1665aeee58..4b6c86d1c71b 100644 --- a/extensions/cloudflare-ai-gateway/index.ts +++ b/extensions/cloudflare-ai-gateway/index.ts @@ -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 }); }, }, ], diff --git a/extensions/cloudflare-ai-gateway/onboard.ts b/extensions/cloudflare-ai-gateway/onboard.ts index cbaf4618af43..21d0f3eac82c 100644 --- a/extensions/cloudflare-ai-gateway/onboard.ts +++ b/extensions/cloudflare-ai-gateway/onboard.ts @@ -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, + ); +} diff --git a/extensions/featherless/index.ts b/extensions/featherless/index.ts index 8a5e945a6926..71e34c8ecab3 100644 --- a/extensions/featherless/index.ts +++ b/extensions/featherless/index.ts @@ -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.", diff --git a/extensions/featherless/onboard.ts b/extensions/featherless/onboard.ts index c5845d3d3d0b..cacd815ef6af 100644 --- a/extensions/featherless/onboard.ts +++ b/extensions/featherless/onboard.ts @@ -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>[0]; + +export const { applyConfig: applyFeatherlessConfig } = + createModelCatalogPresetAppliers(featherlessPreset); +export const { applyConfig: applyFeatherlessConnectionConfig } = + createProviderConnectionPresetAppliers(featherlessPreset); diff --git a/extensions/huggingface/index.ts b/extensions/huggingface/index.ts index 0d8a0f2770bf..39825f40139e 100644 --- a/extensions/huggingface/index.ts +++ b/extensions/huggingface/index.ts @@ -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) => { diff --git a/extensions/huggingface/onboard.ts b/extensions/huggingface/onboard.ts index 3661caef0ebe..b003b345eed7 100644 --- a/extensions/huggingface/onboard.ts +++ b/extensions/huggingface/onboard.ts @@ -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>[0]; + +export const { applyConfig: applyHuggingfaceConfig } = + createModelCatalogPresetAppliers(huggingfacePreset); +export const { applyConfig: applyHuggingfaceConnectionConfig } = + createProviderConnectionPresetAppliers(huggingfacePreset); diff --git a/extensions/longcat/index.ts b/extensions/longcat/index.ts index 9fafa85b02ca..fedc191274a9 100644 --- a/extensions/longcat/index.ts +++ b/extensions/longcat/index.ts @@ -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", }, diff --git a/extensions/longcat/onboard.ts b/extensions/longcat/onboard.ts index 9fe9b838642f..d3ec9a4f4f97 100644 --- a/extensions/longcat/onboard.ts +++ b/extensions/longcat/onboard.ts @@ -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>[0]; + +export const { applyConfig: applyLongCatConfig } = createModelCatalogPresetAppliers(longCatPreset); +export const { applyConfig: applyLongCatConnectionConfig } = + createProviderConnectionPresetAppliers(longCatPreset); diff --git a/extensions/meta/index.ts b/extensions/meta/index.ts index 1455e66ad4ea..aed220998767 100644 --- a/extensions/meta/index.ts +++ b/extensions/meta/index.ts @@ -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", }, diff --git a/extensions/meta/onboard.ts b/extensions/meta/onboard.ts index f2d596437e2a..cc73f67bad1b 100644 --- a/extensions/meta/onboard.ts +++ b/extensions/meta/onboard.ts @@ -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>[0]; + +export const { applyConfig: applyMetaConfig } = createModelCatalogPresetAppliers(metaPreset); +export const { applyConfig: applyMetaConnectionConfig } = + createProviderConnectionPresetAppliers(metaPreset); diff --git a/extensions/mistral/index.ts b/extensions/mistral/index.ts index 9dff6c3c690a..47c481cdaff0 100644 --- a/extensions/mistral/index.ts +++ b/extensions/mistral/index.ts @@ -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, diff --git a/extensions/mistral/onboard.ts b/extensions/mistral/onboard.ts index 7ff7fb3d418e..7f0d061d12ac 100644 --- a/extensions/mistral/onboard.ts +++ b/extensions/mistral/onboard.ts @@ -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>[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); diff --git a/extensions/nvidia/index.ts b/extensions/nvidia/index.ts index 7328c2de783e..255f9d9a6609 100644 --- a/extensions/nvidia/index.ts +++ b/extensions/nvidia/index.ts @@ -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", diff --git a/extensions/nvidia/onboard.ts b/extensions/nvidia/onboard.ts index aa61331c5db4..96a372a0dc6c 100644 --- a/extensions/nvidia/onboard.ts +++ b/extensions/nvidia/onboard.ts @@ -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>[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); diff --git a/extensions/synthetic/index.ts b/extensions/synthetic/index.ts index 59380acba147..54eb7c9d1c86 100644 --- a/extensions/synthetic/index.ts +++ b/extensions/synthetic/index.ts @@ -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", diff --git a/extensions/synthetic/onboard.ts b/extensions/synthetic/onboard.ts index e43d317409a7..a1f1726963f7 100644 --- a/extensions/synthetic/onboard.ts +++ b/extensions/synthetic/onboard.ts @@ -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>[0]; + +export const { + applyConfig: applySyntheticConfig, + applyProviderConfig: applySyntheticProviderConfig, +} = createModelCatalogPresetAppliers(syntheticPreset); +export const { applyConfig: applySyntheticConnectionConfig } = + createProviderConnectionPresetAppliers(syntheticPreset); diff --git a/extensions/together/index.ts b/extensions/together/index.ts index 6cc093b2158f..e3da77803c63 100644 --- a/extensions/together/index.ts +++ b/extensions/together/index.ts @@ -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) diff --git a/extensions/together/onboard.ts b/extensions/together/onboard.ts index f58aeac6c924..857664840467 100644 --- a/extensions/together/onboard.ts +++ b/extensions/together/onboard.ts @@ -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>[0]; + +export const { applyConfig: applyTogetherConfig } = + createModelCatalogPresetAppliers(togetherPreset); +export const { applyConfig: applyTogetherConnectionConfig } = + createProviderConnectionPresetAppliers(togetherPreset); diff --git a/extensions/xiaomi/index.ts b/extensions/xiaomi/index.ts index 863f7339d6ad..a80b7daf5daf 100644 --- a/extensions/xiaomi/index.ts +++ b/extensions/xiaomi/index.ts @@ -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, }), }; } diff --git a/extensions/xiaomi/onboard.ts b/extensions/xiaomi/onboard.ts index c60a98415e61..7d520612360e 100644 --- a/extensions/xiaomi/onboard.ts +++ b/extensions/xiaomi/onboard.ts @@ -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>[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, diff --git a/extensions/zai/index.ts b/extensions/zai/index.ts index 8bba60c04283..43b8835ee817 100644 --- a/extensions/zai/index.ts +++ b/extensions/zai/index.ts @@ -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 }>; - configPatch: ReturnType; + configPatch: ReturnType; 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 } : {}), }); diff --git a/extensions/zai/onboard.ts b/extensions/zai/onboard.ts index 3fadf4666b4c..33fb38c6866d 100644 --- a/extensions/zai/onboard.ts +++ b/extensions/zai/onboard.ts @@ -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); +} diff --git a/src/plugin-sdk/provider-onboard.connection.test.ts b/src/plugin-sdk/provider-onboard.connection.test.ts new file mode 100644 index 000000000000..212dfbc31f3e --- /dev/null +++ b/src/plugin-sdk/provider-onboard.connection.test.ts @@ -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")]); + }, +); diff --git a/src/plugin-sdk/provider-onboard.ts b/src/plugin-sdk/provider-onboard.ts index 9ca71f523228..2eb768046387 100644 --- a/src/plugin-sdk/provider-onboard.ts +++ b/src/plugin-sdk/provider-onboard.ts @@ -49,6 +49,19 @@ export type ProviderOnboardPresetAppliers = { 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(param }); } +/** Build connection-only setup appliers while retaining the default-model merge rule in replace mode. */ +export function createDefaultModelsConnectionPresetAppliers( + params: Parameters>[0], +): ProviderOnboardPresetAppliers { + return createProviderPresetAppliers({ + ...params, + applyPreset: (cfg, preset: Parameters[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(params }); } +/** Apply connection facts and aliases, seeding the supplied catalog only in explicit replace mode. */ +export function applyProviderConnectionConfig( + cfg: OpenClawConfig, + params: Parameters[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( + params: Parameters>[0], +): ProviderOnboardPresetAppliers { + return createProviderPresetAppliers({ ...params, applyPreset: applyProviderConnectionConfig }); +} + /** Ensure static per-model config includes a provider model ref after onboarding. */ export function ensureModelAllowlistEntry(params: { cfg: OpenClawConfig;