Compare commits
2 Commits
main
...
brucemacd/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d30a3b9acb | ||
|
|
1ee548da05 |
@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- CLI/Ollama onboarding: keep the interactive model picker for explicit `openclaw onboard --auth-choice ollama` runs so setup still selects a default model without reintroducing pre-picker auto-pulls. (#49249) Thanks @BruceMacD.
|
||||||
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
|
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
|
||||||
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.
|
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.
|
||||||
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
||||||
|
|||||||
100
extensions/ollama/index.test.ts
Normal file
100
extensions/ollama/index.test.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js";
|
||||||
|
import plugin from "./index.js";
|
||||||
|
|
||||||
|
const promptAndConfigureOllamaMock = vi.hoisted(() =>
|
||||||
|
vi.fn(async () => ({
|
||||||
|
config: {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
ollama: {
|
||||||
|
baseUrl: "http://127.0.0.1:11434",
|
||||||
|
api: "ollama",
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const ensureOllamaModelPulledMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
|
|
||||||
|
vi.mock("openclaw/plugin-sdk/ollama-setup", () => ({
|
||||||
|
promptAndConfigureOllama: promptAndConfigureOllamaMock,
|
||||||
|
ensureOllamaModelPulled: ensureOllamaModelPulledMock,
|
||||||
|
configureOllamaNonInteractive: vi.fn(),
|
||||||
|
buildOllamaProvider: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function registerProvider() {
|
||||||
|
const registerProviderMock = vi.fn();
|
||||||
|
|
||||||
|
plugin.register(
|
||||||
|
createTestPluginApi({
|
||||||
|
id: "ollama",
|
||||||
|
name: "Ollama",
|
||||||
|
source: "test",
|
||||||
|
config: {},
|
||||||
|
runtime: {} as never,
|
||||||
|
registerProvider: registerProviderMock,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(registerProviderMock).toHaveBeenCalledTimes(1);
|
||||||
|
return registerProviderMock.mock.calls[0]?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ollama plugin", () => {
|
||||||
|
it("does not preselect a default model during provider auth setup", async () => {
|
||||||
|
const provider = registerProvider();
|
||||||
|
|
||||||
|
const result = await provider.auth[0].run({
|
||||||
|
config: {},
|
||||||
|
prompter: {} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promptAndConfigureOllamaMock).toHaveBeenCalledWith({
|
||||||
|
cfg: {},
|
||||||
|
prompter: {},
|
||||||
|
});
|
||||||
|
expect(result.configPatch).toEqual({
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
ollama: {
|
||||||
|
baseUrl: "http://127.0.0.1:11434",
|
||||||
|
api: "ollama",
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.defaultModel).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pulls the model the user actually selected", async () => {
|
||||||
|
const provider = registerProvider();
|
||||||
|
const config = {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
ollama: {
|
||||||
|
baseUrl: "http://127.0.0.1:11434",
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const prompter = {} as never;
|
||||||
|
|
||||||
|
await provider.onModelSelected?.({
|
||||||
|
config,
|
||||||
|
model: "ollama/glm-4.7-flash",
|
||||||
|
prompter,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ensureOllamaModelPulledMock).toHaveBeenCalledWith({
|
||||||
|
config,
|
||||||
|
model: "ollama/glm-4.7-flash",
|
||||||
|
prompter,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -49,7 +49,6 @@ export default definePluginEntry({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
configPatch: result.config,
|
configPatch: result.config,
|
||||||
defaultModel: `ollama/${result.defaultModelId}`,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) => {
|
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) => {
|
||||||
@ -118,7 +117,7 @@ export default definePluginEntry({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const providerSetup = await loadProviderSetup();
|
const providerSetup = await loadProviderSetup();
|
||||||
await providerSetup.ensureOllamaModelPulled({ config, prompter });
|
await providerSetup.ensureOllamaModelPulled({ config, model, prompter });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -14,15 +14,11 @@ vi.mock("../agents/auth-profiles.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const openUrlMock = vi.hoisted(() => vi.fn(async () => false));
|
const openUrlMock = vi.hoisted(() => vi.fn(async () => false));
|
||||||
vi.mock("./onboard-helpers.js", async (importOriginal) => {
|
|
||||||
const original = await importOriginal<typeof import("./onboard-helpers.js")>();
|
|
||||||
return { ...original, openUrl: openUrlMock };
|
|
||||||
});
|
|
||||||
|
|
||||||
const isRemoteEnvironmentMock = vi.hoisted(() => vi.fn(() => false));
|
const isRemoteEnvironmentMock = vi.hoisted(() => vi.fn(() => false));
|
||||||
vi.mock("./oauth-env.js", () => ({
|
vi.mock("../plugins/setup-browser.js", async (importOriginal) => {
|
||||||
isRemoteEnvironment: isRemoteEnvironmentMock,
|
const original = await importOriginal<typeof import("../plugins/setup-browser.js")>();
|
||||||
}));
|
return { ...original, openUrl: openUrlMock, isRemoteEnvironment: isRemoteEnvironmentMock };
|
||||||
|
});
|
||||||
|
|
||||||
function createOllamaFetchMock(params: {
|
function createOllamaFetchMock(params: {
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
@ -104,26 +100,28 @@ describe("ollama setup", () => {
|
|||||||
isRemoteEnvironmentMock.mockReset().mockReturnValue(false);
|
isRemoteEnvironmentMock.mockReset().mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns suggested default model for local mode", async () => {
|
it("puts suggested local model first in local mode", async () => {
|
||||||
const prompter = createModePrompter("local");
|
const prompter = createModePrompter("local");
|
||||||
|
|
||||||
const fetchMock = createOllamaFetchMock({ tags: ["llama3:8b"] });
|
const fetchMock = createOllamaFetchMock({ tags: ["llama3:8b"] });
|
||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
const result = await promptAndConfigureOllama({ cfg: {}, prompter });
|
const result = await promptAndConfigureOllama({ cfg: {}, prompter });
|
||||||
|
const modelIds = result.config.models?.providers?.ollama?.models?.map((m) => m.id);
|
||||||
|
|
||||||
expect(result.defaultModelId).toBe("glm-4.7-flash");
|
expect(modelIds?.[0]).toBe("glm-4.7-flash");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns suggested default model for remote mode", async () => {
|
it("puts suggested cloud model first in remote mode", async () => {
|
||||||
const prompter = createModePrompter("remote");
|
const prompter = createModePrompter("remote");
|
||||||
|
|
||||||
const fetchMock = createOllamaFetchMock({ tags: ["llama3:8b"] });
|
const fetchMock = createOllamaFetchMock({ tags: ["llama3:8b"] });
|
||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
const result = await promptAndConfigureOllama({ cfg: {}, prompter });
|
const result = await promptAndConfigureOllama({ cfg: {}, prompter });
|
||||||
|
const modelIds = result.config.models?.providers?.ollama?.models?.map((m) => m.id);
|
||||||
|
|
||||||
expect(result.defaultModelId).toBe("kimi-k2.5:cloud");
|
expect(modelIds?.[0]).toBe("kimi-k2.5:cloud");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mode selection affects model ordering (local)", async () => {
|
it("mode selection affects model ordering (local)", async () => {
|
||||||
@ -134,7 +132,6 @@ describe("ollama setup", () => {
|
|||||||
|
|
||||||
const result = await promptAndConfigureOllama({ cfg: {}, prompter });
|
const result = await promptAndConfigureOllama({ cfg: {}, prompter });
|
||||||
|
|
||||||
expect(result.defaultModelId).toBe("glm-4.7-flash");
|
|
||||||
const modelIds = result.config.models?.providers?.ollama?.models?.map((m) => m.id);
|
const modelIds = result.config.models?.providers?.ollama?.models?.map((m) => m.id);
|
||||||
expect(modelIds?.[0]).toBe("glm-4.7-flash");
|
expect(modelIds?.[0]).toBe("glm-4.7-flash");
|
||||||
expect(modelIds).toContain("llama3:8b");
|
expect(modelIds).toContain("llama3:8b");
|
||||||
@ -238,6 +235,7 @@ describe("ollama setup", () => {
|
|||||||
|
|
||||||
await ensureOllamaModelPulled({
|
await ensureOllamaModelPulled({
|
||||||
config: createDefaultOllamaConfig("ollama/glm-4.7-flash"),
|
config: createDefaultOllamaConfig("ollama/glm-4.7-flash"),
|
||||||
|
model: "ollama/glm-4.7-flash",
|
||||||
prompter,
|
prompter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -253,6 +251,7 @@ describe("ollama setup", () => {
|
|||||||
|
|
||||||
await ensureOllamaModelPulled({
|
await ensureOllamaModelPulled({
|
||||||
config: createDefaultOllamaConfig("ollama/glm-4.7-flash"),
|
config: createDefaultOllamaConfig("ollama/glm-4.7-flash"),
|
||||||
|
model: "ollama/glm-4.7-flash",
|
||||||
prompter,
|
prompter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -266,6 +265,7 @@ describe("ollama setup", () => {
|
|||||||
|
|
||||||
await ensureOllamaModelPulled({
|
await ensureOllamaModelPulled({
|
||||||
config: createDefaultOllamaConfig("ollama/kimi-k2.5:cloud"),
|
config: createDefaultOllamaConfig("ollama/kimi-k2.5:cloud"),
|
||||||
|
model: "ollama/kimi-k2.5:cloud",
|
||||||
prompter,
|
prompter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -281,6 +281,7 @@ describe("ollama setup", () => {
|
|||||||
config: {
|
config: {
|
||||||
agents: { defaults: { model: { primary: "openai/gpt-4o" } } },
|
agents: { defaults: { model: { primary: "openai/gpt-4o" } } },
|
||||||
},
|
},
|
||||||
|
model: "openai/gpt-4o",
|
||||||
prompter,
|
prompter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -293,7 +293,7 @@ async function storeOllamaCredential(agentDir?: string): Promise<void> {
|
|||||||
export async function promptAndConfigureOllama(params: {
|
export async function promptAndConfigureOllama(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
}): Promise<{ config: OpenClawConfig; defaultModelId: string }> {
|
}): Promise<{ config: OpenClawConfig }> {
|
||||||
const { prompter } = params;
|
const { prompter } = params;
|
||||||
|
|
||||||
// 1. Prompt base URL
|
// 1. Prompt base URL
|
||||||
@ -398,14 +398,13 @@ export async function promptAndConfigureOllama(params: {
|
|||||||
...modelNames.filter((name) => !suggestedModels.includes(name)),
|
...modelNames.filter((name) => !suggestedModels.includes(name)),
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultModelId = suggestedModels[0] ?? OLLAMA_DEFAULT_MODEL;
|
|
||||||
const config = applyOllamaProviderConfig(
|
const config = applyOllamaProviderConfig(
|
||||||
params.cfg,
|
params.cfg,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
orderedModelNames,
|
orderedModelNames,
|
||||||
discoveredModelsByName,
|
discoveredModelsByName,
|
||||||
);
|
);
|
||||||
return { config, defaultModelId };
|
return { config };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Non-interactive: auto-discover models and configure provider. */
|
/** Non-interactive: auto-discover models and configure provider. */
|
||||||
@ -512,15 +511,14 @@ export async function configureOllamaNonInteractive(params: {
|
|||||||
/** Pull the configured default Ollama model if it isn't already available locally. */
|
/** Pull the configured default Ollama model if it isn't already available locally. */
|
||||||
export async function ensureOllamaModelPulled(params: {
|
export async function ensureOllamaModelPulled(params: {
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
|
model: string;
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const modelCfg = params.config.agents?.defaults?.model;
|
if (!params.model.startsWith("ollama/")) {
|
||||||
const modelId = typeof modelCfg === "string" ? modelCfg : modelCfg?.primary;
|
|
||||||
if (!modelId?.startsWith("ollama/")) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const baseUrl = params.config.models?.providers?.ollama?.baseUrl ?? OLLAMA_DEFAULT_BASE_URL;
|
const baseUrl = params.config.models?.providers?.ollama?.baseUrl ?? OLLAMA_DEFAULT_BASE_URL;
|
||||||
const modelName = modelId.slice("ollama/".length);
|
const modelName = params.model.slice("ollama/".length);
|
||||||
if (isOllamaCloudModel(modelName)) {
|
if (isOllamaCloudModel(modelName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -410,6 +410,33 @@ describe("runSetupWizard", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prompts for a model during explicit interactive Ollama setup", async () => {
|
||||||
|
promptDefaultModel.mockClear();
|
||||||
|
const prompter = buildWizardPrompter({});
|
||||||
|
const runtime = createRuntime();
|
||||||
|
|
||||||
|
await runSetupWizard(
|
||||||
|
{
|
||||||
|
acceptRisk: true,
|
||||||
|
flow: "quickstart",
|
||||||
|
authChoice: "ollama",
|
||||||
|
installDaemon: false,
|
||||||
|
skipSkills: true,
|
||||||
|
skipSearch: true,
|
||||||
|
skipHealth: true,
|
||||||
|
skipUi: true,
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
prompter,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(promptDefaultModel).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
allowKeep: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows plugin compatibility notices for an existing valid config", async () => {
|
it("shows plugin compatibility notices for an existing valid config", async () => {
|
||||||
buildPluginCompatibilityNotices.mockReturnValue([
|
buildPluginCompatibilityNotices.mockReturnValue([
|
||||||
{
|
{
|
||||||
|
|||||||
@ -482,11 +482,14 @@ export async function runSetupWizard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authChoiceFromPrompt && authChoice !== "custom-api-key") {
|
const shouldPromptModelSelection =
|
||||||
|
authChoice !== "custom-api-key" && (authChoiceFromPrompt || authChoice === "ollama");
|
||||||
|
if (shouldPromptModelSelection) {
|
||||||
const modelSelection = await promptDefaultModel({
|
const modelSelection = await promptDefaultModel({
|
||||||
config: nextConfig,
|
config: nextConfig,
|
||||||
prompter,
|
prompter,
|
||||||
allowKeep: true,
|
// For ollama, don't allow "keep current" since we may need to download the selected model
|
||||||
|
allowKeep: authChoice !== "ollama",
|
||||||
ignoreAllowlist: true,
|
ignoreAllowlist: true,
|
||||||
includeProviderPluginSetups: true,
|
includeProviderPluginSetups: true,
|
||||||
preferredProvider: await resolvePreferredProviderForAuthChoice({
|
preferredProvider: await resolvePreferredProviderForAuthChoice({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user