"use client"; import { KeyRound, Save } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import type { OrganizationAiModelConfigurationV2 } from "@/client/types.gen"; import { type ProviderSchema, type ServiceConfigurationDefaults, ServiceConfigurationForm, type ServiceSegment, } from "@/components/ServiceConfigurationForm"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { LANGUAGE_DISPLAY_NAMES } from "@/constants/languages"; type ModelMode = "realtime" | "dograh" | "byok"; interface DograhDefaults { voices: string[]; allow_custom_input?: boolean; speeds: number[]; languages: string[]; defaults: { voice: string; speed: number; language: string; }; } export interface ModelConfigurationDefaultsV2 { dograh: DograhDefaults; byok: { pipeline: ServiceConfigurationDefaults; realtime: { realtime: Record; llm: Record; embeddings: Record; default_providers: ServiceConfigurationDefaults["default_providers"]; }; }; } interface DograhFormState { api_key: string; voice: string; speed: number; language: string; } interface AIModelConfigurationV2EditorProps { defaults: ModelConfigurationDefaultsV2; configuration?: OrganizationAiModelConfigurationV2 | Record | null; effectiveConfiguration?: Record | null; onSave: (configuration: OrganizationAiModelConfigurationV2) => Promise; submitLabel?: string; } function firstApiKey(value: unknown): string { if (Array.isArray(value)) return String(value[0] || ""); return typeof value === "string" ? value : ""; } function asRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } function isDograhEffectiveConfig(config: Record | null | undefined): boolean { if (!config || config.is_realtime) return false; const llm = asRecord(config.llm); const tts = asRecord(config.tts); const stt = asRecord(config.stt); return llm?.provider === "dograh" && tts?.provider === "dograh" && stt?.provider === "dograh"; } function byokDefaults(defaults: ModelConfigurationDefaultsV2): ServiceConfigurationDefaults { return { llm: defaults.byok.pipeline.llm, tts: defaults.byok.pipeline.tts, stt: defaults.byok.pipeline.stt, embeddings: defaults.byok.pipeline.embeddings, realtime: defaults.byok.realtime.realtime, default_providers: defaults.byok.pipeline.default_providers, }; } function byokConfigToLegacyShape(config: Record | null): Record | null { if (!config || config.mode !== "byok") return null; const byok = asRecord(config.byok); if (!byok) return null; if (byok.mode === "realtime") { const realtime = asRecord(byok.realtime); return { is_realtime: true, realtime: realtime?.realtime, llm: realtime?.llm, embeddings: realtime?.embeddings, }; } const pipeline = asRecord(byok.pipeline); return { is_realtime: false, llm: pipeline?.llm, tts: pipeline?.tts, stt: pipeline?.stt, embeddings: pipeline?.embeddings, }; } function effectiveConfigToLegacyShape(config: Record | null): Record | null { if (!config) return null; return { is_realtime: Boolean(config.is_realtime), llm: config.llm, tts: config.tts, stt: config.stt, realtime: config.realtime, embeddings: config.embeddings, }; } function emptyByokInitialConfig(isRealtime: boolean): Record { return { is_realtime: isRealtime, }; } // The v2 editor surfaces realtime ("Speech to Speech") and pipeline (BYOK) as // separate tabs, so each tab gets its own initial config. A tab is pre-filled // only when the saved (or effective) configuration matches that tab's mode; // otherwise it starts empty so the other tab's data does not leak across. function getByokInitialConfig( configuration: Record | null, effectiveConfiguration: Record | null, wantRealtime: boolean, ): Record { const matchesTab = (config: Record | null) => config ? Boolean(config.is_realtime) === wantRealtime : false; const byokConfiguration = byokConfigToLegacyShape(configuration); if (byokConfiguration) { return matchesTab(byokConfiguration) ? byokConfiguration : emptyByokInitialConfig(wantRealtime); } if (configuration?.mode === "dograh" || isDograhEffectiveConfig(effectiveConfiguration)) { return emptyByokInitialConfig(wantRealtime); } const effective = effectiveConfigToLegacyShape(effectiveConfiguration); return matchesTab(effective) ? (effective as Record) : emptyByokInitialConfig(wantRealtime); } function buildDograhState( defaults: ModelConfigurationDefaultsV2, configuration: Record | null, effectiveConfiguration: Record | null, ): DograhFormState { const fallback = defaults.dograh.defaults; const configuredDograh = configuration?.mode === "dograh" ? asRecord(configuration.dograh) : null; if (configuredDograh) { return { api_key: String(configuredDograh.api_key || ""), voice: String(configuredDograh.voice || fallback.voice), speed: Number(configuredDograh.speed || fallback.speed), language: String(configuredDograh.language || fallback.language), }; } if (isDograhEffectiveConfig(effectiveConfiguration)) { const llm = asRecord(effectiveConfiguration?.llm); const tts = asRecord(effectiveConfiguration?.tts); const stt = asRecord(effectiveConfiguration?.stt); return { api_key: firstApiKey(llm?.api_key || tts?.api_key || stt?.api_key), voice: String(tts?.voice || fallback.voice), speed: Number(tts?.speed || fallback.speed), language: String(stt?.language || fallback.language), }; } return { api_key: "", voice: fallback.voice, speed: fallback.speed, language: fallback.language, }; } function preferredMode( configuration: Record | null, effectiveConfiguration: Record | null, ): ModelMode { if (configuration?.mode === "dograh") return "dograh"; if (configuration?.mode === "byok") { return asRecord(configuration.byok)?.mode === "realtime" ? "realtime" : "byok"; } if (isDograhEffectiveConfig(effectiveConfiguration)) return "dograh"; return Boolean(effectiveConfiguration?.is_realtime) ? "realtime" : "byok"; } function hasRequiredApiKey( service: ServiceSegment, serviceConfiguration: Record, defaults: ServiceConfigurationDefaults, ): boolean { const provider = serviceConfiguration.provider as string | undefined; if (!provider) return false; const providerSchema = service === "realtime" ? defaults.realtime?.[provider] : defaults[service as "llm" | "tts" | "stt" | "embeddings"]?.[provider]; const requiresApiKey = providerSchema?.required?.includes("api_key") ?? false; if (!requiresApiKey) return true; const apiKey = serviceConfiguration.api_key; if (Array.isArray(apiKey)) { return apiKey.some((key) => typeof key === "string" && key.trim().length > 0); } return typeof apiKey === "string" && apiKey.trim().length > 0; } function requireByokService( config: Record, service: ServiceSegment, defaults: ServiceConfigurationDefaults, ): Record { const serviceConfiguration = asRecord(config[service]); if ( !serviceConfiguration || !serviceConfiguration.provider || serviceConfiguration.provider === "dograh" || !hasRequiredApiKey(service, serviceConfiguration, defaults) ) { throw new Error(`${service} configuration is required`); } return serviceConfiguration; } function optionalByokService(config: Record, service: ServiceSegment): Record | undefined { const serviceConfiguration = asRecord(config[service]); if (!serviceConfiguration?.provider || serviceConfiguration.provider === "dograh") return undefined; return serviceConfiguration; } export function AIModelConfigurationV2Editor({ defaults, configuration, effectiveConfiguration, onSave, submitLabel = "Save Configuration", }: AIModelConfigurationV2EditorProps) { const defaultsForByok = useMemo(() => byokDefaults(defaults), [defaults]); const [mode, setMode] = useState("dograh"); const [dograh, setDograh] = useState(() => ({ api_key: "", voice: defaults.dograh.defaults.voice, speed: defaults.dograh.defaults.speed, language: defaults.dograh.defaults.language, })); const [realtimeInitialConfig, setRealtimeInitialConfig] = useState | null>(null); const [pipelineInitialConfig, setPipelineInitialConfig] = useState | null>(null); const [isSavingDograh, setIsSavingDograh] = useState(false); const [isCustomVoice, setIsCustomVoice] = useState(false); const [error, setError] = useState(null); const allowCustomVoice = defaults.dograh.allow_custom_input ?? false; useEffect(() => { const rawConfiguration = asRecord(configuration); const rawEffectiveConfiguration = asRecord(effectiveConfiguration); setMode(preferredMode(rawConfiguration, rawEffectiveConfiguration)); const nextDograh = buildDograhState(defaults, rawConfiguration, rawEffectiveConfiguration); setDograh(nextDograh); setIsCustomVoice(allowCustomVoice && !defaults.dograh.voices.includes(nextDograh.voice)); setRealtimeInitialConfig(getByokInitialConfig(rawConfiguration, rawEffectiveConfiguration, true)); setPipelineInitialConfig(getByokInitialConfig(rawConfiguration, rawEffectiveConfiguration, false)); }, [configuration, defaults, effectiveConfiguration, allowCustomVoice]); const saveDograhConfiguration = async () => { setIsSavingDograh(true); setError(null); try { await onSave({ version: 2, mode: "dograh", dograh: { api_key: dograh.api_key.trim(), voice: dograh.voice, speed: dograh.speed, language: dograh.language, }, }); } catch (err) { setError(err instanceof Error ? err.message : "Failed to save configuration"); } finally { setIsSavingDograh(false); } }; const saveByokConfiguration = async (config: Record) => { setError(null); const isRealtime = Boolean(config.is_realtime); const llm = requireByokService(config, "llm", defaultsForByok); const embeddings = optionalByokService(config, "embeddings"); const body: OrganizationAiModelConfigurationV2 = { version: 2, mode: "byok", byok: isRealtime ? { mode: "realtime", realtime: { realtime: requireByokService(config, "realtime", defaultsForByok) as never, llm: llm as never, ...(embeddings ? { embeddings: embeddings as never } : {}), }, } : { mode: "pipeline", pipeline: { llm: llm as never, tts: requireByokService(config, "tts", defaultsForByok) as never, stt: requireByokService(config, "stt", defaultsForByok) as never, ...(embeddings ? { embeddings: embeddings as never } : {}), }, }, }; await onSave(body); }; return (
{error && (
{error}
)} setMode(value as ModelMode)} className="space-y-6"> Speech to Speech Dograh BYOK

A single speech-to-speech model handles the conversation in realtime (no separate transcriber or voice). An LLM is still required for variable extraction and QA.

{isCustomVoice ? ( setDograh({ ...dograh, voice: event.target.value })} /> ) : ( )} {allowCustomVoice && (
{ const custom = checked as boolean; setIsCustomVoice(custom); if (!custom) { setDograh({ ...dograh, voice: defaults.dograh.defaults.voice }); } }} />
)}
setDograh({ ...dograh, api_key: event.target.value })} placeholder="Enter API key" />
); }