diff --git a/extensions/openrouter/index.test.ts b/extensions/openrouter/index.test.ts index eb93f922a539..78048ec7ae5f 100644 --- a/extensions/openrouter/index.test.ts +++ b/extensions/openrouter/index.test.ts @@ -597,6 +597,30 @@ describe("openrouter provider hooks", () => { expect(contribution?.dynamicSuffix).not.toContain("google/gemini-3.5-flash"); }); + it("omits Fusion prompt details when a narrower scope disables extra body", async () => { + const provider = await registerSingleProviderPlugin(openrouterPlugin); + const contribution = provider.resolveSystemPromptContribution?.({ + provider: "openrouter", + modelId: "openrouter/fusion", + promptMode: "full", + agentId: "analyst", + config: { + agents: { + defaults: { + params: { + extra_body: { + plugins: [{ id: "fusion", analysis_models: ["default/model"] }], + }, + }, + }, + entries: { analyst: { params: { extraBody: null } } }, + }, + }, + } as never); + + expect(contribution).toBeUndefined(); + }); + it("reads per-agent Fusion config from the canonical agent roster", async () => { const provider = await registerSingleProviderPlugin(openrouterPlugin); const contribution = provider.resolveSystemPromptContribution?.({ diff --git a/extensions/openrouter/index.ts b/extensions/openrouter/index.ts index 84c5aa168002..7672584a4725 100644 --- a/extensions/openrouter/index.ts +++ b/extensions/openrouter/index.ts @@ -162,12 +162,11 @@ function resolveFusionExtraBody( ]; let effective: Record | undefined; for (const source of sources) { - const raw = - source && Object.hasOwn(source, "extra_body") ? source.extra_body : source?.extraBody; - const candidate = readRecord(raw); - if (candidate) { - effective = candidate; + if (!source || (!Object.hasOwn(source, "extra_body") && !Object.hasOwn(source, "extraBody"))) { + continue; } + const raw = Object.hasOwn(source, "extra_body") ? source.extra_body : source.extraBody; + effective = readRecord(raw); } return effective; } diff --git a/src/agents/command/model-selection.ts b/src/agents/command/model-selection.ts index 432dfae2f03a..2e8702989e9c 100644 --- a/src/agents/command/model-selection.ts +++ b/src/agents/command/model-selection.ts @@ -514,6 +514,7 @@ export async function resolveEmbeddedModelSelection(params: { cfg: params.cfg, provider, model, + agentId: params.sessionAgentId, }); let catalogForThinking = allowedModelCatalog.length > 0 diff --git a/src/agents/command/run-embedded-attempt.ts b/src/agents/command/run-embedded-attempt.ts index 585e014cd894..3355923f144a 100644 --- a/src/agents/command/run-embedded-attempt.ts +++ b/src/agents/command/run-embedded-attempt.ts @@ -386,6 +386,7 @@ export async function runEmbeddedAgentAttempt(params: { cfg, provider: providerOverride, model: modelOverride, + agentId: sessionAgentId, }); let candidateThinkingCatalog = thinkingCatalog; if ( diff --git a/src/agents/embedded-agent-runner/run/runtime-preparation.ts b/src/agents/embedded-agent-runner/run/runtime-preparation.ts index 7a61bdf065fe..cf2b51b48b34 100644 --- a/src/agents/embedded-agent-runner/run/runtime-preparation.ts +++ b/src/agents/embedded-agent-runner/run/runtime-preparation.ts @@ -220,6 +220,7 @@ export async function prepareEmbeddedRunRuntime(input: { provider, modelId, model: effectiveModel, + agentId: params.agentId, }); const initialThinkLevel = modelSelectionChangedByHook ? (resolveCandidateThinkingLevel({ diff --git a/src/agents/embedded-agent-runner/run/runtime-resolution.test.ts b/src/agents/embedded-agent-runner/run/runtime-resolution.test.ts index 6c30078d9a9b..13992bca51b7 100644 --- a/src/agents/embedded-agent-runner/run/runtime-resolution.test.ts +++ b/src/agents/embedded-agent-runner/run/runtime-resolution.test.ts @@ -26,4 +26,27 @@ describe("resolveInitialThinkLevel", () => { }), ).toBe("ultra"); }); + + it("uses the selected agent model thinking default", () => { + expect( + resolveInitialThinkLevel({ + config: { + agents: { + defaults: { + models: { "openai/gpt-5.5": { params: { thinking: "low" } } }, + }, + entries: { + audit: { + models: { "openai/gpt-5.5": { params: { thinking: "high" } } }, + }, + }, + }, + }, + provider: "openai", + modelId: "gpt-5.5", + model: { reasoning: true }, + agentId: "audit", + }), + ).toBe("high"); + }); }); diff --git a/src/agents/embedded-agent-runner/run/runtime-resolution.ts b/src/agents/embedded-agent-runner/run/runtime-resolution.ts index b78472174ce2..3285e6c08abf 100644 --- a/src/agents/embedded-agent-runner/run/runtime-resolution.ts +++ b/src/agents/embedded-agent-runner/run/runtime-resolution.ts @@ -57,6 +57,7 @@ export function resolveInitialThinkLevel(params: { provider: string; modelId: string; model: { reasoning?: boolean }; + agentId?: string; }): ThinkLevel { if (params.requested) { return params.requested; @@ -65,6 +66,7 @@ export function resolveInitialThinkLevel(params: { cfg: params.config ?? {}, provider: params.provider, model: params.modelId, + agentId: params.agentId, catalog: [ { provider: params.provider, diff --git a/src/agents/model-thinking-default-core.ts b/src/agents/model-thinking-default-core.ts index 36b0da36e709..b457f66fe237 100644 --- a/src/agents/model-thinking-default-core.ts +++ b/src/agents/model-thinking-default-core.ts @@ -14,6 +14,7 @@ import { } from "../auto-reply/thinking.shared.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { ModelCatalogEntry } from "./model-catalog.types.js"; +import { resolveModelExtraParamSources } from "./model-extra-params.js"; import { legacyModelKey, modelKey, normalizeProviderId } from "./model-ref-shared.js"; import { normalizeModelSelection } from "./model-selection-resolve.js"; import { buildConfiguredModelCatalog } from "./model-selection-shared.js"; @@ -24,19 +25,25 @@ type ThinkingDefaultParams = { model: string; catalog?: ModelCatalogEntry[]; agentRuntime?: string | null; + agentId?: string; }; export function resolveConfiguredThinkingDefaultCore(params: { cfg: OpenClawConfig; provider: string; model: string; + agentId?: string; }): ThinkLevel | undefined { - const configuredModels = params.cfg.agents?.defaults?.models; - const canonicalKey = modelKey(params.provider, params.model); - const legacyKey = legacyModelKey(params.provider, params.model); + const { modelParams, agentModelParams } = resolveModelExtraParamSources({ + config: params.cfg, + provider: params.provider, + modelId: params.model, + agentId: params.agentId, + }); const perModelThinking = - configuredModels?.[canonicalKey]?.params?.thinking ?? - (legacyKey ? configuredModels?.[legacyKey]?.params?.thinking : undefined); + agentModelParams && Object.hasOwn(agentModelParams, "thinking") + ? agentModelParams.thinking + : modelParams?.thinking; if ( perModelThinking === false || perModelThinking === "disabled" || diff --git a/src/agents/model-thinking-default.ts b/src/agents/model-thinking-default.ts index 6f243f2a97c2..caddbd32c259 100644 --- a/src/agents/model-thinking-default.ts +++ b/src/agents/model-thinking-default.ts @@ -17,6 +17,7 @@ export function resolveConfiguredThinkingDefault(params: { cfg: OpenClawConfig; provider: string; model: string; + agentId?: string; }): ThinkLevel | undefined { return resolveConfiguredThinkingDefaultCore(params); } @@ -28,6 +29,7 @@ export function resolveThinkingDefault(params: { model: string; catalog?: ModelCatalogEntry[]; agentRuntime?: string | null; + agentId?: string; }): ThinkLevel { return resolveThinkingDefaultCore(params); } @@ -39,6 +41,7 @@ export async function resolveThinkingDefaultWithRuntimeCatalog(params: { model: string; loadRuntimeCatalog: () => Promise; agentRuntime?: string | null; + agentId?: string; }): Promise { const configuredCatalog = buildConfiguredModelCatalog({ cfg: params.cfg }); const configuredSelectedEntry = configuredCatalog.find( diff --git a/src/auto-reply/reply/memory-flush.test.ts b/src/auto-reply/reply/memory-flush.test.ts index 8f57bde36d95..ee37a6a3f928 100644 --- a/src/auto-reply/reply/memory-flush.test.ts +++ b/src/auto-reply/reply/memory-flush.test.ts @@ -305,4 +305,33 @@ describe("Anthropic server compaction host threshold", () => { }), ).toBe(expected); }); + + it("uses the selected agent context window for the fallback threshold", () => { + const cfg: OpenClawConfig = { + agents: { + defaults: { + params: { anthropicServerCompaction: true }, + models: { + "anthropic/claude-opus-4-7": { params: { context1m: false } }, + }, + }, + entries: { + audit: { + models: { + "anthropic/claude-opus-4-7": { params: { context1m: true } }, + }, + }, + }, + }, + }; + + expect( + resolveResponsesServerCompactionThreshold({ + cfg, + provider: "anthropic", + modelId: "claude-opus-4-7", + agentId: "audit", + }), + ).toBe(700_000); + }); }); diff --git a/src/auto-reply/reply/memory-flush.ts b/src/auto-reply/reply/memory-flush.ts index 7037c599b9f1..08e6fc5b5d01 100644 --- a/src/auto-reply/reply/memory-flush.ts +++ b/src/auto-reply/reply/memory-flush.ts @@ -78,7 +78,12 @@ export function resolveResponsesServerCompactionThreshold(params: { baseUrl: configuredModel?.baseUrl ?? providerConfig?.baseUrl, contextWindow: configuredModel?.contextWindow ?? - resolveMemoryFlushContextWindowTokens({ cfg: params.cfg, provider, modelId }), + resolveMemoryFlushContextWindowTokens({ + cfg: params.cfg, + provider, + modelId, + agentId: params.agentId, + }), }, extraParams, ).threshold; @@ -89,6 +94,7 @@ export function resolveResponsesServerCompactionThreshold(params: { cfg: params.cfg, provider, modelId, + agentId: params.agentId, }); return resolveOpenAIResponsesServerCompactionPlan( { diff --git a/src/cron/isolated-agent/run-executor.ts b/src/cron/isolated-agent/run-executor.ts index 97d941534351..384b2e973f53 100644 --- a/src/cron/isolated-agent/run-executor.ts +++ b/src/cron/isolated-agent/run-executor.ts @@ -468,6 +468,7 @@ function createCronPromptExecutor(params: { cfg: params.cfgWithAgentDefaults, provider: providerOverride, model: modelOverride, + agentId: params.agentId, }); if ( candidateConfiguredThinkLevel !== "off" &&