diff --git a/src/commands/onboard-auth.config-core.provider-rewrites.test.ts b/src/commands/onboard-auth.config-core.provider-rewrites.test.ts new file mode 100644 index 00000000000..0e854665a37 --- /dev/null +++ b/src/commands/onboard-auth.config-core.provider-rewrites.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from "vitest"; +import { SYNTHETIC_DEFAULT_MODEL_ID } from "../agents/synthetic-models.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { ModelDefinitionConfig } from "../config/types.models.js"; +import { + applyModelStudioProviderConfig, + applySyntheticProviderConfig, + applyZaiProviderConfig, +} from "./onboard-auth.config-core.js"; + +function makeModel(id: string): ModelDefinitionConfig { + return { + id, + name: id, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 4096, + maxTokens: 1024, + }; +} + +describe("core onboarding provider rewriters", () => { + const secretRef = { + source: "env" as const, + provider: "default", + id: "TEST_API_KEY", + }; + + it("rewrites aliased ZAI providers to the canonical key and preserves secret refs", () => { + const cfg: OpenClawConfig = { + models: { + providers: { + "z-ai": { + api: "openai-completions", + baseUrl: "https://zai-preview.example/v1", + apiKey: secretRef, + models: [makeModel("legacy-zai-model")], + }, + }, + }, + }; + + const result = applyZaiProviderConfig(cfg); + + expect(Object.keys(result.models?.providers ?? {})).toEqual(["zai"]); + expect(result.models?.providers?.zai?.baseUrl).toBe("https://zai-preview.example/v1"); + expect(result.models?.providers?.zai?.apiKey).toEqual(secretRef); + expect(result.models?.providers?.zai?.models?.map((model) => model.id)).toEqual( + expect.arrayContaining(["legacy-zai-model", "glm-5"]), + ); + }); + + it("preserves secret-ref api keys when rewriting Synthetic", () => { + const cfg: OpenClawConfig = { + models: { + providers: { + synthetic: { + api: "openai-completions", + baseUrl: "https://synthetic-legacy.example/v1", + apiKey: secretRef, + models: [makeModel("legacy-synthetic-model")], + }, + }, + }, + }; + + const result = applySyntheticProviderConfig(cfg); + + expect(result.models?.providers?.synthetic?.apiKey).toEqual(secretRef); + expect(result.models?.providers?.synthetic?.models?.map((model) => model.id)).toEqual( + expect.arrayContaining(["legacy-synthetic-model", SYNTHETIC_DEFAULT_MODEL_ID]), + ); + }); + + it("preserves secret-ref api keys when rewriting Model Studio", () => { + const cfg: OpenClawConfig = { + models: { + providers: { + modelstudio: { + api: "openai-completions", + baseUrl: "https://modelstudio-legacy.example/v1", + apiKey: secretRef, + models: [makeModel("legacy-modelstudio-model")], + }, + }, + }, + }; + + const result = applyModelStudioProviderConfig(cfg); + + expect(result.models?.providers?.modelstudio?.apiKey).toEqual(secretRef); + expect(result.models?.providers?.modelstudio?.models?.map((model) => model.id)).toEqual( + expect.arrayContaining(["legacy-modelstudio-model", "qwen3.5-plus"]), + ); + }); +}); diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index ddd65a11fa4..ac616638768 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -15,6 +15,7 @@ import { findNormalizedProviderValue } from "../agents/provider-id.js"; import { buildSyntheticModelDefinition, SYNTHETIC_BASE_URL, + SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_REF, SYNTHETIC_MODEL_CATALOG, } from "../agents/synthetic-models.js"; @@ -57,7 +58,6 @@ export { } from "./onboard-auth.config-litellm.js"; import { applyAgentDefaultModelPrimary, - applyOnboardAuthAgentModelsAndProviders, applyProviderConfigWithDefaultModel, applyProviderConfigWithDefaultModels, applyProviderConfigWithModelCatalog, @@ -91,29 +91,6 @@ import { } from "./onboard-auth.models.js"; export { applyAuthProfileConfig } from "./auth-profile-config.js"; -function mergeProviderModels( - existingProvider: Record | undefined, - defaultModels: T[], -): T[] { - const existingModels = Array.isArray(existingProvider?.models) - ? (existingProvider.models as T[]) - : []; - const mergedModels = [...existingModels]; - const seen = new Set(existingModels.map((model) => model.id)); - for (const model of defaultModels) { - if (!seen.has(model.id)) { - mergedModels.push(model); - seen.add(model.id); - } - } - return mergedModels; -} - -function getNormalizedProviderApiKey(existingProvider: Record | undefined) { - const { apiKey } = (existingProvider ?? {}) as { apiKey?: string }; - return typeof apiKey === "string" ? apiKey.trim() || undefined : undefined; -} - export function applyZaiProviderConfig( cfg: OpenClawConfig, params?: { endpoint?: string; modelId?: string }, @@ -126,9 +103,7 @@ export function applyZaiProviderConfig( ...models[modelRef], alias: models[modelRef]?.alias ?? "GLM", }; - - const providers = { ...cfg.models?.providers }; - const existingProvider = providers.zai; + const existingProvider = findNormalizedProviderValue(cfg.models?.providers, "zai"); const defaultModels = [ buildZaiModelDefinition({ id: "glm-5" }), @@ -138,28 +113,19 @@ export function applyZaiProviderConfig( buildZaiModelDefinition({ id: "glm-4.7-flashx" }), ]; - const mergedModels = mergeProviderModels(existingProvider, defaultModels); - - const { apiKey: _existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< - string, - unknown - > as { apiKey?: string }; - const normalizedApiKey = getNormalizedProviderApiKey(existingProvider); - const baseUrl = params?.endpoint ? resolveZaiBaseUrl(params.endpoint) : (typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl : "") || resolveZaiBaseUrl(); - providers.zai = { - ...existingProviderRest, - baseUrl, + return applyProviderConfigWithDefaultModels(cfg, { + agentModels: models, + providerId: "zai", api: "openai-completions", - ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), - models: mergedModels.length > 0 ? mergedModels : defaultModels, - }; - - return applyOnboardAuthAgentModelsAndProviders(cfg, { agentModels: models, providers }); + baseUrl, + defaultModels, + defaultModelId: modelId, + }); } export function applyZaiConfig( @@ -267,30 +233,16 @@ export function applySyntheticProviderConfig(cfg: OpenClawConfig): OpenClawConfi alias: models[SYNTHETIC_DEFAULT_MODEL_REF]?.alias ?? "MiniMax M2.5", }; - const providers = { ...cfg.models?.providers }; - const existingProvider = providers.synthetic; - const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; const syntheticModels = SYNTHETIC_MODEL_CATALOG.map(buildSyntheticModelDefinition); - const mergedModels = [ - ...existingModels, - ...syntheticModels.filter( - (model) => !existingModels.some((existing) => existing.id === model.id), - ), - ]; - const { apiKey: _existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< - string, - unknown - > as { apiKey?: string }; - const normalizedApiKey = getNormalizedProviderApiKey(existingProvider); - providers.synthetic = { - ...existingProviderRest, - baseUrl: SYNTHETIC_BASE_URL, - api: "anthropic-messages", - ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), - models: mergedModels.length > 0 ? mergedModels : syntheticModels, - }; - return applyOnboardAuthAgentModelsAndProviders(cfg, { agentModels: models, providers }); + return applyProviderConfigWithDefaultModels(cfg, { + agentModels: models, + providerId: "synthetic", + api: "anthropic-messages", + baseUrl: SYNTHETIC_BASE_URL, + defaultModels: syntheticModels, + defaultModelId: SYNTHETIC_DEFAULT_MODEL_ID, + }); } export function applySyntheticConfig(cfg: OpenClawConfig): OpenClawConfig { @@ -590,9 +542,6 @@ function applyModelStudioProviderConfigWithBaseUrl( alias: models[MODELSTUDIO_DEFAULT_MODEL_REF]?.alias ?? "Qwen", }; - const providers = { ...cfg.models?.providers }; - const existingProvider = providers.modelstudio; - const defaultModels = [ buildModelStudioModelDefinition({ id: "qwen3.5-plus" }), buildModelStudioModelDefinition({ id: "qwen3-max-2026-01-23" }), @@ -604,23 +553,14 @@ function applyModelStudioProviderConfigWithBaseUrl( buildModelStudioModelDefinition({ id: "kimi-k2.5" }), ]; - const mergedModels = mergeProviderModels(existingProvider, defaultModels); - - const { apiKey: _existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< - string, - unknown - > as { apiKey?: string }; - const normalizedApiKey = getNormalizedProviderApiKey(existingProvider); - - providers.modelstudio = { - ...existingProviderRest, - baseUrl, + return applyProviderConfigWithDefaultModels(cfg, { + agentModels: models, + providerId: "modelstudio", api: "openai-completions", - ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), - models: mergedModels.length > 0 ? mergedModels : defaultModels, - }; - - return applyOnboardAuthAgentModelsAndProviders(cfg, { agentModels: models, providers }); + baseUrl, + defaultModels, + defaultModelId: "qwen3.5-plus", + }); } export function applyModelStudioProviderConfig(cfg: OpenClawConfig): OpenClawConfig {