mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(ollama): use provider thinking default in SDK session factory (#91657)
* fix(ollama): use provider thinking default in SDK session factory * fix(agents): preserve model metadata for thinking defaults * fix(agents): resolve custom Ollama thinking policy --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
// Agent session SDK tests cover default tool wiring, prompt preservation, and
|
||||
// session write-lock behavior.
|
||||
import { Type } from "typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const thinkingMocks = vi.hoisted(() => ({
|
||||
resolveThinkingDefaultForModel: vi.fn(() => "medium"),
|
||||
}));
|
||||
|
||||
vi.mock("../../auto-reply/thinking.js", () => ({
|
||||
resolveThinkingDefaultForModel: thinkingMocks.resolveThinkingDefaultForModel,
|
||||
}));
|
||||
import type { Model } from "../../llm/types.js";
|
||||
import { AuthStorage } from "./auth-storage.js";
|
||||
import { createExtensionRuntime } from "./extensions/loader.js";
|
||||
@@ -316,3 +324,117 @@ describe("createAgentSession tool defaults", () => {
|
||||
expect(events).toEqual(["lock:start", "lock:end"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAgentSession thinking level defaults", () => {
|
||||
beforeEach(() => {
|
||||
thinkingMocks.resolveThinkingDefaultForModel.mockReset();
|
||||
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("medium");
|
||||
});
|
||||
|
||||
it("uses the provider-specific thinking default for new sessions", async () => {
|
||||
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("off");
|
||||
|
||||
const ollamaModel = {
|
||||
...testModel,
|
||||
provider: "ollama",
|
||||
reasoning: true,
|
||||
params: { canonicalModelId: "qwen3:8b" },
|
||||
compat: { thinkingFormat: "ollama" },
|
||||
} satisfies Model;
|
||||
const { session } = await createAgentSession({
|
||||
model: ollamaModel,
|
||||
resourceLoader: createEmptyResourceLoader(),
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settingsManager: SettingsManager.inMemory(),
|
||||
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
|
||||
});
|
||||
|
||||
expect(session.thinkingLevel).toBe("off");
|
||||
expect(thinkingMocks.resolveThinkingDefaultForModel).toHaveBeenCalledWith({
|
||||
provider: "ollama",
|
||||
model: testModel.id,
|
||||
catalog: [ollamaModel],
|
||||
});
|
||||
});
|
||||
|
||||
it("settings default overrides provider thinking default", async () => {
|
||||
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("off");
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
model: { ...testModel, provider: "ollama", reasoning: true },
|
||||
resourceLoader: createEmptyResourceLoader(),
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settingsManager: SettingsManager.inMemory({ defaultThinkingLevel: "low" }),
|
||||
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
|
||||
});
|
||||
|
||||
// User-configured settings default beats provider default
|
||||
expect(session.thinkingLevel).toBe("low");
|
||||
});
|
||||
|
||||
it("uses Ollama policy for custom providers backed by the Ollama API", async () => {
|
||||
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("off");
|
||||
const customOllamaModel = {
|
||||
...testModel,
|
||||
provider: "ollama-spark",
|
||||
api: "ollama",
|
||||
reasoning: true,
|
||||
} satisfies Model;
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
model: customOllamaModel,
|
||||
resourceLoader: createEmptyResourceLoader(),
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settingsManager: SettingsManager.inMemory(),
|
||||
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
|
||||
});
|
||||
|
||||
expect(session.thinkingLevel).toBe("off");
|
||||
expect(thinkingMocks.resolveThinkingDefaultForModel).toHaveBeenCalledWith({
|
||||
provider: "ollama",
|
||||
model: testModel.id,
|
||||
catalog: [customOllamaModel],
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to DEFAULT_THINKING_LEVEL for non-off provider defaults", async () => {
|
||||
// Non-off provider defaults (adaptive, high, low) preserve prior SDK behaviour
|
||||
// to avoid silent cost changes for DeepSeek, OpenRouter, xAI, and Anthropic users.
|
||||
for (const nonOffDefault of ["adaptive", "high", "low"] as const) {
|
||||
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue(nonOffDefault);
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
model: { ...testModel, reasoning: true },
|
||||
resourceLoader: createEmptyResourceLoader(),
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settingsManager: SettingsManager.inMemory(),
|
||||
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
|
||||
});
|
||||
|
||||
expect(session.thinkingLevel).toBe("medium");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses provider default for legacy sessions that have no thinking entry", async () => {
|
||||
// Sessions created before thinking-level tracking (no thinking_level_change entry)
|
||||
// should inherit the provider default, not the hard-coded global "medium".
|
||||
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("off");
|
||||
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
sessionManager.appendMessage({
|
||||
role: "user",
|
||||
content: "hello",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
model: { ...testModel, provider: "ollama", reasoning: true },
|
||||
resourceLoader: createEmptyResourceLoader(),
|
||||
sessionManager,
|
||||
settingsManager: SettingsManager.inMemory(),
|
||||
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
|
||||
});
|
||||
|
||||
expect(session.thinkingLevel).toBe("off");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Selects models, wires built-in/custom tools, loads resources, and creates AgentSession instances.
|
||||
*/
|
||||
import { join } from "node:path";
|
||||
import { resolveThinkingDefaultForModel } from "../../auto-reply/thinking.js";
|
||||
import { clampThinkingLevel } from "../../llm/model-utils.js";
|
||||
import { streamSimple } from "../../llm/stream.js";
|
||||
import type { Message, Model } from "../../llm/types.js";
|
||||
@@ -268,16 +269,29 @@ export async function createAgentSession(
|
||||
|
||||
let thinkingLevel = options.thinkingLevel;
|
||||
|
||||
// Use "off" when a provider explicitly opts out of thinking (e.g. Ollama). Non-off
|
||||
// provider defaults (high, low, adaptive) fall back to DEFAULT_THINKING_LEVEL to avoid
|
||||
// silent cost changes for DeepSeek, OpenRouter, xAI, and other providers.
|
||||
const resolvedProviderDefault = model
|
||||
? resolveThinkingDefaultForModel({
|
||||
provider: model.api === "ollama" ? "ollama" : model.provider,
|
||||
model: model.id,
|
||||
catalog: [model],
|
||||
})
|
||||
: undefined;
|
||||
const modelThinkingDefault: ThinkingLevel =
|
||||
resolvedProviderDefault === "off" ? "off" : DEFAULT_THINKING_LEVEL;
|
||||
|
||||
// If session has data, restore thinking level from it
|
||||
if (thinkingLevel === undefined && hasExistingSession) {
|
||||
thinkingLevel = hasThinkingEntry
|
||||
? (existingSession.thinkingLevel as ThinkingLevel)
|
||||
: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);
|
||||
: (settingsManager.getDefaultThinkingLevel() ?? modelThinkingDefault);
|
||||
}
|
||||
|
||||
// Fall back to settings default
|
||||
if (thinkingLevel === undefined) {
|
||||
thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
|
||||
thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? modelThinkingDefault;
|
||||
}
|
||||
|
||||
// Clamp to model capabilities
|
||||
|
||||
@@ -768,6 +768,27 @@ describe("resolveThinkingDefaultForModel", () => {
|
||||
}),
|
||||
).toBe("off");
|
||||
});
|
||||
|
||||
it("respects provider-declared 'off' default for reasoning-capable models", () => {
|
||||
// Providers like Ollama declare defaultLevel:"off" even for reasoning=true models
|
||||
// because thinking must be explicitly opted in, not activated by the global default.
|
||||
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ provider }) =>
|
||||
provider === "ollama"
|
||||
? {
|
||||
levels: [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }],
|
||||
defaultLevel: "off",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveThinkingDefaultForModel({
|
||||
provider: "ollama",
|
||||
model: "gemma4",
|
||||
catalog: [{ provider: "ollama", id: "gemma4", reasoning: true }],
|
||||
}),
|
||||
).toBe("off");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeReasoningLevel", () => {
|
||||
|
||||
Reference in New Issue
Block a user