fix(minimax): support token plan tts auth

This commit is contained in:
Peter Steinberger
2026-04-25 10:36:12 +01:00
parent 7fcefd56b7
commit a7604f8170
9 changed files with 352 additions and 38 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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 () => {

View File

@@ -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,

View File

@@ -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 {

View File

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