diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d365739bb7..33bcc3591c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai - Agents/Ollama: forward the configured embedded-run timeout into the global undici stream timeout tuning so slow local Ollama runs no longer inherit the default stream cutoff instead of the operator-set run timeout. (#63175) Thanks @mindcraftreader and @vincentkoc. - Models/Codex: include `apiKey` in the codex provider catalog output so the Pi ModelRegistry validator no longer rejects the entry and silently drops all custom models from every provider in `models.json`. (#66180) Thanks @hoyyeva. +- Tools/image+pdf: normalize configured provider/model refs before media-tool registry lookup so image and PDF tool runs stop rejecting valid Ollama vision models as unknown just because the tool path skipped the usual model-ref normalization step. (#59943) Thanks @yqli2420 and @vincentkoc. - Slack/interactions: apply the configured global `allowFrom` owner allowlist to channel block-action and modal interactive events, require an expected sender id for cross-verification, and reject ambiguous channel types so interactive triggers can no longer bypass the documented allowlist intent in channels without a `users` list. Open-by-default behavior is preserved when no allowlists are configured. (#66028) Thanks @eleqtrizit. - Media-understanding/attachments: fail closed when a local attachment path cannot be canonically resolved via `realpath`, so a `realpath` error can no longer downgrade the canonical-roots allowlist check to a non-canonical comparison; attachments that also have a URL still fall back to the network fetch path. (#66022) Thanks @eleqtrizit. - Agents/gateway-tool: reject `config.patch` and `config.apply` calls from the model-facing gateway tool when they would newly enable any flag enumerated by `openclaw security audit` (for example `dangerouslyDisableDeviceAuth`, `allowInsecureAuth`, `dangerouslyAllowHostHeaderOriginFallback`, `hooks.gmail.allowUnsafeExternalContent`, `tools.exec.applyPatch.workspaceOnly: false`); already-enabled flags pass through unchanged so non-dangerous edits in the same patch still apply, and direct authenticated operator RPC behavior is unchanged. (#62006) Thanks @eleqtrizit. diff --git a/src/agents/tools/media-tool-shared.model-resolution.test.ts b/src/agents/tools/media-tool-shared.model-resolution.test.ts new file mode 100644 index 00000000000..eeba3cb9a51 --- /dev/null +++ b/src/agents/tools/media-tool-shared.model-resolution.test.ts @@ -0,0 +1,62 @@ +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ + normalizeModelRefMock: vi.fn(), +})); + +vi.mock("../model-selection.js", async () => { + const actual = + await vi.importActual("../model-selection.js"); + return { + ...actual, + normalizeModelRef: (...args: Parameters) => + state.normalizeModelRefMock(...args), + }; +}); + +let resolveModelFromRegistry: typeof import("./media-tool-shared.js").resolveModelFromRegistry; + +describe("resolveModelFromRegistry", () => { + beforeAll(async () => { + ({ resolveModelFromRegistry } = await import("./media-tool-shared.js")); + }); + + beforeEach(() => { + state.normalizeModelRefMock + .mockReset() + .mockImplementation((provider: string, model: string) => ({ + provider: provider.trim().toLowerCase(), + model: model.trim().replace(/^ollama\//, ""), + })); + }); + + it("normalizes provider and model refs before registry lookup", () => { + const foundModel = { provider: "ollama", id: "qwen3.5:397b-cloud" }; + const find = vi.fn(() => foundModel); + + const result = resolveModelFromRegistry({ + modelRegistry: { find }, + provider: " OLLAMA ", + modelId: "ollama/qwen3.5:397b-cloud", + }); + + expect(state.normalizeModelRefMock).toHaveBeenCalledWith( + " OLLAMA ", + "ollama/qwen3.5:397b-cloud", + ); + expect(find).toHaveBeenCalledWith("ollama", "qwen3.5:397b-cloud"); + expect(result).toBe(foundModel); + }); + + it("reports the normalized ref when the registry lookup misses", () => { + const find = vi.fn(() => null); + + expect(() => + resolveModelFromRegistry({ + modelRegistry: { find }, + provider: " OLLAMA ", + modelId: "ollama/qwen3.5:397b-cloud", + }), + ).toThrow("Unknown model: ollama/qwen3.5:397b-cloud"); + }); +}); diff --git a/src/agents/tools/media-tool-shared.ts b/src/agents/tools/media-tool-shared.ts index cc4709db424..b2c1d83c6b8 100644 --- a/src/agents/tools/media-tool-shared.ts +++ b/src/agents/tools/media-tool-shared.ts @@ -7,6 +7,7 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "../../shared/string-coerce.js"; +import { normalizeModelRef } from "../model-selection.js"; import { normalizeProviderId } from "../provider-id.js"; import { ToolInputError, readStringArrayParam, readStringParam } from "./common.js"; import type { ImageModelConfig } from "./image-tool.helpers.js"; @@ -400,9 +401,13 @@ export function resolveModelFromRegistry(params: { provider: string; modelId: string; }): Model { - const model = params.modelRegistry.find(params.provider, params.modelId) as Model | null; + const resolvedRef = normalizeModelRef(params.provider, params.modelId); + const model = params.modelRegistry.find( + resolvedRef.provider, + resolvedRef.model, + ) as Model | null; if (!model) { - throw new Error(`Unknown model: ${params.provider}/${params.modelId}`); + throw new Error(`Unknown model: ${resolvedRef.provider}/${resolvedRef.model}`); } return model; }