diff --git a/extensions/openai/realtime-provider-shared.ts b/extensions/openai/realtime-provider-shared.ts new file mode 100644 index 00000000000..a01b293591c --- /dev/null +++ b/extensions/openai/realtime-provider-shared.ts @@ -0,0 +1,33 @@ +export function trimToUndefined(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +export function asFiniteNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +export function asObjectRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +export function readRealtimeErrorDetail(error: unknown): string { + if (typeof error === "string" && error) { + return error; + } + const message = asObjectRecord(error)?.message; + if (typeof message === "string" && message) { + return message; + } + return "Unknown error"; +} + +export function resolveOpenAIProviderConfigRecord( + config: Record, +): Record | undefined { + const providers = asObjectRecord(config.providers); + return ( + asObjectRecord(providers?.openai) ?? asObjectRecord(config.openai) ?? asObjectRecord(config) + ); +} diff --git a/extensions/openai/realtime-transcription-provider.ts b/extensions/openai/realtime-transcription-provider.ts index 637a9f09934..cd6614ff958 100644 --- a/extensions/openai/realtime-transcription-provider.ts +++ b/extensions/openai/realtime-transcription-provider.ts @@ -6,6 +6,12 @@ import type { } from "openclaw/plugin-sdk/realtime-transcription"; import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; import WebSocket from "ws"; +import { + asFiniteNumber, + readRealtimeErrorDetail, + resolveOpenAIProviderConfigRecord, + trimToUndefined, +} from "./realtime-provider-shared.js"; type OpenAIRealtimeTranscriptionProviderConfig = { apiKey?: string; @@ -28,36 +34,10 @@ type RealtimeEvent = { error?: unknown; }; -function trimToUndefined(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; -} - -function asNumber(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) ? value : undefined; -} - -function asObject(value: unknown): Record | undefined { - return typeof value === "object" && value !== null && !Array.isArray(value) - ? (value as Record) - : undefined; -} - -function readRealtimeErrorDetail(error: unknown): string { - if (typeof error === "string" && error) { - return error; - } - const message = asObject(error)?.message; - if (typeof message === "string" && message) { - return message; - } - return "Unknown error"; -} - function normalizeProviderConfig( config: RealtimeTranscriptionProviderConfig, ): OpenAIRealtimeTranscriptionProviderConfig { - const providers = asObject(config.providers); - const raw = asObject(providers?.openai) ?? asObject(config.openai) ?? asObject(config); + const raw = resolveOpenAIProviderConfigRecord(config); return { apiKey: normalizeResolvedSecretInputString({ @@ -69,8 +49,8 @@ function normalizeProviderConfig( path: "plugins.entries.voice-call.config.streaming.openaiApiKey", }), model: trimToUndefined(raw?.model) ?? trimToUndefined(raw?.sttModel), - silenceDurationMs: asNumber(raw?.silenceDurationMs), - vadThreshold: asNumber(raw?.vadThreshold), + silenceDurationMs: asFiniteNumber(raw?.silenceDurationMs), + vadThreshold: asFiniteNumber(raw?.vadThreshold), }; } diff --git a/extensions/openai/realtime-voice-provider.ts b/extensions/openai/realtime-voice-provider.ts index 4d36c6ccde5..b8f09cca7bc 100644 --- a/extensions/openai/realtime-voice-provider.ts +++ b/extensions/openai/realtime-voice-provider.ts @@ -7,6 +7,12 @@ import type { } from "openclaw/plugin-sdk/realtime-voice"; import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; import WebSocket from "ws"; +import { + asFiniteNumber, + readRealtimeErrorDetail, + resolveOpenAIProviderConfigRecord, + trimToUndefined, +} from "./realtime-provider-shared.js"; export type OpenAIRealtimeVoice = | "alloy" @@ -78,36 +84,10 @@ type RealtimeSessionUpdate = { }; }; -function trimToUndefined(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; -} - -function asNumber(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) ? value : undefined; -} - -function asObject(value: unknown): Record | undefined { - return typeof value === "object" && value !== null && !Array.isArray(value) - ? (value as Record) - : undefined; -} - -function readRealtimeErrorDetail(error: unknown): string { - if (typeof error === "string" && error) { - return error; - } - const message = asObject(error)?.message; - if (typeof message === "string" && message) { - return message; - } - return "Unknown error"; -} - function normalizeProviderConfig( config: RealtimeVoiceProviderConfig, ): OpenAIRealtimeVoiceProviderConfig { - const providers = asObject(config.providers); - const raw = asObject(providers?.openai) ?? asObject(config.openai) ?? asObject(config); + const raw = resolveOpenAIProviderConfigRecord(config); return { apiKey: normalizeResolvedSecretInputString({ value: raw?.apiKey, @@ -115,10 +95,10 @@ function normalizeProviderConfig( }), model: trimToUndefined(raw?.model), voice: trimToUndefined(raw?.voice) as OpenAIRealtimeVoice | undefined, - temperature: asNumber(raw?.temperature), - vadThreshold: asNumber(raw?.vadThreshold), - silenceDurationMs: asNumber(raw?.silenceDurationMs), - prefixPaddingMs: asNumber(raw?.prefixPaddingMs), + temperature: asFiniteNumber(raw?.temperature), + vadThreshold: asFiniteNumber(raw?.vadThreshold), + silenceDurationMs: asFiniteNumber(raw?.silenceDurationMs), + prefixPaddingMs: asFiniteNumber(raw?.prefixPaddingMs), azureEndpoint: trimToUndefined(raw?.azureEndpoint), azureDeployment: trimToUndefined(raw?.azureDeployment), azureApiVersion: trimToUndefined(raw?.azureApiVersion),