From a7604f8170a3dadb2eef542fda3b0512c5422cab Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 10:36:12 +0100 Subject: [PATCH] fix(minimax): support token plan tts auth --- CHANGELOG.md | 1 + .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- docs/providers/minimax.md | 12 +- docs/tools/tts.md | 11 +- extensions/minimax/minimax.live.test.ts | 35 ++++ extensions/minimax/speech-provider.test.ts | 171 +++++++++++++++--- extensions/minimax/speech-provider.ts | 81 ++++++++- extensions/minimax/tts.ts | 14 +- src/plugin-sdk/provider-auth.ts | 61 +++++++ 9 files changed, 352 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8bbab03bf6..815d4283414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 2872651ff84..99d4b0480a5 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -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 diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index 1c928780ac7..88bc78ff027 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -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 diff --git a/docs/tools/tts.md b/docs/tools/tts.md index 6f8529eb180..bd1f071d652 100644 --- a/docs/tools/tts.md +++ b/docs/tools/tts.md @@ -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 diff --git a/extensions/minimax/minimax.live.test.ts b/extensions/minimax/minimax.live.test.ts index 7b1f26f5d79..7ed844cdb62 100644 --- a/extensions/minimax/minimax.live.test.ts +++ b/extensions/minimax/minimax.live.test.ts @@ -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); +}); diff --git a/extensions/minimax/speech-provider.test.ts b/extensions/minimax/speech-provider.test.ts index 6d62ce3faa6..84be7f1ddfa 100644 --- a/extensions/minimax/speech-provider.test.ts +++ b/extensions/minimax/speech-provider.test.ts @@ -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 () => { diff --git a/extensions/minimax/speech-provider.ts b/extensions/minimax/speech-provider.ts index c9b526c89e2..9ab723ea94a 100644 --- a/extensions/minimax/speech-provider.ts +++ b/extensions/minimax/speech-provider.ts @@ -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 { + return await resolveProviderAuthProfileApiKey({ + cfg, + provider: MINIMAX_PORTAL_PROVIDER_ID, + }); +} + +async function resolveMinimaxTtsApiKey(params: { + cfg: OpenClawConfig | undefined; + configApiKey?: string; +}): Promise { + return ( + params.configApiKey ?? + (await resolveMinimaxPortalProfileToken(params.cfg)) ?? + resolveMinimaxTokenPlanEnvKey() ?? + trimToUndefined(process.env.MINIMAX_API_KEY) + ); +} + function normalizeMinimaxProviderConfig( rawConfig: Record, + 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, diff --git a/extensions/minimax/tts.ts b/extensions/minimax/tts.ts index 364e786bebb..22cf850dd5b 100644 --- a/extensions/minimax/tts.ts +++ b/extensions/minimax/tts.ts @@ -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 { diff --git a/src/plugin-sdk/provider-auth.ts b/src/plugin-sdk/provider-auth.ts index 35f77b24d1c..8ef12652f83 100644 --- a/src/plugin-sdk/provider-auth.ts +++ b/src/plugin-sdk/provider-auth.ts @@ -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 { + 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; +}