mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:40:42 +00:00
* fix(minimax): use dedicated image generation endpoint MiniMax image generation uses a dedicated API endpoint (api.minimax.io/v1/image_generation) that is separate from the text/chat API endpoint (api.minimax.io/anthropic). Previously, the resolveMinimaxImageBaseUrl function would extract the origin from the provider's configured baseUrl. If a user had configured their baseUrl to the chat endpoint (e.g., api.minimax.chat/anthropic), the image generation would incorrectly use that endpoint, resulting in "invalid api key" errors. This fix always uses the dedicated image generation endpoint, ignoring the provider's baseUrl configuration for image generation. Fixes #61149 * fix(minimax): support CN endpoint for image generation Respect MINIMAX_API_HOST environment variable to determine whether to use the global (api.minimax.io) or CN (api.minimaxi.com) endpoint for image generation. This ensures that CN users who configure MINIMAX_API_HOST to use api.minimaxi.com will continue to use the CN endpoint for image generation, while global users continue to use api.minimax.io. The original bug was caused by the code extracting the origin from the provider's configured baseUrl, which could be set to incorrect endpoints like api.minimax.chat. This fix uses the dedicated image generation endpoints instead. Fixes #61149 * fix(minimax): infer CN endpoint from provider config when env is unset When MINIMAX_API_HOST is not set, fall back to checking the provider's configured baseUrl to determine whether to use the CN or global image endpoint. This ensures CN users who went through onboarding (which sets models.providers.minimax.baseUrl to https://api.minimaxi.com/anthropic) are correctly routed to the CN image endpoint. The isMinimaxCnHost check ensures we only use the baseUrl origin for CN detection - invalid endpoints like api.minimax.chat would not match minimaxi.com and would correctly fall through to the global default. Fixes #61149 * test(minimax): cover dedicated image endpoints * fix(logging): handle context assembly diagnostics * Revert "fix(logging): handle context assembly diagnostics" This reverts commit f51d2f7d67f8193268dd37553ac77e80a0423390. * test(minimax): isolate image endpoint env * docs(changelog): credit minimax image fix --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
211 lines
6.3 KiB
TypeScript
211 lines
6.3 KiB
TypeScript
import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
|
|
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
|
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
|
import {
|
|
assertOkOrThrowHttpError,
|
|
postJsonRequest,
|
|
resolveProviderHttpRequestConfig,
|
|
} from "openclaw/plugin-sdk/provider-http";
|
|
|
|
const DEFAULT_MINIMAX_IMAGE_BASE_URL = "https://api.minimax.io";
|
|
const CN_MINIMAX_IMAGE_BASE_URL = "https://api.minimaxi.com";
|
|
const DEFAULT_MODEL = "image-01";
|
|
const DEFAULT_OUTPUT_MIME = "image/png";
|
|
const MINIMAX_SUPPORTED_ASPECT_RATIOS = [
|
|
"1:1",
|
|
"16:9",
|
|
"4:3",
|
|
"3:2",
|
|
"2:3",
|
|
"3:4",
|
|
"9:16",
|
|
"21:9",
|
|
] as const;
|
|
|
|
type MinimaxImageApiResponse = {
|
|
data?: {
|
|
image_base64?: string[];
|
|
};
|
|
metadata?: {
|
|
success_count?: number;
|
|
failed_count?: number;
|
|
};
|
|
id?: string;
|
|
base_resp?: {
|
|
status_code?: number;
|
|
status_msg?: string;
|
|
};
|
|
};
|
|
|
|
function isMinimaxCnHost(value: string | undefined): boolean {
|
|
const trimmed = value?.trim();
|
|
if (!trimmed) {
|
|
return false;
|
|
}
|
|
const candidate = /^[a-z][a-z\d+.-]*:\/\//iu.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
try {
|
|
const hostname = new URL(candidate).hostname.toLowerCase();
|
|
return hostname === "minimaxi.com" || hostname.endsWith(".minimaxi.com");
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function resolveMinimaxImageBaseUrl(
|
|
cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"],
|
|
providerId: string,
|
|
): string {
|
|
// MiniMax image generation uses dedicated endpoints that are separate from
|
|
// the text/chat API endpoints. First check MINIMAX_API_HOST env var,
|
|
// then fall back to the provider's configured baseUrl to determine region.
|
|
const apiHost = process.env.MINIMAX_API_HOST;
|
|
if (isMinimaxCnHost(apiHost)) {
|
|
return CN_MINIMAX_IMAGE_BASE_URL;
|
|
}
|
|
// CN onboarding stores region in provider config without requiring env var
|
|
const providerBaseUrl = cfg?.models?.providers?.[providerId]?.baseUrl;
|
|
if (isMinimaxCnHost(providerBaseUrl)) {
|
|
return CN_MINIMAX_IMAGE_BASE_URL;
|
|
}
|
|
return DEFAULT_MINIMAX_IMAGE_BASE_URL;
|
|
}
|
|
|
|
function buildMinimaxImageProvider(providerId: string): ImageGenerationProvider {
|
|
return {
|
|
id: providerId,
|
|
label: "MiniMax",
|
|
defaultModel: DEFAULT_MODEL,
|
|
models: [DEFAULT_MODEL],
|
|
isConfigured: ({ agentDir }) =>
|
|
isProviderApiKeyConfigured({
|
|
provider: providerId,
|
|
agentDir,
|
|
}),
|
|
capabilities: {
|
|
generate: {
|
|
maxCount: 9,
|
|
supportsSize: false,
|
|
supportsAspectRatio: true,
|
|
supportsResolution: false,
|
|
},
|
|
edit: {
|
|
enabled: true,
|
|
maxCount: 9,
|
|
maxInputImages: 1,
|
|
supportsSize: false,
|
|
supportsAspectRatio: true,
|
|
supportsResolution: false,
|
|
},
|
|
geometry: {
|
|
aspectRatios: [...MINIMAX_SUPPORTED_ASPECT_RATIOS],
|
|
},
|
|
},
|
|
async generateImage(req) {
|
|
const auth = await resolveApiKeyForProvider({
|
|
provider: providerId,
|
|
cfg: req.cfg,
|
|
agentDir: req.agentDir,
|
|
store: req.authStore,
|
|
});
|
|
if (!auth.apiKey) {
|
|
throw new Error("MiniMax API key missing");
|
|
}
|
|
|
|
const baseUrl = resolveMinimaxImageBaseUrl(req.cfg, providerId);
|
|
const {
|
|
baseUrl: resolvedBaseUrl,
|
|
allowPrivateNetwork,
|
|
headers,
|
|
dispatcherPolicy,
|
|
} = resolveProviderHttpRequestConfig({
|
|
baseUrl,
|
|
defaultBaseUrl: DEFAULT_MINIMAX_IMAGE_BASE_URL,
|
|
allowPrivateNetwork: false,
|
|
defaultHeaders: {
|
|
Authorization: `Bearer ${auth.apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
provider: providerId,
|
|
capability: "image",
|
|
transport: "http",
|
|
});
|
|
|
|
const body: Record<string, unknown> = {
|
|
model: req.model || DEFAULT_MODEL,
|
|
prompt: req.prompt,
|
|
response_format: "base64",
|
|
n: req.count ?? 1,
|
|
};
|
|
|
|
if (req.aspectRatio?.trim()) {
|
|
body.aspect_ratio = req.aspectRatio.trim();
|
|
}
|
|
|
|
// Map input images to subject_reference for image-to-image generation
|
|
if (req.inputImages && req.inputImages.length > 0) {
|
|
const ref = req.inputImages[0];
|
|
const mime = ref.mimeType || "image/jpeg";
|
|
const dataUrl = `data:${mime};base64,${ref.buffer.toString("base64")}`;
|
|
body.subject_reference = [{ type: "character", image_file: dataUrl }];
|
|
}
|
|
const { response, release } = await postJsonRequest({
|
|
url: `${resolvedBaseUrl}/v1/image_generation`,
|
|
headers,
|
|
body,
|
|
timeoutMs: req.timeoutMs,
|
|
fetchFn: fetch,
|
|
allowPrivateNetwork,
|
|
dispatcherPolicy,
|
|
});
|
|
try {
|
|
await assertOkOrThrowHttpError(response, "MiniMax image generation failed");
|
|
|
|
const data = (await response.json()) as MinimaxImageApiResponse;
|
|
|
|
const baseResp = data.base_resp;
|
|
if (baseResp && typeof baseResp.status_code === "number" && baseResp.status_code !== 0) {
|
|
const msg = baseResp.status_msg ?? "";
|
|
throw new Error(`MiniMax image generation API error (${baseResp.status_code}): ${msg}`);
|
|
}
|
|
|
|
const base64Images = data.data?.image_base64 ?? [];
|
|
const failedCount = data.metadata?.failed_count ?? 0;
|
|
|
|
if (base64Images.length === 0) {
|
|
const reason =
|
|
failedCount > 0 ? `${failedCount} image(s) failed to generate` : "no images returned";
|
|
throw new Error(`MiniMax image generation returned no images: ${reason}`);
|
|
}
|
|
|
|
const images = base64Images
|
|
.map((b64, index) => {
|
|
if (!b64) {
|
|
return null;
|
|
}
|
|
return {
|
|
buffer: Buffer.from(b64, "base64"),
|
|
mimeType: DEFAULT_OUTPUT_MIME,
|
|
fileName: `image-${index + 1}.png`,
|
|
};
|
|
})
|
|
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
|
|
|
return {
|
|
images,
|
|
model: req.model || DEFAULT_MODEL,
|
|
};
|
|
} finally {
|
|
await release();
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildMinimaxImageGenerationProvider(): ImageGenerationProvider {
|
|
return buildMinimaxImageProvider("minimax");
|
|
}
|
|
|
|
export function buildMinimaxPortalImageGenerationProvider(): ImageGenerationProvider {
|
|
return buildMinimaxImageProvider("minimax-portal");
|
|
}
|