test: speed up provider entry tests

This commit is contained in:
Peter Steinberger
2026-04-07 17:36:17 +01:00
parent c1fc2ed0e8
commit 0828db93e9
7 changed files with 414 additions and 412 deletions

View File

@@ -9,12 +9,20 @@ import {
registerProviderPlugin,
requireRegisteredProvider,
} from "../../test/helpers/plugins/provider-registration.js";
import googlePlugin from "./index.js";
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
import { registerGoogleProvider } from "./provider-registration.js";
const googleProviderPlugin = {
register(api: Parameters<typeof registerGoogleProvider>[0]) {
registerGoogleProvider(api);
registerGoogleGeminiCliProvider(api);
},
};
describe("google provider plugin hooks", () => {
it("owns replay policy and reasoning mode for the direct Gemini provider", async () => {
const { providers } = await registerProviderPlugin({
plugin: googlePlugin,
plugin: googleProviderPlugin,
id: "google",
name: "Google Provider",
});
@@ -85,7 +93,7 @@ describe("google provider plugin hooks", () => {
it("owns Gemini CLI tool schema normalization", async () => {
const { providers } = await registerProviderPlugin({
plugin: googlePlugin,
plugin: googleProviderPlugin,
id: "google",
name: "Google Provider",
});
@@ -132,7 +140,7 @@ describe("google provider plugin hooks", () => {
it("wires google-thinking stream hooks for direct and Gemini CLI providers", async () => {
const { providers } = await registerProviderPlugin({
plugin: googlePlugin,
plugin: googleProviderPlugin,
id: "google",
name: "Google Provider",
});

View File

@@ -1,20 +1,10 @@
import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import type { MediaUnderstandingProvider } from "openclaw/plugin-sdk/media-understanding";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family";
import {
GOOGLE_GEMINI_DEFAULT_MODEL,
applyGoogleGeminiModelDefault,
normalizeGoogleProviderConfig,
normalizeGoogleModelId,
resolveGoogleGenerativeAiTransport,
} from "./api.js";
import { buildGoogleGeminiCliBackend } from "./cli-backend.js";
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
import { buildGoogleMusicGenerationProvider } from "./music-generation-provider.js";
import { isModernGoogleModel, resolveGoogleGeminiForwardCompatModel } from "./provider-models.js";
import { registerGoogleProvider } from "./provider-registration.js";
import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js";
import { buildGoogleVideoGenerationProvider } from "./video-generation-provider.js";
@@ -28,13 +18,6 @@ type GoogleMediaUnderstandingProvider = MediaUnderstandingProvider & {
describeVideo: NonNullable<MediaUnderstandingProvider["describeVideo"]>;
};
const GOOGLE_GEMINI_PROVIDER_HOOKS = {
...buildProviderReplayFamilyHooks({
family: "google-gemini",
}),
...buildProviderStreamFamilyHooks("google-thinking"),
};
async function loadGoogleImageGenerationProvider(): Promise<ImageGenerationProvider> {
if (!googleImageGenerationProviderPromise) {
googleImageGenerationProviderPromise = import("./image-generation-provider.js").then((mod) =>
@@ -126,47 +109,7 @@ export default definePluginEntry({
register(api) {
api.registerCliBackend(buildGoogleGeminiCliBackend());
registerGoogleGeminiCliProvider(api);
api.registerProvider({
id: "google",
label: "Google AI Studio",
docsPath: "/providers/models",
hookAliases: ["google-antigravity", "google-vertex"],
envVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: "google",
methodId: "api-key",
label: "Google Gemini API key",
hint: "AI Studio / Gemini API key",
optionKey: "geminiApiKey",
flagName: "--gemini-api-key",
envVar: "GEMINI_API_KEY",
promptMessage: "Enter Gemini API key",
defaultModel: GOOGLE_GEMINI_DEFAULT_MODEL,
expectedProviders: ["google"],
applyConfig: (cfg) => applyGoogleGeminiModelDefault(cfg).next,
wizard: {
choiceId: "gemini-api-key",
choiceLabel: "Google Gemini API key",
groupId: "google",
groupLabel: "Google",
groupHint: "Gemini API key + OAuth",
},
}),
],
normalizeTransport: ({ api, baseUrl }) =>
resolveGoogleGenerativeAiTransport({ api, baseUrl }),
normalizeConfig: ({ provider, providerConfig }) =>
normalizeGoogleProviderConfig(provider, providerConfig),
normalizeModelId: ({ modelId }) => normalizeGoogleModelId(modelId),
resolveDynamicModel: (ctx) =>
resolveGoogleGeminiForwardCompatModel({
providerId: ctx.provider,
ctx,
}),
...GOOGLE_GEMINI_PROVIDER_HOOKS,
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
});
registerGoogleProvider(api);
api.registerImageGenerationProvider(createLazyGoogleImageGenerationProvider());
api.registerMediaUnderstandingProvider(createLazyGoogleMediaUnderstandingProvider());
api.registerMusicGenerationProvider(buildGoogleMusicGenerationProvider());

View File

@@ -0,0 +1,62 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family";
import {
GOOGLE_GEMINI_DEFAULT_MODEL,
applyGoogleGeminiModelDefault,
normalizeGoogleProviderConfig,
normalizeGoogleModelId,
resolveGoogleGenerativeAiTransport,
} from "./api.js";
import { isModernGoogleModel, resolveGoogleGeminiForwardCompatModel } from "./provider-models.js";
const GOOGLE_GEMINI_PROVIDER_HOOKS = {
...buildProviderReplayFamilyHooks({
family: "google-gemini",
}),
...buildProviderStreamFamilyHooks("google-thinking"),
};
export function registerGoogleProvider(api: OpenClawPluginApi) {
api.registerProvider({
id: "google",
label: "Google AI Studio",
docsPath: "/providers/models",
hookAliases: ["google-antigravity", "google-vertex"],
envVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: "google",
methodId: "api-key",
label: "Google Gemini API key",
hint: "AI Studio / Gemini API key",
optionKey: "geminiApiKey",
flagName: "--gemini-api-key",
envVar: "GEMINI_API_KEY",
promptMessage: "Enter Gemini API key",
defaultModel: GOOGLE_GEMINI_DEFAULT_MODEL,
expectedProviders: ["google"],
applyConfig: (cfg) => applyGoogleGeminiModelDefault(cfg).next,
wizard: {
choiceId: "gemini-api-key",
choiceLabel: "Google Gemini API key",
groupId: "google",
groupLabel: "Google",
groupHint: "Gemini API key + OAuth",
},
}),
],
normalizeTransport: ({ api, baseUrl }) => resolveGoogleGenerativeAiTransport({ api, baseUrl }),
normalizeConfig: ({ provider, providerConfig }) =>
normalizeGoogleProviderConfig(provider, providerConfig),
normalizeModelId: ({ modelId }) => normalizeGoogleModelId(modelId),
resolveDynamicModel: (ctx) =>
resolveGoogleGeminiForwardCompatModel({
providerId: ctx.provider,
ctx,
}),
...GOOGLE_GEMINI_PROVIDER_HOOKS,
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
});
}

View File

@@ -5,12 +5,20 @@ import {
registerProviderPlugin,
requireRegisteredProvider,
} from "../../test/helpers/plugins/provider-registration.js";
import minimaxPlugin from "./index.js";
import { registerMinimaxProviders } from "./provider-registration.js";
import { createMiniMaxWebSearchProvider } from "./src/minimax-web-search-provider.js";
const minimaxProviderPlugin = {
register(api: Parameters<typeof registerMinimaxProviders>[0]) {
registerMinimaxProviders(api);
api.registerWebSearchProvider(createMiniMaxWebSearchProvider());
},
};
describe("minimax provider hooks", () => {
it("keeps native reasoning mode for MiniMax transports", async () => {
const { providers } = await registerProviderPlugin({
plugin: minimaxPlugin,
plugin: minimaxProviderPlugin,
id: "minimax",
name: "MiniMax Provider",
});
@@ -38,7 +46,7 @@ describe("minimax provider hooks", () => {
it("owns replay policy for Anthropic and OpenAI-compatible MiniMax transports", async () => {
const { providers } = await registerProviderPlugin({
plugin: minimaxPlugin,
plugin: minimaxProviderPlugin,
id: "minimax",
name: "MiniMax Provider",
});
@@ -75,7 +83,7 @@ describe("minimax provider hooks", () => {
it("owns fast-mode stream wrapping for MiniMax transports", async () => {
const { providers } = await registerProviderPlugin({
plugin: minimaxPlugin,
plugin: minimaxProviderPlugin,
id: "minimax",
name: "MiniMax Provider",
});
@@ -133,7 +141,7 @@ describe("minimax provider hooks", () => {
it("registers the bundled MiniMax web search provider", () => {
const webSearchProviders: unknown[] = [];
minimaxPlugin.register({
minimaxProviderPlugin.register({
registerProvider() {},
registerMediaUnderstandingProvider() {},
registerImageGenerationProvider() {},
@@ -155,7 +163,7 @@ describe("minimax provider hooks", () => {
it("prefers minimax-portal oauth when resolving MiniMax usage auth", async () => {
const { providers } = await registerProviderPlugin({
plugin: minimaxPlugin,
plugin: minimaxProviderPlugin,
id: "minimax",
name: "MiniMax Provider",
});

View File

@@ -1,21 +1,4 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
definePluginEntry,
type ProviderAuthContext,
type ProviderAuthResult,
type ProviderCatalogContext,
} from "openclaw/plugin-sdk/plugin-entry";
import {
MINIMAX_OAUTH_MARKER,
ensureAuthProfileStore,
listProfilesForProvider,
} from "openclaw/plugin-sdk/provider-auth";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family";
import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage";
import { isMiniMaxModernModelId, MINIMAX_DEFAULT_MODEL_ID } from "./api.js";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import {
buildMinimaxImageGenerationProvider,
buildMinimaxPortalImageGenerationProvider,
@@ -25,295 +8,19 @@ import {
minimaxPortalMediaUnderstandingProvider,
} from "./media-understanding-provider.js";
import { buildMinimaxMusicGenerationProvider } from "./music-generation-provider.js";
import type { MiniMaxRegion } from "./oauth.js";
import { applyMinimaxApiConfig, applyMinimaxApiConfigCn } from "./onboard.js";
import { buildMinimaxPortalProvider, buildMinimaxProvider } from "./provider-catalog.js";
import { registerMinimaxProviders } from "./provider-registration.js";
import { buildMinimaxSpeechProvider } from "./speech-provider.js";
import { createMiniMaxWebSearchProvider } from "./src/minimax-web-search-provider.js";
import { buildMinimaxVideoGenerationProvider } from "./video-generation-provider.js";
const API_PROVIDER_ID = "minimax";
const PORTAL_PROVIDER_ID = "minimax-portal";
const PROVIDER_LABEL = "MiniMax";
const DEFAULT_MODEL = MINIMAX_DEFAULT_MODEL_ID;
const DEFAULT_BASE_URL_CN = "https://api.minimaxi.com/anthropic";
const DEFAULT_BASE_URL_GLOBAL = "https://api.minimax.io/anthropic";
const MINIMAX_USAGE_ENV_VAR_KEYS = [
"MINIMAX_OAUTH_TOKEN",
"MINIMAX_CODE_PLAN_KEY",
"MINIMAX_CODING_API_KEY",
"MINIMAX_API_KEY",
] as const;
const HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
family: "hybrid-anthropic-openai",
anthropicModelDropThinkingBlocks: true,
});
const MINIMAX_FAST_MODE_STREAM_HOOKS = buildProviderStreamFamilyHooks("minimax-fast-mode");
function resolveMinimaxReasoningOutputMode(): "native" {
// Keep MiniMax on native reasoning mode. Tagged enforcement previously
// suppressed normal assistant replies on this Anthropic-compatible surface.
return "native";
}
function getDefaultBaseUrl(region: MiniMaxRegion): string {
return region === "cn" ? DEFAULT_BASE_URL_CN : DEFAULT_BASE_URL_GLOBAL;
}
function apiModelRef(modelId: string): string {
return `${API_PROVIDER_ID}/${modelId}`;
}
function portalModelRef(modelId: string): string {
return `${PORTAL_PROVIDER_ID}/${modelId}`;
}
function buildPortalProviderCatalog(params: { baseUrl: string; apiKey: string }) {
return {
...buildMinimaxPortalProvider(),
baseUrl: params.baseUrl,
apiKey: params.apiKey,
};
}
function resolveApiCatalog(ctx: ProviderCatalogContext) {
const apiKey = ctx.resolveProviderApiKey(API_PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...buildMinimaxProvider(ctx.env),
apiKey,
},
};
}
function resolvePortalCatalog(ctx: ProviderCatalogContext) {
const explicitProvider = ctx.config.models?.providers?.[PORTAL_PROVIDER_ID];
const envApiKey = ctx.resolveProviderApiKey(PORTAL_PROVIDER_ID).apiKey;
const authStore = ensureAuthProfileStore(ctx.agentDir, {
allowKeychainPrompt: false,
});
const hasProfiles = listProfilesForProvider(authStore, PORTAL_PROVIDER_ID).length > 0;
const explicitApiKey =
typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined;
const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? MINIMAX_OAUTH_MARKER : undefined);
if (!apiKey) {
return null;
}
const explicitBaseUrl =
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : undefined;
return {
provider: buildPortalProviderCatalog({
baseUrl: explicitBaseUrl || buildMinimaxPortalProvider(ctx.env).baseUrl,
apiKey,
}),
};
}
function createOAuthHandler(region: MiniMaxRegion) {
const defaultBaseUrl = getDefaultBaseUrl(region);
const regionLabel = region === "cn" ? "CN" : "Global";
return async (ctx: ProviderAuthContext): Promise<ProviderAuthResult> => {
const progress = ctx.prompter.progress(`Starting MiniMax OAuth (${regionLabel})…`);
try {
const { loginMiniMaxPortalOAuth } = await import("./oauth.runtime.js");
const result = await loginMiniMaxPortalOAuth({
openUrl: ctx.openUrl,
note: ctx.prompter.note,
progress,
region,
});
progress.stop("MiniMax OAuth complete");
if (result.notification_message) {
await ctx.prompter.note(result.notification_message, "MiniMax OAuth");
}
const baseUrl = result.resourceUrl || defaultBaseUrl;
return buildOauthProviderAuthResult({
providerId: PORTAL_PROVIDER_ID,
defaultModel: portalModelRef(DEFAULT_MODEL),
access: result.access,
refresh: result.refresh,
expires: result.expires,
configPatch: {
models: {
providers: {
[PORTAL_PROVIDER_ID]: {
baseUrl,
models: [],
},
},
},
agents: {
defaults: {
models: {
[portalModelRef("MiniMax-M2.7")]: { alias: "minimax-m2.7" },
[portalModelRef("MiniMax-M2.7-highspeed")]: {
alias: "minimax-m2.7-highspeed",
},
},
},
},
},
notes: [
"MiniMax OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.",
`Base URL defaults to ${defaultBaseUrl}. Override models.providers.${PORTAL_PROVIDER_ID}.baseUrl if needed.`,
...(result.notification_message ? [result.notification_message] : []),
],
});
} catch (err) {
const errorMsg = formatErrorMessage(err);
progress.stop(`MiniMax OAuth failed: ${errorMsg}`);
await ctx.prompter.note(
"If OAuth fails, verify your MiniMax account has portal access and try again.",
"MiniMax OAuth",
);
throw err;
}
};
}
export default definePluginEntry({
id: API_PROVIDER_ID,
id: "minimax",
name: "MiniMax",
description: "Bundled MiniMax API-key and OAuth provider plugin",
register(api) {
api.registerProvider({
id: API_PROVIDER_ID,
label: PROVIDER_LABEL,
hookAliases: ["minimax-cn"],
docsPath: "/providers/minimax",
envVars: ["MINIMAX_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: API_PROVIDER_ID,
methodId: "api-global",
label: "MiniMax API key (Global)",
hint: "Global endpoint - api.minimax.io",
optionKey: "minimaxApiKey",
flagName: "--minimax-api-key",
envVar: "MINIMAX_API_KEY",
promptMessage:
"Enter MiniMax API key (sk-api- or sk-cp-)\nhttps://platform.minimax.io/user-center/basic-information/interface-key",
profileId: "minimax:global",
allowProfile: false,
defaultModel: apiModelRef(DEFAULT_MODEL),
expectedProviders: ["minimax"],
applyConfig: (cfg) => applyMinimaxApiConfig(cfg),
wizard: {
choiceId: "minimax-global-api",
choiceLabel: "MiniMax API key (Global)",
choiceHint: "Global endpoint - api.minimax.io",
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
},
}),
createProviderApiKeyAuthMethod({
providerId: API_PROVIDER_ID,
methodId: "api-cn",
label: "MiniMax API key (CN)",
hint: "CN endpoint - api.minimaxi.com",
optionKey: "minimaxApiKey",
flagName: "--minimax-api-key",
envVar: "MINIMAX_API_KEY",
promptMessage:
"Enter MiniMax CN API key (sk-api- or sk-cp-)\nhttps://platform.minimaxi.com/user-center/basic-information/interface-key",
profileId: "minimax:cn",
allowProfile: false,
defaultModel: apiModelRef(DEFAULT_MODEL),
expectedProviders: ["minimax", "minimax-cn"],
applyConfig: (cfg) => applyMinimaxApiConfigCn(cfg),
wizard: {
choiceId: "minimax-cn-api",
choiceLabel: "MiniMax API key (CN)",
choiceHint: "CN endpoint - api.minimaxi.com",
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => resolveApiCatalog(ctx),
},
resolveUsageAuth: async (ctx) => {
const portalOauth = await ctx.resolveOAuthToken({ provider: PORTAL_PROVIDER_ID });
if (portalOauth) {
return portalOauth;
}
const apiKey = ctx.resolveApiKeyFromConfigAndStore({
providerIds: [API_PROVIDER_ID, PORTAL_PROVIDER_ID],
envDirect: MINIMAX_USAGE_ENV_VAR_KEYS.map((name) => ctx.env[name]),
});
return apiKey ? { token: apiKey } : null;
},
...HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS,
...MINIMAX_FAST_MODE_STREAM_HOOKS,
resolveReasoningOutputMode: () => resolveMinimaxReasoningOutputMode(),
isModernModelRef: ({ modelId }) => isMiniMaxModernModelId(modelId),
fetchUsageSnapshot: async (ctx) =>
await fetchMinimaxUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
});
registerMinimaxProviders(api);
api.registerMediaUnderstandingProvider(minimaxMediaUnderstandingProvider);
api.registerMediaUnderstandingProvider(minimaxPortalMediaUnderstandingProvider);
api.registerProvider({
id: PORTAL_PROVIDER_ID,
label: PROVIDER_LABEL,
hookAliases: ["minimax-portal-cn"],
docsPath: "/providers/minimax",
envVars: ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
catalog: {
run: async (ctx) => resolvePortalCatalog(ctx),
},
auth: [
{
id: "oauth",
label: "MiniMax OAuth (Global)",
hint: "Global endpoint - api.minimax.io",
kind: "device_code",
wizard: {
choiceId: "minimax-global-oauth",
choiceLabel: "MiniMax OAuth (Global)",
choiceHint: "Global endpoint - api.minimax.io",
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
},
run: createOAuthHandler("global"),
},
{
id: "oauth-cn",
label: "MiniMax OAuth (CN)",
hint: "CN endpoint - api.minimaxi.com",
kind: "device_code",
wizard: {
choiceId: "minimax-cn-oauth",
choiceLabel: "MiniMax OAuth (CN)",
choiceHint: "CN endpoint - api.minimaxi.com",
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
},
run: createOAuthHandler("cn"),
},
],
...HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS,
...MINIMAX_FAST_MODE_STREAM_HOOKS,
resolveReasoningOutputMode: () => resolveMinimaxReasoningOutputMode(),
isModernModelRef: ({ modelId }) => isMiniMaxModernModelId(modelId),
});
api.registerImageGenerationProvider(buildMinimaxImageGenerationProvider());
api.registerImageGenerationProvider(buildMinimaxPortalImageGenerationProvider());
api.registerMusicGenerationProvider(buildMinimaxMusicGenerationProvider());

View File

@@ -0,0 +1,296 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type {
OpenClawPluginApi,
ProviderAuthContext,
ProviderAuthResult,
ProviderCatalogContext,
} from "openclaw/plugin-sdk/plugin-entry";
import {
MINIMAX_OAUTH_MARKER,
ensureAuthProfileStore,
listProfilesForProvider,
} from "openclaw/plugin-sdk/provider-auth";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family";
import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage";
import { isMiniMaxModernModelId, MINIMAX_DEFAULT_MODEL_ID } from "./api.js";
import type { MiniMaxRegion } from "./oauth.js";
import { applyMinimaxApiConfig, applyMinimaxApiConfigCn } from "./onboard.js";
import { buildMinimaxPortalProvider, buildMinimaxProvider } from "./provider-catalog.js";
const API_PROVIDER_ID = "minimax";
const PORTAL_PROVIDER_ID = "minimax-portal";
const PROVIDER_LABEL = "MiniMax";
const DEFAULT_MODEL = MINIMAX_DEFAULT_MODEL_ID;
const DEFAULT_BASE_URL_CN = "https://api.minimaxi.com/anthropic";
const DEFAULT_BASE_URL_GLOBAL = "https://api.minimax.io/anthropic";
const MINIMAX_USAGE_ENV_VAR_KEYS = [
"MINIMAX_OAUTH_TOKEN",
"MINIMAX_CODE_PLAN_KEY",
"MINIMAX_CODING_API_KEY",
"MINIMAX_API_KEY",
] as const;
const HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
family: "hybrid-anthropic-openai",
anthropicModelDropThinkingBlocks: true,
});
const MINIMAX_FAST_MODE_STREAM_HOOKS = buildProviderStreamFamilyHooks("minimax-fast-mode");
function resolveMinimaxReasoningOutputMode(): "native" {
return "native";
}
function getDefaultBaseUrl(region: MiniMaxRegion): string {
return region === "cn" ? DEFAULT_BASE_URL_CN : DEFAULT_BASE_URL_GLOBAL;
}
function apiModelRef(modelId: string): string {
return `${API_PROVIDER_ID}/${modelId}`;
}
function portalModelRef(modelId: string): string {
return `${PORTAL_PROVIDER_ID}/${modelId}`;
}
function buildPortalProviderCatalog(params: { baseUrl: string; apiKey: string }) {
return {
...buildMinimaxPortalProvider(),
baseUrl: params.baseUrl,
apiKey: params.apiKey,
};
}
function resolveApiCatalog(ctx: ProviderCatalogContext) {
const apiKey = ctx.resolveProviderApiKey(API_PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...buildMinimaxProvider(ctx.env),
apiKey,
},
};
}
function resolvePortalCatalog(ctx: ProviderCatalogContext) {
const explicitProvider = ctx.config.models?.providers?.[PORTAL_PROVIDER_ID];
const envApiKey = ctx.resolveProviderApiKey(PORTAL_PROVIDER_ID).apiKey;
const authStore = ensureAuthProfileStore(ctx.agentDir, {
allowKeychainPrompt: false,
});
const hasProfiles = listProfilesForProvider(authStore, PORTAL_PROVIDER_ID).length > 0;
const explicitApiKey =
typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined;
const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? MINIMAX_OAUTH_MARKER : undefined);
if (!apiKey) {
return null;
}
const explicitBaseUrl =
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : undefined;
return {
provider: buildPortalProviderCatalog({
baseUrl: explicitBaseUrl || buildMinimaxPortalProvider(ctx.env).baseUrl,
apiKey,
}),
};
}
function createOAuthHandler(region: MiniMaxRegion) {
const defaultBaseUrl = getDefaultBaseUrl(region);
const regionLabel = region === "cn" ? "CN" : "Global";
return async (ctx: ProviderAuthContext): Promise<ProviderAuthResult> => {
const progress = ctx.prompter.progress(`Starting MiniMax OAuth (${regionLabel})…`);
try {
const { loginMiniMaxPortalOAuth } = await import("./oauth.runtime.js");
const result = await loginMiniMaxPortalOAuth({
openUrl: ctx.openUrl,
note: ctx.prompter.note,
progress,
region,
});
progress.stop("MiniMax OAuth complete");
if (result.notification_message) {
await ctx.prompter.note(result.notification_message, "MiniMax OAuth");
}
const baseUrl = result.resourceUrl || defaultBaseUrl;
return buildOauthProviderAuthResult({
providerId: PORTAL_PROVIDER_ID,
defaultModel: portalModelRef(DEFAULT_MODEL),
access: result.access,
refresh: result.refresh,
expires: result.expires,
configPatch: {
models: {
providers: {
[PORTAL_PROVIDER_ID]: {
baseUrl,
models: [],
},
},
},
agents: {
defaults: {
models: {
[portalModelRef("MiniMax-M2.7")]: { alias: "minimax-m2.7" },
[portalModelRef("MiniMax-M2.7-highspeed")]: {
alias: "minimax-m2.7-highspeed",
},
},
},
},
},
notes: [
"MiniMax OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.",
`Base URL defaults to ${defaultBaseUrl}. Override models.providers.${PORTAL_PROVIDER_ID}.baseUrl if needed.`,
...(result.notification_message ? [result.notification_message] : []),
],
});
} catch (err) {
const errorMsg = formatErrorMessage(err);
progress.stop(`MiniMax OAuth failed: ${errorMsg}`);
await ctx.prompter.note(
"If OAuth fails, verify your MiniMax account has portal access and try again.",
"MiniMax OAuth",
);
throw err;
}
};
}
export function registerMinimaxProviders(api: OpenClawPluginApi) {
api.registerProvider({
id: API_PROVIDER_ID,
label: PROVIDER_LABEL,
hookAliases: ["minimax-cn"],
docsPath: "/providers/minimax",
envVars: ["MINIMAX_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: API_PROVIDER_ID,
methodId: "api-global",
label: "MiniMax API key (Global)",
hint: "Global endpoint - api.minimax.io",
optionKey: "minimaxApiKey",
flagName: "--minimax-api-key",
envVar: "MINIMAX_API_KEY",
promptMessage:
"Enter MiniMax API key (sk-api- or sk-cp-)\nhttps://platform.minimax.io/user-center/basic-information/interface-key",
profileId: "minimax:global",
allowProfile: false,
defaultModel: apiModelRef(DEFAULT_MODEL),
expectedProviders: ["minimax"],
applyConfig: (cfg) => applyMinimaxApiConfig(cfg),
wizard: {
choiceId: "minimax-global-api",
choiceLabel: "MiniMax API key (Global)",
choiceHint: "Global endpoint - api.minimax.io",
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
},
}),
createProviderApiKeyAuthMethod({
providerId: API_PROVIDER_ID,
methodId: "api-cn",
label: "MiniMax API key (CN)",
hint: "CN endpoint - api.minimaxi.com",
optionKey: "minimaxApiKey",
flagName: "--minimax-api-key",
envVar: "MINIMAX_API_KEY",
promptMessage:
"Enter MiniMax CN API key (sk-api- or sk-cp-)\nhttps://platform.minimaxi.com/user-center/basic-information/interface-key",
profileId: "minimax:cn",
allowProfile: false,
defaultModel: apiModelRef(DEFAULT_MODEL),
expectedProviders: ["minimax", "minimax-cn"],
applyConfig: (cfg) => applyMinimaxApiConfigCn(cfg),
wizard: {
choiceId: "minimax-cn-api",
choiceLabel: "MiniMax API key (CN)",
choiceHint: "CN endpoint - api.minimaxi.com",
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => resolveApiCatalog(ctx),
},
resolveUsageAuth: async (ctx) => {
const portalOauth = await ctx.resolveOAuthToken({ provider: PORTAL_PROVIDER_ID });
if (portalOauth) {
return portalOauth;
}
const apiKey = ctx.resolveApiKeyFromConfigAndStore({
providerIds: [API_PROVIDER_ID, PORTAL_PROVIDER_ID],
envDirect: MINIMAX_USAGE_ENV_VAR_KEYS.map((name) => ctx.env[name]),
});
return apiKey ? { token: apiKey } : null;
},
...HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS,
...MINIMAX_FAST_MODE_STREAM_HOOKS,
resolveReasoningOutputMode: () => resolveMinimaxReasoningOutputMode(),
isModernModelRef: ({ modelId }) => isMiniMaxModernModelId(modelId),
fetchUsageSnapshot: async (ctx) =>
await fetchMinimaxUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
});
api.registerProvider({
id: PORTAL_PROVIDER_ID,
label: PROVIDER_LABEL,
hookAliases: ["minimax-portal-cn"],
docsPath: "/providers/minimax",
envVars: ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
catalog: {
run: async (ctx) => resolvePortalCatalog(ctx),
},
auth: [
{
id: "oauth",
label: "MiniMax OAuth (Global)",
hint: "Global endpoint - api.minimax.io",
kind: "device_code",
wizard: {
choiceId: "minimax-global-oauth",
choiceLabel: "MiniMax OAuth (Global)",
choiceHint: "Global endpoint - api.minimax.io",
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
},
run: createOAuthHandler("global"),
},
{
id: "oauth-cn",
label: "MiniMax OAuth (CN)",
hint: "CN endpoint - api.minimaxi.com",
kind: "device_code",
wizard: {
choiceId: "minimax-cn-oauth",
choiceLabel: "MiniMax OAuth (CN)",
choiceHint: "CN endpoint - api.minimaxi.com",
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
},
run: createOAuthHandler("cn"),
},
],
...HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS,
...MINIMAX_FAST_MODE_STREAM_HOOKS,
resolveReasoningOutputMode: () => resolveMinimaxReasoningOutputMode(),
isModernModelRef: ({ modelId }) => isMiniMaxModernModelId(modelId),
});
}

View File

@@ -2,11 +2,10 @@ import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runt
import { createNonExitingRuntime } from "openclaw/plugin-sdk/runtime-env";
import { withEnv } from "openclaw/plugin-sdk/testing";
import { describe, expect, it, vi } from "vitest";
import { capturePluginRegistration } from "../../src/plugins/captured-registration.js";
import { createWizardPrompter } from "../../test/helpers/wizard-prompter.js";
import xaiPlugin from "./index.js";
import { resolveXaiCatalogEntry } from "./model-definitions.js";
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
import { resolveFallbackXaiAuth } from "./src/tool-auth-shared.js";
import { __testing, createXaiWebSearchProvider } from "./web-search.js";
const {
@@ -188,84 +187,63 @@ describe("xai web search config resolution", () => {
});
it("reuses the plugin web search api key for provider auth fallback", () => {
const captured = capturePluginRegistration(xaiPlugin);
const provider = captured.providers[0];
expect(
provider?.resolveSyntheticAuth?.({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-provider-fallback", // pragma: allowlist secret
},
resolveFallbackXaiAuth({
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-provider-fallback", // pragma: allowlist secret
},
},
},
},
},
provider: "xai",
providerConfig: undefined,
}),
} as never),
).toEqual({
apiKey: "xai-provider-fallback",
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key",
});
});
it("reuses the legacy grok web search api key for provider auth fallback", () => {
const captured = capturePluginRegistration(xaiPlugin);
const provider = captured.providers[0];
expect(
provider?.resolveSyntheticAuth?.({
config: {
tools: {
web: {
search: {
grok: {
apiKey: "xai-legacy-fallback", // pragma: allowlist secret
},
resolveFallbackXaiAuth({
tools: {
web: {
search: {
grok: {
apiKey: "xai-legacy-fallback", // pragma: allowlist secret
},
},
},
},
provider: "xai",
providerConfig: undefined,
}),
} as never),
).toEqual({
apiKey: "xai-legacy-fallback",
source: "tools.web.search.grok.apiKey",
mode: "api-key",
});
});
it("returns a managed marker for SecretRef-backed plugin auth fallback", () => {
const captured = capturePluginRegistration(xaiPlugin);
const provider = captured.providers[0];
expect(
provider?.resolveSyntheticAuth?.({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/api-key" },
},
resolveFallbackXaiAuth({
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/api-key" },
},
},
},
},
},
provider: "xai",
providerConfig: undefined,
}),
} as never),
).toEqual({
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key",
});
});