Skip to content

Commit 1cae588

Browse files
committed
fix(ollama): use provider thinking default in SDK session factory
1 parent 257b251 commit 1cae588

3 files changed

Lines changed: 128 additions & 3 deletions

File tree

src/agents/sessions/sdk.test.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
// Agent session SDK tests cover default tool wiring, prompt preservation, and
22
// session write-lock behavior.
33
import { Type } from "typebox";
4-
import { describe, expect, it } from "vitest";
4+
import { beforeEach, describe, expect, it, vi } from "vitest";
5+
6+
const thinkingMocks = vi.hoisted(() => ({
7+
resolveThinkingDefaultForModel: vi.fn(() => "medium"),
8+
}));
9+
10+
vi.mock("../../auto-reply/thinking.js", () => ({
11+
resolveThinkingDefaultForModel: thinkingMocks.resolveThinkingDefaultForModel,
12+
}));
513
import type { Model } from "../../llm/types.js";
614
import { AuthStorage } from "./auth-storage.js";
715
import { createExtensionRuntime } from "./extensions/loader.js";
@@ -316,3 +324,85 @@ describe("createAgentSession tool defaults", () => {
316324
expect(events).toEqual(["lock:start", "lock:end"]);
317325
});
318326
});
327+
328+
describe("createAgentSession thinking level defaults", () => {
329+
beforeEach(() => {
330+
thinkingMocks.resolveThinkingDefaultForModel.mockReset();
331+
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("medium");
332+
});
333+
334+
it("uses the provider-specific thinking default for new sessions", async () => {
335+
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("off");
336+
337+
const { session } = await createAgentSession({
338+
model: { ...testModel, provider: "ollama", reasoning: true },
339+
resourceLoader: createEmptyResourceLoader(),
340+
sessionManager: SessionManager.inMemory(),
341+
settingsManager: SettingsManager.inMemory(),
342+
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
343+
});
344+
345+
expect(session.thinkingLevel).toBe("off");
346+
expect(thinkingMocks.resolveThinkingDefaultForModel).toHaveBeenCalledWith({
347+
provider: "ollama",
348+
model: testModel.id,
349+
catalog: [{ provider: "ollama", id: testModel.id, reasoning: true }],
350+
});
351+
});
352+
353+
it("settings default overrides provider thinking default", async () => {
354+
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("off");
355+
356+
const { session } = await createAgentSession({
357+
model: { ...testModel, provider: "ollama", reasoning: true },
358+
resourceLoader: createEmptyResourceLoader(),
359+
sessionManager: SessionManager.inMemory(),
360+
settingsManager: SettingsManager.inMemory({ defaultThinkingLevel: "low" }),
361+
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
362+
});
363+
364+
// User-configured settings default beats provider default
365+
expect(session.thinkingLevel).toBe("low");
366+
});
367+
368+
it("falls back to DEFAULT_THINKING_LEVEL for non-off provider defaults", async () => {
369+
// Non-off provider defaults (adaptive, high, low) preserve prior SDK behaviour
370+
// to avoid silent cost changes for DeepSeek, OpenRouter, xAI, and Anthropic users.
371+
for (const nonOffDefault of ["adaptive", "high", "low"] as const) {
372+
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue(nonOffDefault);
373+
374+
const { session } = await createAgentSession({
375+
model: { ...testModel, reasoning: true },
376+
resourceLoader: createEmptyResourceLoader(),
377+
sessionManager: SessionManager.inMemory(),
378+
settingsManager: SettingsManager.inMemory(),
379+
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
380+
});
381+
382+
expect(session.thinkingLevel).toBe("medium");
383+
}
384+
});
385+
386+
it("uses provider default for legacy sessions that have no thinking entry", async () => {
387+
// Sessions created before thinking-level tracking (no thinking_level_change entry)
388+
// should inherit the provider default, not the hard-coded global "medium".
389+
thinkingMocks.resolveThinkingDefaultForModel.mockReturnValue("off");
390+
391+
const sessionManager = SessionManager.inMemory();
392+
sessionManager.appendMessage({
393+
role: "user",
394+
content: "hello",
395+
timestamp: Date.now(),
396+
});
397+
398+
const { session } = await createAgentSession({
399+
model: { ...testModel, provider: "ollama", reasoning: true },
400+
resourceLoader: createEmptyResourceLoader(),
401+
sessionManager,
402+
settingsManager: SettingsManager.inMemory(),
403+
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
404+
});
405+
406+
expect(session.thinkingLevel).toBe("off");
407+
});
408+
});

src/agents/sessions/sdk.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Selects models, wires built-in/custom tools, loads resources, and creates AgentSession instances.
55
*/
66
import { join } from "node:path";
7+
import { resolveThinkingDefaultForModel } from "../../auto-reply/thinking.js";
78
import { clampThinkingLevel } from "../../llm/model-utils.js";
89
import { streamSimple } from "../../llm/stream.js";
910
import type { Message, Model } from "../../llm/types.js";
@@ -268,16 +269,29 @@ export async function createAgentSession(
268269

269270
let thinkingLevel = options.thinkingLevel;
270271

272+
// Use "off" when a provider explicitly opts out of thinking (e.g. Ollama). Non-off
273+
// provider defaults (high, low, adaptive) fall back to DEFAULT_THINKING_LEVEL to avoid
274+
// silent cost changes for DeepSeek, OpenRouter, xAI, and other providers.
275+
const resolvedProviderDefault = model
276+
? resolveThinkingDefaultForModel({
277+
provider: model.provider,
278+
model: model.id,
279+
catalog: [{ provider: model.provider, id: model.id, reasoning: model.reasoning }],
280+
})
281+
: undefined;
282+
const modelThinkingDefault: ThinkingLevel =
283+
resolvedProviderDefault === "off" ? "off" : DEFAULT_THINKING_LEVEL;
284+
271285
// If session has data, restore thinking level from it
272286
if (thinkingLevel === undefined && hasExistingSession) {
273287
thinkingLevel = hasThinkingEntry
274288
? (existingSession.thinkingLevel as ThinkingLevel)
275-
: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);
289+
: (settingsManager.getDefaultThinkingLevel() ?? modelThinkingDefault);
276290
}
277291

278292
// Fall back to settings default
279293
if (thinkingLevel === undefined) {
280-
thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
294+
thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? modelThinkingDefault;
281295
}
282296

283297
// Clamp to model capabilities

src/auto-reply/thinking.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,27 @@ describe("resolveThinkingDefaultForModel", () => {
510510
}),
511511
).toBe("off");
512512
});
513+
514+
it("respects provider-declared 'off' default for reasoning-capable models", () => {
515+
// Providers like Ollama declare defaultLevel:"off" even for reasoning=true models
516+
// because thinking must be explicitly opted in, not activated by the global default.
517+
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ provider }) =>
518+
provider === "ollama"
519+
? {
520+
levels: [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }],
521+
defaultLevel: "off",
522+
}
523+
: undefined,
524+
);
525+
526+
expect(
527+
resolveThinkingDefaultForModel({
528+
provider: "ollama",
529+
model: "gemma4",
530+
catalog: [{ provider: "ollama", id: "gemma4", reasoning: true }],
531+
}),
532+
).toBe("off");
533+
});
513534
});
514535

515536
describe("normalizeReasoningLevel", () => {

0 commit comments

Comments
 (0)