Skip to content

fix(tts): resolve TTS API keys through auth profile store #402

@alexey-pelykh

Description

@alexey-pelykh

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:

  1. If auth profiles have a matching key → use it (new behavior, better)
  2. If not → fall back to existing config resolution (unchanged behavior)

Depends on

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions