diff --git a/src/agents/tools/pdf-tool.model-config.test.ts b/src/agents/tools/pdf-tool.model-config.test.ts index d2850c01590..e2049bbc2ab 100644 --- a/src/agents/tools/pdf-tool.model-config.test.ts +++ b/src/agents/tools/pdf-tool.model-config.test.ts @@ -32,7 +32,12 @@ vi.mock("./model-config.helpers.js", () => ({ if (provider === "google") { return Boolean(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY); } - if (provider === "minimax" || provider === "minimax-cn") { + if ( + provider === "minimax" || + provider === "minimax-cn" || + provider === "minimax-portal" || + provider === "minimax-portal-cn" + ) { return Boolean(process.env.MINIMAX_API_KEY); } return false; @@ -113,7 +118,7 @@ describe("resolvePdfModelConfigForTool", () => { ); }); - it("does not add configured MiniMax chat models as automatic PDF image fallbacks", () => { + it("uses configured MiniMax chat models for PDF text extraction fallback", () => { vi.stubEnv("MINIMAX_API_KEY", "minimax-test"); const cfg = { ...withDefaultModel("openai/gpt-5.4"), @@ -126,7 +131,7 @@ describe("resolvePdfModelConfigForTool", () => { id: "MiniMax-M2.7", name: "MiniMax M2.7", reasoning: false, - input: ["text", "image"], + input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128_000, maxTokens: 8_192, @@ -138,7 +143,27 @@ describe("resolvePdfModelConfigForTool", () => { } as OpenClawConfig; expect(resolvePdfModelConfigForTool({ cfg, agentDir: TEST_AGENT_DIR })).toEqual({ - primary: "minimax/MiniMax-VL-01", + primary: "minimax/MiniMax-M2.7", + }); + }); + + it("uses the default MiniMax chat model for PDF text extraction fallback", () => { + vi.stubEnv("MINIMAX_API_KEY", "minimax-test"); + const cfg = { + ...withDefaultModel("minimax-portal/MiniMax-M2.7"), + models: { + providers: { + "minimax-portal": { + baseUrl: "https://api.minimax.io/anthropic", + api: "anthropic-messages", + models: [], + }, + }, + }, + } as OpenClawConfig; + + expect(resolvePdfModelConfigForTool({ cfg, agentDir: TEST_AGENT_DIR })).toEqual({ + primary: "minimax-portal/MiniMax-M2.7", }); }); diff --git a/src/agents/tools/pdf-tool.model-config.ts b/src/agents/tools/pdf-tool.model-config.ts index 33d50b26205..f6aa35a7543 100644 --- a/src/agents/tools/pdf-tool.model-config.ts +++ b/src/agents/tools/pdf-tool.model-config.ts @@ -5,7 +5,7 @@ import { resolveDefaultMediaModel, } from "../../media-understanding/defaults.js"; import type { AuthProfileStore } from "../auth-profiles/types.js"; -import { isMinimaxVlmProvider } from "../minimax-vlm.js"; +import { isMinimaxVlmModel, isMinimaxVlmProvider } from "../minimax-vlm.js"; import { coerceImageModelConfig, type ImageModelConfig, @@ -55,6 +55,78 @@ function resolveImageCandidateRefs(params: { .filter((value): value is string => Boolean(value)); } +function formatProviderModelRef(providerId: string, modelId: string): string { + const slash = modelId.indexOf("/"); + if (slash > 0 && modelId.slice(0, slash).trim() === providerId) { + return modelId; + } + return `${providerId}/${modelId}`; +} + +function isMinimaxVlmModelRef(ref: string): boolean { + const slash = ref.indexOf("/"); + if (slash <= 0) { + return false; + } + return isMinimaxVlmModel(ref.slice(0, slash), ref.slice(slash + 1)); +} + +function resolveMinimaxTextExtractionCandidateRefs(params: { + cfg?: OpenClawConfig; + primary: { provider: string; model: string }; + primaryProviderOk: boolean; + agentDir: string; + authStore?: AuthProfileStore; +}): string[] { + const candidates: string[] = []; + const addCandidate = (providerId: string, modelId: string) => { + const provider = providerId.trim(); + const model = modelId.trim(); + if (!provider || !model || isMinimaxVlmModel(provider, model)) { + return; + } + const ref = formatProviderModelRef(provider, model); + if (!candidates.includes(ref)) { + candidates.push(ref); + } + }; + + if (params.primaryProviderOk && isMinimaxVlmProvider(params.primary.provider)) { + addCandidate(params.primary.provider, params.primary.model); + } + + const providers = params.cfg?.models?.providers; + if (!providers || typeof providers !== "object") { + return candidates; + } + + for (const [providerKey, providerCfg] of Object.entries(providers)) { + const providerId = providerKey.trim(); + if ( + !providerId || + !isMinimaxVlmProvider(providerId) || + !hasAuthForProvider({ + provider: providerId, + agentDir: params.agentDir, + authStore: params.authStore, + }) + ) { + continue; + } + const modelId = (providerCfg?.models ?? []) + .find((model) => { + const id = model?.id?.trim(); + return Boolean(id) && Array.isArray(model?.input) && model.input.includes("text"); + }) + ?.id?.trim(); + if (modelId) { + addCandidate(providerId, modelId); + } + } + + return candidates; +} + export function resolvePdfModelConfigForTool(params: { cfg?: OpenClawConfig; agentDir: string; @@ -138,6 +210,13 @@ export function resolvePdfModelConfigForTool(params: { agentDir: params.agentDir, workspaceDir: params.workspaceDir, authStore: params.authStore, + }).filter((ref) => !isMinimaxVlmModelRef(ref)); + const minimaxTextExtractionCandidates = resolveMinimaxTextExtractionCandidateRefs({ + cfg: params.cfg, + primary, + primaryProviderOk: providerOk, + agentDir: params.agentDir, + authStore: params.authStore, }); if (params.cfg?.models?.providers && typeof params.cfg.models.providers === "object") { @@ -180,11 +259,19 @@ export function resolvePdfModelConfigForTool(params: { } else if (providerOk && primarySupportsNativePdf && (providerVision || providerDefault)) { preferred = providerVision ?? `${primary.provider}/${providerDefault}`; } else { - preferred = nativePdfCandidates[0] ?? genericImageCandidates[0] ?? null; + preferred = + nativePdfCandidates[0] ?? + minimaxTextExtractionCandidates[0] ?? + genericImageCandidates[0] ?? + null; } if (preferred?.trim()) { - for (const candidate of [...nativePdfCandidates, ...genericImageCandidates]) { + for (const candidate of [ + ...nativePdfCandidates, + ...minimaxTextExtractionCandidates, + ...genericImageCandidates, + ]) { if (candidate !== preferred) { addFallback(candidate); }