mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(minimax): support token plan tts auth
This commit is contained in:
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
|
||||
- GitHub Copilot: preserve encrypted Responses reasoning item IDs during replay so Copilot can validate encrypted reasoning payloads across requests. (#71448) Thanks @a410979729-sys.
|
||||
- Agents/replies: recover final-answer text when streamed assistant chunks contain only whitespace, preventing completed turns from surfacing as empty-payload errors. Fixes #71454. (#71467) Thanks @Sanjays2402.
|
||||
- Feishu/TTS: transcode voice-intent MP3 and other audio replies to Ogg/Opus before sending native Feishu audio bubbles, while keeping ordinary MP3 attachments as files. Fixes #61249 and #37868.
|
||||
- Providers/MiniMax: let TTS use MiniMax portal OAuth and Token Plan credentials before falling back to `MINIMAX_API_KEY`, and include current TTS HD model ids. Fixes #55017.
|
||||
- Telegram/webhook: acknowledge validated webhook updates before running bot middleware, keeping slow agent turns from tripping Telegram delivery retries while preserving per-chat processing lanes. Fixes #71392. Thanks @joelforsberg46-source.
|
||||
- MCP: retire one-shot embedded bundled MCP runtimes at run end, skip bundle-MCP startup when a runtime tool allowlist cannot reach bundle-MCP tools, and add `mcp.sessionIdleTtlMs` idle eviction for leaked session runtimes. Fixes #71106, #71110, #70389, and #70808.
|
||||
- MCP/config reload: hot-apply `mcp.*` changes by disposing cached session MCP runtimes, and dispose bundled MCP runtimes during gateway shutdown so removed `mcp.servers` entries reap child processes promptly. Fixes #60656.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
1c8faa44e6ad80aeca7add9793d1dee1b7c552a0220c3dcebd8475b7ecd69342 plugin-sdk-api-baseline.json
|
||||
6ae517ad38d843fb3453cff8c9a081f1f9b7fa54ee563dcef69524ed7013b57f plugin-sdk-api-baseline.jsonl
|
||||
3a217ac0157fb46f42d455f1509b70a6d4ca3c41d6da00ac412b642875e4c9ef plugin-sdk-api-baseline.json
|
||||
d962b39c50017ef8e8545d9a3902a4f37f19d338256d8c96c9f860c7a120b687 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -20,7 +20,7 @@ Provider split:
|
||||
| Provider ID | Auth | Capabilities |
|
||||
| ---------------- | ------- | --------------------------------------------------------------- |
|
||||
| `minimax` | API key | Text, image generation, image understanding, speech, web search |
|
||||
| `minimax-portal` | OAuth | Text, image generation, image understanding |
|
||||
| `minimax-portal` | OAuth | Text, image generation, image understanding, speech |
|
||||
|
||||
## Built-in catalog
|
||||
|
||||
@@ -251,6 +251,16 @@ The bundled `minimax` plugin registers MiniMax T2A v2 as a speech provider for
|
||||
|
||||
- Default TTS model: `speech-2.8-hd`
|
||||
- Default voice: `English_expressive_narrator`
|
||||
- Supported bundled model ids include `speech-2.8-hd`, `speech-2.8-turbo`,
|
||||
`speech-2.6-hd`, `speech-2.6-turbo`, `speech-02-hd`,
|
||||
`speech-02-turbo`, `speech-01-hd`, and `speech-01-turbo`.
|
||||
- Auth resolution is `messages.tts.providers.minimax.apiKey`, then
|
||||
`minimax-portal` OAuth/token auth profiles, then Token Plan environment
|
||||
keys (`MINIMAX_OAUTH_TOKEN`, `MINIMAX_CODE_PLAN_KEY`,
|
||||
`MINIMAX_CODING_API_KEY`), then `MINIMAX_API_KEY`.
|
||||
- If no TTS host is configured, OpenClaw reuses the configured
|
||||
`minimax-portal` OAuth host and strips Anthropic-compatible path suffixes
|
||||
such as `/anthropic`.
|
||||
- Normal audio attachments stay MP3.
|
||||
- Voice-note targets such as Feishu and Telegram are transcoded from MiniMax
|
||||
MP3 to 48kHz Opus with `ffmpeg`, because the Feishu/Lark file API only
|
||||
|
||||
@@ -42,7 +42,9 @@ If you want OpenAI, ElevenLabs, Google Gemini, Gradium, MiniMax, Vydra, xAI, or
|
||||
- `ELEVENLABS_API_KEY` (or `XI_API_KEY`)
|
||||
- `GEMINI_API_KEY` (or `GOOGLE_API_KEY`)
|
||||
- `GRADIUM_API_KEY`
|
||||
- `MINIMAX_API_KEY`
|
||||
- `MINIMAX_API_KEY`; MiniMax TTS also accepts Token Plan auth via
|
||||
`MINIMAX_OAUTH_TOKEN`, `MINIMAX_CODE_PLAN_KEY`, or
|
||||
`MINIMAX_CODING_API_KEY`
|
||||
- `OPENAI_API_KEY`
|
||||
- `VYDRA_API_KEY`
|
||||
- `XAI_API_KEY`
|
||||
@@ -181,6 +183,13 @@ Full schema is in [Gateway configuration](/gateway/configuration).
|
||||
}
|
||||
```
|
||||
|
||||
MiniMax TTS auth resolution is `messages.tts.providers.minimax.apiKey`, then
|
||||
stored `minimax-portal` OAuth/token profiles, then Token Plan environment keys
|
||||
(`MINIMAX_OAUTH_TOKEN`, `MINIMAX_CODE_PLAN_KEY`,
|
||||
`MINIMAX_CODING_API_KEY`), then `MINIMAX_API_KEY`. When no explicit TTS
|
||||
`baseUrl` is set, OpenClaw can reuse the configured `minimax-portal` OAuth
|
||||
host for Token Plan speech.
|
||||
|
||||
### Google Gemini primary
|
||||
|
||||
```json5
|
||||
|
||||
@@ -14,10 +14,17 @@ const MINIMAX_SEARCH_KEY =
|
||||
process.env.MINIMAX_CODING_API_KEY?.trim() ||
|
||||
MINIMAX_API_KEY ||
|
||||
"";
|
||||
const MINIMAX_TTS_TOKEN_PLAN_KEY =
|
||||
process.env.MINIMAX_OAUTH_TOKEN?.trim() ||
|
||||
process.env.MINIMAX_CODE_PLAN_KEY?.trim() ||
|
||||
process.env.MINIMAX_CODING_API_KEY?.trim() ||
|
||||
"";
|
||||
const describeLive =
|
||||
isLiveTestEnabled() && MINIMAX_SEARCH_KEY.length > 0 ? describe : describe.skip;
|
||||
const describeTtsLive =
|
||||
isLiveTestEnabled() && MINIMAX_API_KEY.length > 0 ? describe : describe.skip;
|
||||
const describeTokenPlanTtsLive =
|
||||
isLiveTestEnabled() && MINIMAX_TTS_TOKEN_PLAN_KEY.length > 0 ? describe : describe.skip;
|
||||
|
||||
const registerMinimaxPlugin = () =>
|
||||
registerProviderPlugin({
|
||||
@@ -77,3 +84,31 @@ describeTtsLive("minimax tts live", () => {
|
||||
expect(voiceNote.audioBuffer.byteLength).toBeGreaterThan(512);
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
describeTokenPlanTtsLive("minimax token plan tts live", () => {
|
||||
it("synthesizes TTS with Token Plan auth without MINIMAX_API_KEY", async () => {
|
||||
const savedApiKey = process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
try {
|
||||
const provider = buildMinimaxSpeechProvider();
|
||||
|
||||
const audioFile = await provider.synthesize({
|
||||
text: "OpenClaw MiniMax Token Plan text to speech integration test OK.",
|
||||
cfg: { plugins: { enabled: true } } as never,
|
||||
providerConfig: {},
|
||||
target: "audio-file",
|
||||
timeoutMs: 90_000,
|
||||
});
|
||||
|
||||
expect(audioFile.outputFormat).toBe("mp3");
|
||||
expect(audioFile.fileExtension).toBe(".mp3");
|
||||
expect(audioFile.audioBuffer.byteLength).toBeGreaterThan(512);
|
||||
} finally {
|
||||
if (savedApiKey === undefined) {
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
} else {
|
||||
process.env.MINIMAX_API_KEY = savedApiKey;
|
||||
}
|
||||
}
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runFfmpegMock = vi.hoisted(() => vi.fn());
|
||||
@@ -8,6 +11,13 @@ vi.mock("openclaw/plugin-sdk/media-runtime", () => ({
|
||||
|
||||
import { buildMinimaxSpeechProvider } from "./speech-provider.js";
|
||||
|
||||
function clearMinimaxAuthEnv() {
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MINIMAX_OAUTH_TOKEN;
|
||||
delete process.env.MINIMAX_CODE_PLAN_KEY;
|
||||
delete process.env.MINIMAX_CODING_API_KEY;
|
||||
}
|
||||
|
||||
describe("buildMinimaxSpeechProvider", () => {
|
||||
const provider = buildMinimaxSpeechProvider();
|
||||
|
||||
@@ -23,15 +33,28 @@ describe("buildMinimaxSpeechProvider", () => {
|
||||
|
||||
it("exposes models and voices", () => {
|
||||
expect(provider.models).toContain("speech-2.8-hd");
|
||||
expect(provider.models).toEqual(expect.arrayContaining(["speech-2.6-hd", "speech-02-hd"]));
|
||||
expect(provider.voices).toContain("English_expressive_narrator");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isConfigured", () => {
|
||||
const savedEnv = { ...process.env };
|
||||
let tempStateDir: string;
|
||||
let tempAgentDir: string;
|
||||
|
||||
afterEach(() => {
|
||||
beforeEach(async () => {
|
||||
tempStateDir = await mkdtemp(path.join(tmpdir(), "openclaw-minimax-tts-auth-"));
|
||||
tempAgentDir = path.join(tempStateDir, "agents", "main", "agent");
|
||||
await mkdir(tempAgentDir, { recursive: true });
|
||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||
process.env.OPENCLAW_AGENT_DIR = tempAgentDir;
|
||||
clearMinimaxAuthEnv();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
process.env = { ...savedEnv };
|
||||
await rm(tempStateDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns true when apiKey is in provider config", () => {
|
||||
@@ -41,7 +64,6 @@ describe("buildMinimaxSpeechProvider", () => {
|
||||
});
|
||||
|
||||
it("returns false when no apiKey anywhere", () => {
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(false);
|
||||
});
|
||||
|
||||
@@ -49,6 +71,29 @@ describe("buildMinimaxSpeechProvider", () => {
|
||||
process.env.MINIMAX_API_KEY = "sk-env";
|
||||
expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when a MiniMax Token Plan env var is set", () => {
|
||||
process.env.MINIMAX_CODING_API_KEY = "sk-cp-env";
|
||||
expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when a MiniMax portal auth profile is available", async () => {
|
||||
await writeFile(
|
||||
path.join(tempAgentDir, "auth-profiles.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"minimax-portal:test": {
|
||||
type: "token",
|
||||
provider: "minimax-portal",
|
||||
token: "portal-token",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveConfig", () => {
|
||||
@@ -94,14 +139,30 @@ describe("buildMinimaxSpeechProvider", () => {
|
||||
});
|
||||
|
||||
it("keeps trusted MINIMAX_API_HOST fallback for TTS baseUrl", () => {
|
||||
process.env.MINIMAX_API_HOST = "https://env.api.com";
|
||||
process.env.MINIMAX_API_HOST = "https://api.minimax.io/anthropic";
|
||||
process.env.MINIMAX_TTS_MODEL = "speech-01-240228";
|
||||
process.env.MINIMAX_TTS_VOICE_ID = "Chinese (Mandarin)_Gentle_Boy";
|
||||
const config = provider.resolveConfig!({ rawConfig: {}, cfg: {} as never, timeoutMs: 30000 });
|
||||
expect(config.baseUrl).toBe("https://env.api.com");
|
||||
expect(config.baseUrl).toBe("https://api.minimax.io");
|
||||
expect(config.model).toBe("speech-01-240228");
|
||||
expect(config.voiceId).toBe("Chinese (Mandarin)_Gentle_Boy");
|
||||
});
|
||||
|
||||
it("derives the TTS host from minimax-portal OAuth config", () => {
|
||||
delete process.env.MINIMAX_API_HOST;
|
||||
const config = provider.resolveConfig!({
|
||||
rawConfig: {},
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
"minimax-portal": { baseUrl: "https://api.minimaxi.com/anthropic" },
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
expect(config.baseUrl).toBe("https://api.minimaxi.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDirectiveToken", () => {
|
||||
@@ -217,15 +278,29 @@ describe("buildMinimaxSpeechProvider", () => {
|
||||
|
||||
describe("synthesize", () => {
|
||||
const savedFetch = globalThis.fetch;
|
||||
const savedEnv = { ...process.env };
|
||||
let tempStateDir: string;
|
||||
let tempAgentDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
tempStateDir = await mkdtemp(path.join(tmpdir(), "openclaw-minimax-tts-synth-"));
|
||||
tempAgentDir = path.join(tempStateDir, "agents", "main", "agent");
|
||||
await mkdir(tempAgentDir, { recursive: true });
|
||||
process.env = {
|
||||
...savedEnv,
|
||||
OPENCLAW_AGENT_DIR: tempAgentDir,
|
||||
OPENCLAW_STATE_DIR: tempStateDir,
|
||||
};
|
||||
clearMinimaxAuthEnv();
|
||||
vi.stubGlobal("fetch", vi.fn());
|
||||
runFfmpegMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
globalThis.fetch = savedFetch;
|
||||
process.env = { ...savedEnv };
|
||||
vi.restoreAllMocks();
|
||||
await rm(tempStateDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("makes correct API call and decodes hex response", async () => {
|
||||
@@ -328,24 +403,74 @@ describe("buildMinimaxSpeechProvider", () => {
|
||||
expect(body.voice_setting.pitch).toBe(0);
|
||||
});
|
||||
|
||||
it("uses a MiniMax Token Plan env var when no API key is configured", async () => {
|
||||
process.env.MINIMAX_CODING_API_KEY = "sk-cp-env";
|
||||
const hexAudio = Buffer.from("audio").toString("hex");
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ data: { audio: hexAudio } }), { status: 200 }),
|
||||
);
|
||||
|
||||
await provider.synthesize({
|
||||
text: "Token plan TTS",
|
||||
cfg: {} as never,
|
||||
providerConfig: {},
|
||||
target: "audio-file",
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
|
||||
const [, init] = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||
expect(init?.headers).toMatchObject({ Authorization: "Bearer sk-cp-env" });
|
||||
});
|
||||
|
||||
it("uses a minimax-portal auth profile before env API keys", async () => {
|
||||
process.env.MINIMAX_API_KEY = "sk-env";
|
||||
await writeFile(
|
||||
path.join(tempAgentDir, "auth-profiles.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"minimax-portal:test": {
|
||||
type: "token",
|
||||
provider: "minimax-portal",
|
||||
token: "portal-token",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const hexAudio = Buffer.from("audio").toString("hex");
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ data: { audio: hexAudio } }), { status: 200 }),
|
||||
);
|
||||
|
||||
await provider.synthesize({
|
||||
text: "Portal TTS",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
"minimax-portal": { baseUrl: "https://api.minimaxi.com/anthropic" },
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
providerConfig: {},
|
||||
target: "audio-file",
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
|
||||
const [url, init] = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||
expect(url).toBe("https://api.minimaxi.com/v1/t2a_v2");
|
||||
expect(init?.headers).toMatchObject({ Authorization: "Bearer portal-token" });
|
||||
});
|
||||
|
||||
it("throws when API key is missing", async () => {
|
||||
const savedKey = process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
try {
|
||||
await expect(
|
||||
provider.synthesize({
|
||||
text: "Test",
|
||||
cfg: {} as never,
|
||||
providerConfig: {},
|
||||
target: "audio-file",
|
||||
timeoutMs: 30000,
|
||||
}),
|
||||
).rejects.toThrow("MiniMax API key missing");
|
||||
} finally {
|
||||
if (savedKey) {
|
||||
process.env.MINIMAX_API_KEY = savedKey;
|
||||
}
|
||||
}
|
||||
await expect(
|
||||
provider.synthesize({
|
||||
text: "Test",
|
||||
cfg: {} as never,
|
||||
providerConfig: {},
|
||||
target: "audio-file",
|
||||
timeoutMs: 30000,
|
||||
}),
|
||||
).rejects.toThrow("MiniMax TTS auth missing");
|
||||
});
|
||||
|
||||
it("throws on API error with response body", async () => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { runFfmpeg } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
isProviderAuthProfileConfigured,
|
||||
type OpenClawConfig,
|
||||
resolveProviderAuthProfileApiKey,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
||||
import type {
|
||||
SpeechDirectiveTokenParseContext,
|
||||
@@ -18,6 +23,13 @@ import {
|
||||
normalizeMinimaxTtsBaseUrl,
|
||||
} from "./tts.js";
|
||||
|
||||
const MINIMAX_PORTAL_PROVIDER_ID = "minimax-portal";
|
||||
const MINIMAX_TOKEN_PLAN_ENV_VARS = [
|
||||
"MINIMAX_OAUTH_TOKEN",
|
||||
"MINIMAX_CODE_PLAN_KEY",
|
||||
"MINIMAX_CODING_API_KEY",
|
||||
] as const;
|
||||
|
||||
type MinimaxTtsProviderConfig = {
|
||||
apiKey?: string;
|
||||
baseUrl: string;
|
||||
@@ -36,8 +48,47 @@ type MinimaxTtsProviderOverrides = {
|
||||
pitch?: number;
|
||||
};
|
||||
|
||||
function resolveConfiguredPortalTtsBaseUrl(cfg: OpenClawConfig | undefined): string | undefined {
|
||||
const providers = asObject(asObject(cfg?.models)?.providers);
|
||||
const portalProvider = asObject(providers?.[MINIMAX_PORTAL_PROVIDER_ID]);
|
||||
const portalBaseUrl = trimToUndefined(portalProvider?.baseUrl);
|
||||
return portalBaseUrl ? normalizeMinimaxTtsBaseUrl(portalBaseUrl) : undefined;
|
||||
}
|
||||
|
||||
function resolveMinimaxTokenPlanEnvKey(): string | undefined {
|
||||
for (const envVar of MINIMAX_TOKEN_PLAN_ENV_VARS) {
|
||||
const value = trimToUndefined(process.env[envVar]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function resolveMinimaxPortalProfileToken(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
): Promise<string | undefined> {
|
||||
return await resolveProviderAuthProfileApiKey({
|
||||
cfg,
|
||||
provider: MINIMAX_PORTAL_PROVIDER_ID,
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveMinimaxTtsApiKey(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
configApiKey?: string;
|
||||
}): Promise<string | undefined> {
|
||||
return (
|
||||
params.configApiKey ??
|
||||
(await resolveMinimaxPortalProfileToken(params.cfg)) ??
|
||||
resolveMinimaxTokenPlanEnvKey() ??
|
||||
trimToUndefined(process.env.MINIMAX_API_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeMinimaxProviderConfig(
|
||||
rawConfig: Record<string, unknown>,
|
||||
cfg?: OpenClawConfig,
|
||||
): MinimaxTtsProviderConfig {
|
||||
const providers = asObject(rawConfig.providers);
|
||||
const raw = asObject(providers?.minimax) ?? asObject(rawConfig.minimax);
|
||||
@@ -49,6 +100,7 @@ function normalizeMinimaxProviderConfig(
|
||||
baseUrl: normalizeMinimaxTtsBaseUrl(
|
||||
trimToUndefined(raw?.baseUrl) ??
|
||||
trimToUndefined(process.env.MINIMAX_API_HOST) ??
|
||||
resolveConfiguredPortalTtsBaseUrl(cfg) ??
|
||||
DEFAULT_MINIMAX_TTS_BASE_URL,
|
||||
),
|
||||
model:
|
||||
@@ -65,11 +117,14 @@ function normalizeMinimaxProviderConfig(
|
||||
};
|
||||
}
|
||||
|
||||
function readMinimaxProviderConfig(config: SpeechProviderConfig): MinimaxTtsProviderConfig {
|
||||
const normalized = normalizeMinimaxProviderConfig({});
|
||||
function readMinimaxProviderConfig(
|
||||
config: SpeechProviderConfig,
|
||||
cfg?: OpenClawConfig,
|
||||
): MinimaxTtsProviderConfig {
|
||||
const normalized = normalizeMinimaxProviderConfig({}, cfg);
|
||||
return {
|
||||
apiKey: trimToUndefined(config.apiKey) ?? normalized.apiKey,
|
||||
baseUrl: trimToUndefined(config.baseUrl) ?? normalized.baseUrl,
|
||||
baseUrl: normalizeMinimaxTtsBaseUrl(trimToUndefined(config.baseUrl) ?? normalized.baseUrl),
|
||||
model: trimToUndefined(config.model) ?? normalized.model,
|
||||
voiceId: trimToUndefined(config.voiceId) ?? normalized.voiceId,
|
||||
speed: asFiniteNumber(config.speed) ?? normalized.speed,
|
||||
@@ -196,7 +251,7 @@ export function buildMinimaxSpeechProvider(): SpeechProviderPlugin {
|
||||
autoSelectOrder: 40,
|
||||
models: MINIMAX_TTS_MODELS,
|
||||
voices: MINIMAX_TTS_VOICES,
|
||||
resolveConfig: ({ rawConfig }) => normalizeMinimaxProviderConfig(rawConfig),
|
||||
resolveConfig: ({ rawConfig, cfg }) => normalizeMinimaxProviderConfig(rawConfig, cfg),
|
||||
parseDirectiveToken,
|
||||
resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => {
|
||||
const base = normalizeMinimaxProviderConfig(baseTtsConfig);
|
||||
@@ -242,14 +297,22 @@ export function buildMinimaxSpeechProvider(): SpeechProviderPlugin {
|
||||
...(asFiniteNumber(params.pitch) == null ? {} : { pitch: asFiniteNumber(params.pitch) }),
|
||||
}),
|
||||
listVoices: async () => MINIMAX_TTS_VOICES.map((voice) => ({ id: voice, name: voice })),
|
||||
isConfigured: ({ providerConfig }) =>
|
||||
Boolean(readMinimaxProviderConfig(providerConfig).apiKey || process.env.MINIMAX_API_KEY),
|
||||
isConfigured: ({ cfg, providerConfig }) =>
|
||||
Boolean(
|
||||
readMinimaxProviderConfig(providerConfig, cfg).apiKey ||
|
||||
isProviderAuthProfileConfigured({ cfg, provider: MINIMAX_PORTAL_PROVIDER_ID }) ||
|
||||
resolveMinimaxTokenPlanEnvKey() ||
|
||||
process.env.MINIMAX_API_KEY,
|
||||
),
|
||||
synthesize: async (req) => {
|
||||
const config = readMinimaxProviderConfig(req.providerConfig);
|
||||
const config = readMinimaxProviderConfig(req.providerConfig, req.cfg);
|
||||
const overrides = readMinimaxOverrides(req.providerOverrides);
|
||||
const apiKey = config.apiKey || process.env.MINIMAX_API_KEY;
|
||||
const apiKey = await resolveMinimaxTtsApiKey({
|
||||
cfg: req.cfg,
|
||||
configApiKey: config.apiKey,
|
||||
});
|
||||
if (!apiKey) {
|
||||
throw new Error("MiniMax API key missing");
|
||||
throw new Error("MiniMax TTS auth missing");
|
||||
}
|
||||
const audioBuffer = await minimaxTTS({
|
||||
text: req.text,
|
||||
|
||||
@@ -6,7 +6,17 @@ import {
|
||||
|
||||
export const DEFAULT_MINIMAX_TTS_BASE_URL = "https://api.minimax.io";
|
||||
|
||||
export const MINIMAX_TTS_MODELS = ["speech-2.8-hd", "speech-01-240228"] as const;
|
||||
export const MINIMAX_TTS_MODELS = [
|
||||
"speech-2.8-hd",
|
||||
"speech-2.8-turbo",
|
||||
"speech-2.6-hd",
|
||||
"speech-2.6-turbo",
|
||||
"speech-02-hd",
|
||||
"speech-02-turbo",
|
||||
"speech-01-hd",
|
||||
"speech-01-turbo",
|
||||
"speech-01-240228",
|
||||
] as const;
|
||||
|
||||
export const MINIMAX_TTS_VOICES = [
|
||||
"English_expressive_narrator",
|
||||
@@ -21,7 +31,7 @@ export function normalizeMinimaxTtsBaseUrl(baseUrl?: string): string {
|
||||
if (!trimmed) {
|
||||
return DEFAULT_MINIMAX_TTS_BASE_URL;
|
||||
}
|
||||
return trimmed.replace(/\/+$/, "");
|
||||
return trimmed.replace(/\/+$/, "").replace(/\/(?:anthropic|v1)$/i, "");
|
||||
}
|
||||
|
||||
function normalizeMinimaxTtsPitch(pitch: number): number {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// Public auth/onboarding helpers for provider plugins.
|
||||
|
||||
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||
import { resolveApiKeyForProfile } from "../agents/auth-profiles/oauth.js";
|
||||
import { resolveAuthProfileOrder } from "../agents/auth-profiles/order.js";
|
||||
import { listProfilesForProvider } from "../agents/auth-profiles/profiles.js";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles/store.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth-env.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export type { SecretInput } from "../config/types.secrets.js";
|
||||
@@ -98,3 +102,60 @@ export function isProviderApiKeyConfigured(params: {
|
||||
});
|
||||
return listProfilesForProvider(store, params.provider).length > 0;
|
||||
}
|
||||
|
||||
export function listUsableProviderAuthProfileIds(params: {
|
||||
provider: string;
|
||||
cfg?: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
}): { agentDir: string; profileIds: string[] } {
|
||||
try {
|
||||
const agentDir = params.agentDir?.trim() || resolveOpenClawAgentDir();
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
return {
|
||||
agentDir,
|
||||
profileIds: resolveAuthProfileOrder({
|
||||
cfg: params.cfg,
|
||||
store,
|
||||
provider: params.provider,
|
||||
}),
|
||||
};
|
||||
} catch {
|
||||
return { agentDir: "", profileIds: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export function isProviderAuthProfileConfigured(params: {
|
||||
provider: string;
|
||||
cfg?: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
}): boolean {
|
||||
return listUsableProviderAuthProfileIds(params).profileIds.length > 0;
|
||||
}
|
||||
|
||||
export async function resolveProviderAuthProfileApiKey(params: {
|
||||
provider: string;
|
||||
cfg?: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
}): Promise<string | undefined> {
|
||||
const { agentDir, profileIds } = listUsableProviderAuthProfileIds(params);
|
||||
if (!agentDir || profileIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
for (const profileId of profileIds) {
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
cfg: params.cfg,
|
||||
store,
|
||||
agentDir,
|
||||
profileId,
|
||||
});
|
||||
if (resolved?.apiKey) {
|
||||
return resolved.apiKey;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user