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:
Chunyue Wang
2026-06-11 19:36:05 +08:00
committed by GitHub
parent 16ea3f272f
commit 4f3c2cd2df
3 changed files with 160 additions and 3 deletions
+123 -1
View File
@@ -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");
});
});
+16 -2
View File
@@ -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
+21
View File
@@ -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", () => {