fix(pdf): use MiniMax text model fallback

This commit is contained in:
Neerav Makwana
2026-05-22 22:53:12 -04:00
committed by Peter Steinberger
parent 1a60c19743
commit 89bb62e2d7
2 changed files with 119 additions and 7 deletions

View File

@@ -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",
});
});

View File

@@ -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);
}