From 4c0e9a4b2e47acc2c1a1bb127886c6bd6ece8d00 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 19:40:16 +0100 Subject: [PATCH] fix(plugins): honor inferred agent model defaults --- CHANGELOG.md | 2 +- extensions/active-memory/index.test.ts | 47 ++++++++++++++++++++++++++ extensions/active-memory/index.ts | 17 ++++++---- src/hooks/llm-slug-generator.test.ts | 47 +++++++++++++++++++++++++- src/hooks/llm-slug-generator.ts | 13 +++---- 5 files changed, 110 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a38fbb2c44a..65bcbd0c7cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,7 +85,7 @@ Docs: https://docs.openclaw.ai - ACP/OpenCode: update the bundled acpx runtime to 0.6.0 and cover the OpenCode ACP bind path in Docker live tests. - Providers/OpenCode Go: add DeepSeek V4 Pro and DeepSeek V4 Flash to the Go catalog while the bundled Pi registry catches up. Fixes #71587. - Providers/OpenCode Go: route DeepSeek V4 Pro/Flash through the OpenAI-compatible Go endpoint and suppress invalid `reasoning_effort: "off"` payloads, fixing tool-enabled requests for `opencode-go/deepseek-v4-flash`. Fixes #71683. -- Plugins/Skill Workshop: run the LLM reviewer on the configured agent default model instead of the hardcoded OpenAI SDK fallback when hook context lacks model metadata. Fixes #71659. +- Plugins/model defaults: run Skill Workshop review, Active Memory recall, and session-memory slug generation on the configured agent default model instead of the hardcoded OpenAI SDK fallback when hook context lacks model metadata. Fixes #71659. - Providers/Venice: fill the required DeepSeek V4 `reasoning_content` placeholder for `venice/deepseek-v4-pro` and `venice/deepseek-v4-flash` replay turns without sending native DeepSeek `thinking` controls that Venice rejects. Fixes #71628. - Browser/existing-session: support per-profile Chrome MCP command/args, map `cdpUrl` to `--browserUrl` or `--wsEndpoint`, and avoid combining endpoint flags with `--userDataDir`. Fixes #47879, #48037, and #62706. Thanks @puneet1409, @zhehao, and @madkow1001. - Media/plugins: bound MIME sniffing and ZIP archive preflight before handing diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index b2ab36658e2..a54cacbaf4a 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -922,6 +922,53 @@ describe("active-memory plugin", () => { }); }); + it("infers the configured provider for bare active-memory default models", async () => { + api.config = { + agents: { + defaults: { + model: { primary: "gpt-5.5" }, + }, + }, + models: { + providers: { + "openai-codex": { + baseUrl: "https://chatgpt.com/backend-api/codex", + models: [ + { + id: "gpt-5.5", + name: "GPT 5.5", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 128_000, + }, + ], + }, + }, + }, + }; + api.pluginConfig = { + agents: ["main"], + }; + plugin.register(api as unknown as OpenClawPluginApi); + + await hooks.before_prompt_build( + { prompt: "what wings should i order? bare model default", messages: [] }, + { + agentId: "main", + trigger: "user", + sessionKey: "agent:main:main", + messageProvider: "webchat", + }, + ); + + expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({ + provider: "openai-codex", + model: "gpt-5.5", + }); + }); + it("skips recall when no model or explicit fallback resolves", async () => { api.config = {}; api.pluginConfig = { diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 79cda33ed55..8dfc7dc3fe8 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -7,6 +7,7 @@ import { resolveAgentDir, resolveAgentEffectiveModelPrimary, resolveAgentWorkspaceDir, + resolveDefaultModelForAgent, } from "openclaw/plugin-sdk/agent-runtime"; import { resolveLivePluginConfigObject, @@ -1550,13 +1551,11 @@ function extractRecentTurns(messages: unknown[]): ActiveRecallRecentTurn[] { return turns; } -function parseModelCandidate(modelRef: string | undefined) { +function parseModelCandidate(modelRef: string | undefined, defaultProvider = DEFAULT_PROVIDER) { if (!modelRef) { return undefined; } - return ( - parseModelRef(modelRef, DEFAULT_PROVIDER) ?? { provider: DEFAULT_PROVIDER, model: modelRef } - ); + return parseModelRef(modelRef, defaultProvider) ?? { provider: defaultProvider, model: modelRef }; } function getModelRef( @@ -1570,14 +1569,20 @@ function getModelRef( ): { provider: string; model: string } | undefined { const currentRunModel = ctx?.modelProviderId && ctx?.modelId ? `${ctx.modelProviderId}/${ctx.modelId}` : undefined; + const configuredDefaultModel = resolveAgentEffectiveModelPrimary(api.config, agentId) + ? resolveDefaultModelForAgent({ cfg: api.config, agentId }) + : undefined; + const defaultProvider = configuredDefaultModel?.provider ?? DEFAULT_PROVIDER; const candidates = [ config.model, currentRunModel, - resolveAgentEffectiveModelPrimary(api.config, agentId), + configuredDefaultModel + ? `${configuredDefaultModel.provider}/${configuredDefaultModel.model}` + : undefined, config.modelFallback, ]; for (const candidate of candidates) { - const parsed = parseModelCandidate(candidate); + const parsed = parseModelCandidate(candidate, defaultProvider); if (parsed) { return parsed; } diff --git a/src/hooks/llm-slug-generator.test.ts b/src/hooks/llm-slug-generator.test.ts index 4467d21e7b5..66b2be9a202 100644 --- a/src/hooks/llm-slug-generator.test.ts +++ b/src/hooks/llm-slug-generator.test.ts @@ -7,7 +7,13 @@ vi.mock("../agents/agent-scope.js", () => ({ resolveDefaultAgentId: vi.fn(() => "main"), resolveAgentWorkspaceDir: vi.fn(() => "/tmp/openclaw-agent"), resolveAgentDir: vi.fn(() => "/tmp/openclaw-agent/.openclaw-agent"), - resolveAgentEffectiveModelPrimary: vi.fn(() => null), + resolveAgentEffectiveModelPrimary: vi.fn((cfg: OpenClawConfig) => { + const model = cfg.agents?.defaults?.model; + if (typeof model === "string") { + return model; + } + return model?.primary; + }), })); vi.mock("../agents/pi-embedded.js", () => ({ @@ -58,4 +64,43 @@ describe("generateSlugViaLLM", () => { }), ); }); + + it("infers provider metadata for bare configured agent models", async () => { + await generateSlugViaLLM({ + sessionContent: "hello", + cfg: { + agents: { + defaults: { + model: { primary: "gpt-5.5" }, + }, + }, + models: { + providers: { + "openai-codex": { + baseUrl: "https://chatgpt.com/backend-api/codex", + models: [ + { + id: "gpt-5.5", + name: "GPT 5.5", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 128_000, + }, + ], + }, + }, + }, + } as OpenClawConfig, + }); + + expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toEqual( + expect.objectContaining({ + provider: "openai-codex", + model: "gpt-5.5", + }), + ); + }); }); diff --git a/src/hooks/llm-slug-generator.ts b/src/hooks/llm-slug-generator.ts index f32d71562d3..7f656831c3f 100644 --- a/src/hooks/llm-slug-generator.ts +++ b/src/hooks/llm-slug-generator.ts @@ -9,10 +9,8 @@ import { resolveDefaultAgentId, resolveAgentWorkspaceDir, resolveAgentDir, - resolveAgentEffectiveModelPrimary, } from "../agents/agent-scope.js"; -import { DEFAULT_PROVIDER, DEFAULT_MODEL } from "../agents/defaults.js"; -import { parseModelRef } from "../agents/model-selection.js"; +import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { resolveAgentTimeoutMs } from "../agents/timeout.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -55,11 +53,10 @@ ${params.sessionContent.slice(0, 2000)} Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", "bug-fix"`; - // Resolve model from agent config instead of using hardcoded defaults - const modelRef = resolveAgentEffectiveModelPrimary(params.cfg, agentId); - const parsed = modelRef ? parseModelRef(modelRef, DEFAULT_PROVIDER) : null; - const provider = parsed?.provider ?? DEFAULT_PROVIDER; - const model = parsed?.model ?? DEFAULT_MODEL; + const { provider, model } = resolveDefaultModelForAgent({ + cfg: params.cfg, + agentId, + }); const timeoutMs = resolveSlugGeneratorTimeoutMs(params.cfg); const result = await runEmbeddedPiAgent({