Problem
TTS has its own credential resolution (resolveTtsApiKey in src/tts/tts.ts) that completely bypasses the auth profile store. This means:
- OpenAI API key in auth profiles (
openai:default) is invisible to TTS
- ElevenLabs keys must be configured separately in
messages.tts.elevenlabs.apiKey or ELEVENLABS_API_KEY
- No key rotation support for TTS providers
- Onboarding doesn't know TTS needs its own credential path
This is an upstream design gap — verified identical in OpenClaw (openclaw/main). Never fixed.
Current resolution (TTS)
// src/tts/tts.ts — resolveTtsApiKey (SYNC)
export function resolveTtsApiKey(
config: ResolvedTtsConfig,
provider: TtsProvider,
): string | undefined {
if (provider === "elevenlabs") {
return config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY;
}
if (provider === "openai") {
return config.openai.apiKey || process.env.OPENAI_API_KEY;
}
}
Current resolution (auth profiles)
// src/auth/provider-auth.ts
// resolveApiKeyForProvider: profile store → env var fallback (ASYNC)
// Already handles "openai" → OPENAI_API_KEY, "deepgram" → DEEPGRAM_API_KEY
Proposed fix
resolveTtsApiKey should use resolveApiKeyForProvider from src/auth/provider-auth.ts as the first check, before config and env vars. This is a direct import — no DI wrapper needed (STT and media-understanding already import auth profiles directly).
Resolution order (new)
resolveApiKeyForProvider (auth profile store → env var fallback)
→ Config fallback (messages.tts.{provider}.apiKey) — backwards compat only
Note: resolveApiKeyForProvider already includes env var fallback internally, so the config-embedded apiKey fields become a tertiary backwards-compat fallback only.
Implementation
import { resolveApiKeyForProvider } from "../auth/provider-auth.js";
export async function resolveTtsApiKey(
config: ResolvedTtsConfig,
provider: TtsProvider,
): Promise<string | undefined> {
// Skip auth lookup for providers that don't need API keys
if (provider === "edge") return undefined;
// 1. Auth profile store (unified credential system)
try {
const auth = await resolveApiKeyForProvider({
provider: ttsProviderToAuthProvider(provider),
});
if (auth.apiKey) return auth.apiKey;
} catch {
// no profile found — fall through
}
// 2. Config fallback (backwards compat)
if (provider === "elevenlabs") {
return config.elevenlabs.apiKey || undefined;
}
if (provider === "openai") {
return config.openai.apiKey || undefined;
}
return undefined;
}
function ttsProviderToAuthProvider(provider: TtsProvider): string {
// Direct 1:1 mapping — no translation needed today
return provider;
}
Sync → async impact
resolveTtsApiKey becomes async because resolveApiKeyForProvider is async (profile store I/O). This impacts 8 call sites across 3 files, including 2 currently-sync functions that must become async:
| Function |
File |
Calls |
Currently |
Impact |
getTtsProvider() |
src/tts/tts.ts |
2 |
sync |
Must become async — cascades to callers |
isTtsProviderConfigured() |
src/tts/tts.ts |
1 |
sync |
Must become async — cascades to callers |
textToSpeech() |
src/tts/tts.ts |
1 |
async |
Add await |
tts.status handler |
src/gateway/server-methods/tts.ts |
2 |
async |
Add await |
tts.providers handler |
src/gateway/server-methods/tts.ts |
2 |
async |
Add await |
handleTtsCommands() |
src/auto-reply/reply/commands-tts.ts |
2 |
async |
Add await |
Provider mapping
| TTS provider |
Auth profile provider |
Notes |
openai |
openai |
Same API key for models and TTS |
elevenlabs |
elevenlabs |
Requires #403 (add to auth provider system) |
edge |
N/A |
No API key needed |
Backwards compatibility
Fully backwards-compatible:
- If auth profiles have a matching key → use it (new behavior, better)
- If not → fall back to existing config resolution (unchanged behavior)
Depends on
Related
Problem
TTS has its own credential resolution (
resolveTtsApiKeyinsrc/tts/tts.ts) that completely bypasses the auth profile store. This means:openai:default) is invisible to TTSmessages.tts.elevenlabs.apiKeyorELEVENLABS_API_KEYThis is an upstream design gap — verified identical in OpenClaw (
openclaw/main). Never fixed.Current resolution (TTS)
Current resolution (auth profiles)
Proposed fix
resolveTtsApiKeyshould useresolveApiKeyForProviderfromsrc/auth/provider-auth.tsas the first check, before config and env vars. This is a direct import — no DI wrapper needed (STT and media-understanding already import auth profiles directly).Resolution order (new)
Note:
resolveApiKeyForProvideralready includes env var fallback internally, so the config-embeddedapiKeyfields become a tertiary backwards-compat fallback only.Implementation
Sync → async impact
resolveTtsApiKeybecomes async becauseresolveApiKeyForProvideris async (profile store I/O). This impacts 8 call sites across 3 files, including 2 currently-sync functions that must become async:getTtsProvider()src/tts/tts.tsisTtsProviderConfigured()src/tts/tts.tstextToSpeech()src/tts/tts.tsawaittts.statushandlersrc/gateway/server-methods/tts.tsawaittts.providershandlersrc/gateway/server-methods/tts.tsawaithandleTtsCommands()src/auto-reply/reply/commands-tts.tsawaitProvider mapping
openaiopenaielevenlabselevenlabsedgeBackwards compatibility
Fully backwards-compatible:
Depends on
Related
src/agents/provider-auth.tsfor OpenClaw)