mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ollama): enable cloud max thinking (#121074)
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
This commit is contained in:
@@ -1120,8 +1120,10 @@ For full setup and behavior, see [Ollama Web Search](/tools/ollama-search).
|
||||
For native requests, thinking control is forwarded directly: `/think off`
|
||||
and `openclaw agent --thinking off` send top-level `think: false` unless
|
||||
an explicit `params.think`/`params.thinking` is configured; `/think
|
||||
low|medium|high` send the matching effort string; `/think max` maps to
|
||||
Ollama's highest effort, `think: "high"`.
|
||||
low|medium|high` send the matching effort string. Verified full-effort
|
||||
Ollama Cloud families such as GLM 5.2 and DeepSeek V4 also send native
|
||||
`think: "max"` for `/think max`; other models and local servers keep the
|
||||
compatible `think: "high"` mapping.
|
||||
|
||||
<Tip>
|
||||
For the OpenAI-compatible endpoint instead, see "Legacy OpenAI-compatible mode" above — streaming and tool calling may not work together there.
|
||||
|
||||
@@ -29,7 +29,7 @@ title: "Thinking levels"
|
||||
- Anthropic Claude Opus 4.7+ also exposes `/think max`; it maps to the same provider-owned max effort path.
|
||||
- Direct DeepSeek V4 models expose `/think xhigh|max`; both map to DeepSeek `reasoning_effort: "max"` while lower non-off levels map to `high`.
|
||||
- OpenRouter-routed DeepSeek V4 models expose `/think xhigh` and send OpenRouter-supported `reasoning.effort` values instead of DeepSeek-native top-level `reasoning_effort`. Lower non-off levels map to `high`, and stored `max` overrides fall back to `xhigh`.
|
||||
- Ollama thinking-capable models expose `/think low|medium|high|max`; `max` maps to native `think: "high"` because Ollama's native API accepts `low`, `medium`, and `high` effort strings.
|
||||
- Ollama thinking-capable models expose `/think low|medium|high|max`. Verified full-effort Ollama Cloud families such as GLM 5.2 and DeepSeek V4 send each matching native `think` effort, including `max`; other models and local Ollama keep the compatible `high` mapping for `/think max`.
|
||||
- OpenAI GPT models map `/think` through model-specific Responses API effort support. `/think off` sends `reasoning.effort: "none"` only when the target model supports it; otherwise OpenClaw omits the disabled reasoning payload instead of sending an unsupported value.
|
||||
- GPT-5.6 Sol and Terra expose native `/think ultra` through the Codex runtime. GPT-5.6 Luna exposes levels through `max` because its Codex catalog does not advertise Ultra.
|
||||
- The embedded OpenClaw runtime exposes logical `/think ultra` for GPT-5.6 Sol, Terra, and Luna. It sends provider max effort and adds run-scoped proactive sub-agent orchestration guidance.
|
||||
|
||||
@@ -238,8 +238,12 @@ async function augmentOllamaCatalog(
|
||||
|
||||
function captureWrappedOllamaPayload(
|
||||
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "max" | undefined,
|
||||
route: { provider?: string; modelId?: string; baseUrl?: string } = {},
|
||||
) {
|
||||
const provider = registerProvider();
|
||||
const providerId = route.provider ?? "ollama";
|
||||
const modelId = route.modelId ?? "qwen3.5:9b";
|
||||
const baseUrl = route.baseUrl ?? "http://127.0.0.1:11434";
|
||||
let payloadSeen: Record<string, unknown> | undefined;
|
||||
const baseStreamFn = vi.fn((_model, _context, options) => {
|
||||
const payload: Record<string, unknown> = {
|
||||
@@ -256,22 +260,22 @@ function captureWrappedOllamaPayload(
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
[providerId]: {
|
||||
api: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
baseUrl,
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "ollama",
|
||||
modelId: "qwen3.5:9b",
|
||||
provider: providerId,
|
||||
modelId,
|
||||
thinkingLevel,
|
||||
model: {
|
||||
api: "ollama",
|
||||
provider: "ollama",
|
||||
id: "qwen3.5:9b",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
provider: providerId,
|
||||
id: modelId,
|
||||
baseUrl,
|
||||
contextWindow: 131_072,
|
||||
},
|
||||
streamFn: baseStreamFn,
|
||||
@@ -283,8 +287,8 @@ function captureWrappedOllamaPayload(
|
||||
void wrapped(
|
||||
{
|
||||
api: "ollama",
|
||||
provider: "ollama",
|
||||
id: "qwen3.5:9b",
|
||||
provider: providerId,
|
||||
id: modelId,
|
||||
} as never,
|
||||
{} as never,
|
||||
{},
|
||||
@@ -2422,13 +2426,13 @@ describe("ollama plugin", () => {
|
||||
thinkingLevel: "off" as const,
|
||||
expectedThink: false,
|
||||
},
|
||||
...(["low", "medium", "high"] as const).map((thinkingLevel) => ({
|
||||
name: `preserves native Ollama ${thinkingLevel} thinking on the wire`,
|
||||
thinkingLevel,
|
||||
expectedThink: thinkingLevel,
|
||||
})),
|
||||
{
|
||||
name: "wraps native Ollama payloads with top-level think effort when thinking is enabled",
|
||||
thinkingLevel: "low" as const,
|
||||
expectedThink: "low",
|
||||
},
|
||||
{
|
||||
name: "maps native Ollama max thinking to the highest supported wire effort",
|
||||
name: "keeps the compatible local Ollama max mapping",
|
||||
thinkingLevel: "max" as const,
|
||||
expectedThink: "high",
|
||||
},
|
||||
@@ -2444,6 +2448,16 @@ describe("ollama plugin", () => {
|
||||
expect((payloadSeen?.options as Record<string, unknown> | undefined)?.think).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves native Ollama Cloud max thinking on the wire", () => {
|
||||
const { payloadSeen } = captureWrappedOllamaPayload("max", {
|
||||
provider: "ollama-cloud",
|
||||
modelId: "glm-5.2",
|
||||
baseUrl: "https://ollama.com",
|
||||
});
|
||||
|
||||
expect(payloadSeen?.think).toBe("max");
|
||||
});
|
||||
|
||||
it("keeps native Ollama thinking off by default while exposing opt-in effort levels", () => {
|
||||
const provider = registerProvider();
|
||||
|
||||
|
||||
@@ -106,14 +106,62 @@ describe("ollama provider policy public artifact", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("exposes max thinking for reasoning-capable models without full plugin activation", () => {
|
||||
expect(resolveThinkingProfile({ reasoning: true })).toEqual({
|
||||
it("exposes every native effort for reasoning-capable models without full plugin activation", () => {
|
||||
expect(
|
||||
resolveThinkingProfile({ provider: "ollama", modelId: "qwen3:32b", reasoning: true }),
|
||||
).toEqual({
|
||||
levels: [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }],
|
||||
defaultLevel: "off",
|
||||
});
|
||||
expect(resolveThinkingProfile({ reasoning: false })).toEqual({
|
||||
expect(
|
||||
resolveThinkingProfile({ provider: "ollama", modelId: "llama3.2", reasoning: false }),
|
||||
).toEqual({
|
||||
levels: [{ id: "off" }],
|
||||
defaultLevel: "off",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["glm-5.2", "deepseek-v4-pro:cloud"])(
|
||||
"exposes full native effort for cloud model %s when lightweight projections omit metadata",
|
||||
(modelId) => {
|
||||
expect(resolveThinkingProfile({ provider: "ollama-cloud", modelId }).levels).toEqual([
|
||||
{ id: "off" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
{ id: "max" },
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["minimax-m2.7", "glm-5.1", "kimi-k2.5", "custom-thinking-model"])(
|
||||
"does not invent effort levels for catalog-light cloud model %s",
|
||||
(modelId) => {
|
||||
expect(resolveThinkingProfile({ provider: "ollama-cloud", modelId }).levels).toEqual([
|
||||
{ id: "off" },
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps explicit non-reasoning metadata authoritative for known cloud model ids", () => {
|
||||
expect(
|
||||
resolveThinkingProfile({
|
||||
provider: "ollama-cloud",
|
||||
modelId: "glm-5.2",
|
||||
reasoning: false,
|
||||
}).levels,
|
||||
).toEqual([{ id: "off" }]);
|
||||
});
|
||||
|
||||
it("does not infer thinking support for unknown models without catalog metadata", () => {
|
||||
expect(resolveThinkingProfile({ provider: "ollama", modelId: "llama3.2" }).levels).toEqual([
|
||||
{ id: "off" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not apply cloud catalog facts to an unqualified local model", () => {
|
||||
expect(resolveThinkingProfile({ provider: "ollama", modelId: "glm-5.2" }).levels).toEqual([
|
||||
{ id: "off" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// Ollama API module exposes the plugin public contract.
|
||||
import type {
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
ProviderNormalizeResolvedModelContext,
|
||||
ProviderThinkingProfile,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { isCloudModelRef, normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
|
||||
import { OLLAMA_CLOUD_PROVIDER_ID, OLLAMA_DEFAULT_BASE_URL } from "./src/defaults.js";
|
||||
import { supportsOllamaCloudFullThinkingEffort } from "./src/model-reasoning.js";
|
||||
|
||||
type OllamaProviderConfigDraft = Partial<ModelProviderConfig>;
|
||||
|
||||
@@ -64,9 +67,16 @@ export function projectConfiguredModelRow(ctx: ProviderNormalizeResolvedModelCon
|
||||
}
|
||||
|
||||
export function resolveThinkingProfile({
|
||||
modelId,
|
||||
provider,
|
||||
reasoning,
|
||||
}: {
|
||||
reasoning?: boolean;
|
||||
}): ProviderThinkingProfile {
|
||||
return reasoning ? OLLAMA_REASONING_THINKING_PROFILE : OLLAMA_NON_REASONING_THINKING_PROFILE;
|
||||
}: ProviderDefaultThinkingPolicyContext): ProviderThinkingProfile {
|
||||
const isCloudRoute =
|
||||
normalizeProviderId(provider) === OLLAMA_CLOUD_PROVIDER_ID || isCloudModelRef(modelId);
|
||||
const supportsThinking =
|
||||
reasoning === true ||
|
||||
(reasoning === undefined && isCloudRoute && supportsOllamaCloudFullThinkingEffort(modelId));
|
||||
return supportsThinking
|
||||
? OLLAMA_REASONING_THINKING_PROFILE
|
||||
: OLLAMA_NON_REASONING_THINKING_PROFILE;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Ollama plugin module owns model-specific native thinking contracts.
|
||||
export function supportsOllamaCloudFullThinkingEffort(modelId: string): boolean {
|
||||
// These hosted families accept low, medium, high, and max even when
|
||||
// lightweight catalog projections omit their reasoning metadata.
|
||||
const normalized = normalizeOllamaCloudModelId(modelId);
|
||||
return normalized === "glm-5.2" || /^deepseek-v4-(?:flash|pro)$/.test(normalized);
|
||||
}
|
||||
|
||||
function normalizeOllamaCloudModelId(modelId: string): string {
|
||||
return modelId
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/(?::cloud|-cloud)$/, "");
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
OLLAMA_DEFAULT_MAX_TOKENS,
|
||||
OLLAMA_LOCAL_CONTEXT_TOKENS,
|
||||
} from "./defaults.js";
|
||||
import { supportsOllamaCloudFullThinkingEffort } from "./model-reasoning.js";
|
||||
|
||||
export type OllamaTagModel = {
|
||||
name: string;
|
||||
@@ -343,15 +344,6 @@ export function isReasoningModelHeuristic(modelId: string): boolean {
|
||||
return /r1|reasoning|think|reason/i.test(modelId);
|
||||
}
|
||||
|
||||
function isKnownOllamaCloudReasoningModel(modelId: string): boolean {
|
||||
// Match both the canonical direct-host id and the local `:cloud` routing alias.
|
||||
const normalized = modelId
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/:cloud$/, "");
|
||||
return normalized === "glm-5.2" || /^deepseek-v4-(?:flash|pro)$/.test(normalized);
|
||||
}
|
||||
|
||||
export function buildOllamaModelDefinition(
|
||||
modelId: string,
|
||||
contextWindow?: number,
|
||||
@@ -361,7 +353,7 @@ export function buildOllamaModelDefinition(
|
||||
const hasVision = capabilities?.includes("vision") ?? false;
|
||||
const input: ("text" | "image")[] = hasVision ? ["text", "image"] : ["text"];
|
||||
const reasoning =
|
||||
isKnownOllamaCloudReasoningModel(modelId) ||
|
||||
supportsOllamaCloudFullThinkingEffort(modelId) ||
|
||||
(capabilities === undefined
|
||||
? isReasoningModelHeuristic(modelId)
|
||||
: capabilities.includes("thinking"));
|
||||
|
||||
@@ -15,8 +15,9 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-stream-shared";
|
||||
import { isLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { shouldWrapOllamaCompatMoonshotThinking } from "./model-behavior.js";
|
||||
import { supportsOllamaCloudFullThinkingEffort } from "./model-reasoning.js";
|
||||
|
||||
export type OllamaThinkValue = boolean | "low" | "medium" | "high";
|
||||
export type OllamaThinkValue = boolean | "low" | "medium" | "high" | "max";
|
||||
|
||||
export function resolveConfiguredOllamaProviderConfig(params: {
|
||||
config?: OpenClawConfig;
|
||||
@@ -114,42 +115,56 @@ function createOllamaThinkingWrapper(
|
||||
});
|
||||
}
|
||||
|
||||
function resolveOllamaThinkValue(thinkingLevel: unknown): OllamaThinkValue | undefined {
|
||||
if (thinkingLevel === "off") {
|
||||
function normalizeOllamaThinkValue(
|
||||
value: unknown,
|
||||
nativeMax: boolean,
|
||||
): OllamaThinkValue | undefined {
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (value === "off") {
|
||||
return false;
|
||||
}
|
||||
if (thinkingLevel === "low" || thinkingLevel === "medium" || thinkingLevel === "high") {
|
||||
return thinkingLevel;
|
||||
if (value === "low" || value === "medium" || value === "high") {
|
||||
return value;
|
||||
}
|
||||
if (thinkingLevel === "minimal") {
|
||||
if (value === "max") {
|
||||
// Verified full-effort Cloud families accept native max. Keep the shipped
|
||||
// high fallback for local and model-specific contracts without that tier.
|
||||
return nativeMax ? "max" : "high";
|
||||
}
|
||||
if (value === "minimal") {
|
||||
return "low";
|
||||
}
|
||||
if (thinkingLevel === "xhigh" || thinkingLevel === "adaptive" || thinkingLevel === "max") {
|
||||
if (value === "xhigh" || value === "adaptive") {
|
||||
// These OpenClaw-only tiers are not advertised by Ollama; keep their established high mapping.
|
||||
return "high";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveOllamaThinkValue(
|
||||
thinkingLevel: unknown,
|
||||
nativeMax: boolean,
|
||||
): OllamaThinkValue | undefined {
|
||||
return normalizeOllamaThinkValue(thinkingLevel, nativeMax);
|
||||
}
|
||||
|
||||
export function resolveOllamaThinkParamValue(
|
||||
params: Record<string, unknown> | undefined,
|
||||
nativeMax = false,
|
||||
): OllamaThinkValue | undefined {
|
||||
const raw = params?.think ?? params?.thinking;
|
||||
if (typeof raw === "boolean") {
|
||||
return raw;
|
||||
}
|
||||
if (raw === "off") {
|
||||
return false;
|
||||
}
|
||||
if (raw === "low" || raw === "medium" || raw === "high") {
|
||||
return raw;
|
||||
}
|
||||
if (raw === "minimal") {
|
||||
return "low";
|
||||
}
|
||||
if (raw === "xhigh" || raw === "adaptive" || raw === "max") {
|
||||
return "high";
|
||||
}
|
||||
return undefined;
|
||||
return normalizeOllamaThinkValue(params?.think ?? params?.thinking, nativeMax);
|
||||
}
|
||||
|
||||
export function supportsNativeOllamaMax(
|
||||
model: Pick<ProviderRuntimeModel, "id" | "provider"> | undefined,
|
||||
providerId?: string,
|
||||
): boolean {
|
||||
const isCloudProvider =
|
||||
normalizeProviderId(model?.provider ?? "") === "ollama-cloud" ||
|
||||
normalizeProviderId(providerId ?? "") === "ollama-cloud";
|
||||
return isCloudProvider && supportsOllamaCloudFullThinkingEffort(model?.id ?? "");
|
||||
}
|
||||
|
||||
export function shouldForwardNativeOllamaThink(
|
||||
@@ -209,9 +224,12 @@ export function createConfiguredOllamaCompatStreamWrapper(
|
||||
streamFn = wrapOllamaCompatNumCtx(streamFn, resolveOllamaNumCtx(model));
|
||||
}
|
||||
|
||||
const configuredThinkValue = model ? resolveOllamaThinkParamValue(model.params) : undefined;
|
||||
const nativeMax = supportsNativeOllamaMax(model, ctx.provider);
|
||||
const configuredThinkValue = model
|
||||
? resolveOllamaThinkParamValue(model.params, nativeMax)
|
||||
: undefined;
|
||||
const runtimeThinkValue = isNativeOllamaTransport
|
||||
? resolveOllamaThinkValue(ctx.thinkingLevel)
|
||||
? resolveOllamaThinkValue(ctx.thinkingLevel, nativeMax)
|
||||
: undefined;
|
||||
// "off" is also the implicit agent default. Preserve explicit native Ollama
|
||||
// model config unless the active run requests a non-off thinking level.
|
||||
|
||||
@@ -320,7 +320,16 @@ describe("createConfiguredOllamaCompatStreamWrapper", () => {
|
||||
expect(payload.options).toEqual({ num_ctx: 131072 });
|
||||
});
|
||||
|
||||
it.each([
|
||||
it.each<{
|
||||
name: string;
|
||||
id: string;
|
||||
contextWindow: number;
|
||||
provider?: string;
|
||||
reasoning?: boolean;
|
||||
thinkingLevel: string;
|
||||
params?: Record<string, unknown>;
|
||||
expectedThink: boolean | string | undefined;
|
||||
}>([
|
||||
{
|
||||
name: "forwards think=false on native Ollama chat requests when thinking is off",
|
||||
id: "qwen3:32b",
|
||||
@@ -353,57 +362,99 @@ describe("createConfiguredOllamaCompatStreamWrapper", () => {
|
||||
thinkingLevel: "low",
|
||||
expectedThink: undefined,
|
||||
},
|
||||
{
|
||||
name: "forwards the native think effort on native Ollama chat requests when thinking is enabled",
|
||||
id: "qwen3:32b",
|
||||
...(["low", "medium", "high"] as const).map((thinkingLevel) => ({
|
||||
name: `preserves native Ollama ${thinkingLevel} thinking on the wire`,
|
||||
id: "gpt-oss:20b",
|
||||
contextWindow: 131072,
|
||||
thinkingLevel: "low",
|
||||
expectedThink: "low",
|
||||
},
|
||||
thinkingLevel,
|
||||
expectedThink: thinkingLevel,
|
||||
})),
|
||||
{
|
||||
name: "maps native Ollama max thinking to think=high on the wire",
|
||||
name: "keeps the compatible local Ollama max mapping",
|
||||
id: "gpt-oss:20b",
|
||||
contextWindow: 131072,
|
||||
thinkingLevel: "max",
|
||||
expectedThink: "high",
|
||||
},
|
||||
])("$name", async ({ id, contextWindow, reasoning, thinkingLevel, params, expectedThink }) => {
|
||||
await withSuccessfulOllamaFetch(async (fetchMock) => {
|
||||
const model = {
|
||||
api: "ollama",
|
||||
provider: "ollama",
|
||||
id,
|
||||
contextWindow,
|
||||
...(reasoning === undefined ? {} : { reasoning }),
|
||||
...(params ? { params } : {}),
|
||||
};
|
||||
const wrapped = expectDefined(
|
||||
createConfiguredOllamaCompatStreamWrapper({
|
||||
provider: "ollama",
|
||||
modelId: id,
|
||||
model,
|
||||
streamFn: createOllamaStreamFn("http://ollama-host:11434"),
|
||||
thinkingLevel,
|
||||
} as never),
|
||||
"wrapped Ollama stream function",
|
||||
);
|
||||
const stream = await Promise.resolve(
|
||||
wrapped(
|
||||
model as never,
|
||||
{ messages: [{ role: "user", content: "hello" }] } as never,
|
||||
{} as never,
|
||||
),
|
||||
);
|
||||
await collectStreamEvents(stream);
|
||||
{
|
||||
name: "does not infer native max support from a local cloud model alias",
|
||||
id: "glm-5.2:cloud",
|
||||
contextWindow: 131072,
|
||||
thinkingLevel: "max",
|
||||
expectedThink: "high",
|
||||
},
|
||||
{
|
||||
name: "preserves native Ollama Cloud max thinking on the wire",
|
||||
id: "glm-5.2",
|
||||
provider: "ollama-cloud",
|
||||
contextWindow: 131072,
|
||||
thinkingLevel: "max",
|
||||
expectedThink: "max",
|
||||
},
|
||||
{
|
||||
name: "keeps the high fallback for Ollama Cloud GPT-OSS",
|
||||
id: "gpt-oss:120b",
|
||||
provider: "ollama-cloud",
|
||||
contextWindow: 131072,
|
||||
thinkingLevel: "max",
|
||||
expectedThink: "high",
|
||||
},
|
||||
{
|
||||
name: "keeps the high fallback for Cloud models without a verified max tier",
|
||||
id: "kimi-k2.5",
|
||||
provider: "ollama-cloud",
|
||||
contextWindow: 131072,
|
||||
thinkingLevel: "max",
|
||||
expectedThink: "high",
|
||||
},
|
||||
])(
|
||||
"$name",
|
||||
async ({
|
||||
id,
|
||||
provider = "ollama",
|
||||
contextWindow,
|
||||
reasoning,
|
||||
thinkingLevel,
|
||||
params,
|
||||
expectedThink,
|
||||
}) => {
|
||||
await withSuccessfulOllamaFetch(async (fetchMock) => {
|
||||
const model = {
|
||||
api: "ollama",
|
||||
provider,
|
||||
id,
|
||||
contextWindow,
|
||||
...(reasoning === undefined ? {} : { reasoning }),
|
||||
...(params ? { params } : {}),
|
||||
};
|
||||
const wrapped = expectDefined(
|
||||
createConfiguredOllamaCompatStreamWrapper({
|
||||
provider,
|
||||
modelId: id,
|
||||
model,
|
||||
streamFn: createOllamaStreamFn("http://ollama-host:11434"),
|
||||
thinkingLevel,
|
||||
} as never),
|
||||
"wrapped Ollama stream function",
|
||||
);
|
||||
const stream = await Promise.resolve(
|
||||
wrapped(
|
||||
model as never,
|
||||
{ messages: [{ role: "user", content: "hello" }] } as never,
|
||||
{} as never,
|
||||
),
|
||||
);
|
||||
await collectStreamEvents(stream);
|
||||
|
||||
const requestBody = getGuardedFetchJsonBody(fetchMock);
|
||||
expect(requestBody.think).toBe(expectedThink);
|
||||
expect(requireOptionalRecord(requestBody.options)?.think).toBeUndefined();
|
||||
if (reasoning !== false) {
|
||||
expect(requireOptionalRecord(requestBody.options)?.num_ctx).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
const requestBody = getGuardedFetchJsonBody(fetchMock);
|
||||
expect(requestBody.think).toBe(expectedThink);
|
||||
expect(requireOptionalRecord(requestBody.options)?.think).toBeUndefined();
|
||||
if (reasoning !== false) {
|
||||
expect(requireOptionalRecord(requestBody.options)?.num_ctx).toBeUndefined();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("passes resolved provider request timeouts to native Ollama chat fetches", async () => {
|
||||
await withMockNdjsonFetch(
|
||||
@@ -2645,16 +2696,55 @@ describe("createOllamaStreamFn", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps configured native Ollama params.thinking=max to the stable top-level think value", async () => {
|
||||
it.each(["low", "medium", "high"] as const)(
|
||||
"preserves configured native Ollama params.thinking=%s",
|
||||
async (thinking) => {
|
||||
await expectSuccessfulOllamaRequest(
|
||||
{ baseUrl: "http://ollama-host:11434", model: { params: { thinking } } },
|
||||
({ body }) => {
|
||||
expect(body.think).toBe(thinking);
|
||||
expect(requireOptionalRecord(body.options)?.think).toBeUndefined();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps configured local Ollama params.thinking=max compatible", async () => {
|
||||
await expectSuccessfulOllamaRequest(
|
||||
{ baseUrl: "http://ollama-host:11434", model: { params: { thinking: "max" } } },
|
||||
({ body }) => {
|
||||
expect(body.think).toBe("high");
|
||||
expect(requireOptionalRecord(body.options)?.think).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves configured Ollama Cloud params.thinking=max", async () => {
|
||||
await expectSuccessfulOllamaRequest(
|
||||
{
|
||||
baseUrl: "https://ollama.com",
|
||||
model: { provider: "ollama-cloud", id: "glm-5.2", params: { thinking: "max" } },
|
||||
},
|
||||
({ body }) => {
|
||||
expect(body.think).toBe("max");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["gpt-oss:120b", "kimi-k2.5", "custom-thinking-model"])(
|
||||
"keeps configured Ollama Cloud %s params.thinking=max compatible",
|
||||
async (id) => {
|
||||
await expectSuccessfulOllamaRequest(
|
||||
{
|
||||
baseUrl: "https://ollama.com",
|
||||
model: { provider: "ollama-cloud", id, params: { thinking: "max" } },
|
||||
},
|
||||
({ body }) => {
|
||||
expect(body.think).toBe("high");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("uses the default loopback policy when baseUrl is empty", async () => {
|
||||
await expectSuccessfulOllamaRequest({ baseUrl: "" }, ({ request }) => {
|
||||
expect(request.url).toBe("http://127.0.0.1:11434/api/chat");
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type OllamaThinkValue,
|
||||
resolveOllamaConfiguredNumCtx,
|
||||
resolveOllamaThinkParamValue,
|
||||
supportsNativeOllamaMax,
|
||||
shouldForwardNativeOllamaThink,
|
||||
} from "./stream-compat.js";
|
||||
import { OLLAMA_INCOMPLETE_STREAM_ERROR } from "./stream-contract.js";
|
||||
@@ -245,7 +246,7 @@ function resolveOllamaTopLevelParams(
|
||||
}
|
||||
}
|
||||
}
|
||||
const think = resolveOllamaThinkParamValue(params);
|
||||
const think = resolveOllamaThinkParamValue(params, supportsNativeOllamaMax(model));
|
||||
if (think !== undefined && shouldForwardNativeOllamaThink(model, think)) {
|
||||
requestParams.think = think;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user