From 8df3e1ad43b99169722f60115fe6da9cbec2b706 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:28:10 +0200 Subject: [PATCH] Reuse Athena router key for OpenClaw dictation --- integrations/openclaw-athena-talk/README.md | 6 +++-- .../openclaw-athena-talk/dist/index.js | 13 +++++++++-- integrations/openclaw-athena-talk/index.ts | 12 ++++++++-- .../openclaw-athena-talk/package-lock.json | 4 ++-- .../openclaw-athena-talk/package.json | 2 +- .../test-transcription.mjs | 22 +++++++++++++++++++ 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/integrations/openclaw-athena-talk/README.md b/integrations/openclaw-athena-talk/README.md index b91c946..43e93be 100644 --- a/integrations/openclaw-athena-talk/README.md +++ b/integrations/openclaw-athena-talk/README.md @@ -67,8 +67,10 @@ the 8 kHz G.711 audio through the Gateway. The plugin converts it to PCM WAV and calls the same Athena `/audio/transcriptions` endpoint used by Talk. The transcribed text is returned to the composer; this path does not invoke the agent or TTS. The transcription provider reuses `talk.realtime.providers.athena-talk` -and the configured model provider for its URL/key, so no second credential is -needed. In `talk.catalog`, it appears under `transcription.providers`. OpenClaw +and the configured model provider for its URL/key. If that model provider has +no key, it reuses `tts.providers.openai.apiKey` only when the TTS and STT URLs +have the same origin. No second credential is needed. In `talk.catalog`, it +appears under `transcription.providers`. OpenClaw currently gives a transcription provider five seconds to return its final text after recording stops; the plugin caps its Whisper request at 4.5 seconds. diff --git a/integrations/openclaw-athena-talk/dist/index.js b/integrations/openclaw-athena-talk/dist/index.js index 3fb0ec2..6f48ff9 100644 --- a/integrations/openclaw-athena-talk/dist/index.js +++ b/integrations/openclaw-athena-talk/dist/index.js @@ -95,10 +95,19 @@ function resolveModelProvider(cfg, requested) { function resolveConfig(req) { const raw = record(req.providerConfig); const modelProvider = resolveModelProvider(req.cfg, raw.modelProvider); + const baseUrl = String(raw.baseUrl || modelProvider.baseUrl || "http://192.168.1.212:8081/v1").replace(/\/$/, ""); + const tts = record(record(record(req.cfg).tts).providers).openai; + let sharedTtsKey = ""; + try { + if (new URL(String(record(tts).baseUrl)).origin === new URL(baseUrl).origin) { + sharedTtsKey = typeof record(tts).apiKey === "string" ? record(tts).apiKey : ""; + } + } + catch { /* The TTS provider may be absent or use another URL. */ } return { modelProvider: String(raw.modelProvider || ""), - baseUrl: String(raw.baseUrl || modelProvider.baseUrl || "http://192.168.1.212:8081/v1").replace(/\/$/, ""), - apiKey: String(raw.apiKey || modelProvider.apiKey || ""), + baseUrl, + apiKey: String(raw.apiKey || modelProvider.apiKey || sharedTtsKey || ""), voice: String(raw.voice || req.voice || "alloy"), language: String(raw.language || req.language || "de"), vadThreshold: Number(raw.vadThreshold ?? 0.018), diff --git a/integrations/openclaw-athena-talk/index.ts b/integrations/openclaw-athena-talk/index.ts index 2e16cc0..ec652a5 100644 --- a/integrations/openclaw-athena-talk/index.ts +++ b/integrations/openclaw-athena-talk/index.ts @@ -114,10 +114,18 @@ function resolveModelProvider(cfg: unknown, requested?: unknown): Record { const raw = record(req.providerConfig); const modelProvider = resolveModelProvider(req.cfg, raw.modelProvider); + const baseUrl = String(raw.baseUrl || modelProvider.baseUrl || "http://192.168.1.212:8081/v1").replace(/\/$/, ""); + const tts = record(record(record(req.cfg).tts).providers).openai; + let sharedTtsKey = ""; + try { + if (new URL(String(record(tts).baseUrl)).origin === new URL(baseUrl).origin) { + sharedTtsKey = typeof record(tts).apiKey === "string" ? record(tts).apiKey : ""; + } + } catch { /* The TTS provider may be absent or use another URL. */ } return { modelProvider: String(raw.modelProvider || ""), - baseUrl: String(raw.baseUrl || modelProvider.baseUrl || "http://192.168.1.212:8081/v1").replace(/\/$/, ""), - apiKey: String(raw.apiKey || modelProvider.apiKey || ""), + baseUrl, + apiKey: String(raw.apiKey || modelProvider.apiKey || sharedTtsKey || ""), voice: String(raw.voice || req.voice || "alloy"), language: String(raw.language || req.language || "de"), vadThreshold: Number(raw.vadThreshold ?? 0.018), diff --git a/integrations/openclaw-athena-talk/package-lock.json b/integrations/openclaw-athena-talk/package-lock.json index 9434fc9..9930a78 100644 --- a/integrations/openclaw-athena-talk/package-lock.json +++ b/integrations/openclaw-athena-talk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@casaderoll/openclaw-athena-talk", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@casaderoll/openclaw-athena-talk", - "version": "1.2.0", + "version": "1.2.1", "devDependencies": { "@types/node": "^24.0.0", "openclaw": "2026.9.4", diff --git a/integrations/openclaw-athena-talk/package.json b/integrations/openclaw-athena-talk/package.json index 165aafa..59892a1 100644 --- a/integrations/openclaw-athena-talk/package.json +++ b/integrations/openclaw-athena-talk/package.json @@ -1,6 +1,6 @@ { "name": "@casaderoll/openclaw-athena-talk", - "version": "1.2.0", + "version": "1.2.1", "private": true, "description": "Local OpenClaw Talk provider backed by Athena Whisper and Qwen3-TTS", "type": "module", diff --git a/integrations/openclaw-athena-talk/test-transcription.mjs b/integrations/openclaw-athena-talk/test-transcription.mjs index 7a215c8..bf5bce9 100644 --- a/integrations/openclaw-athena-talk/test-transcription.mjs +++ b/integrations/openclaw-athena-talk/test-transcription.mjs @@ -57,3 +57,25 @@ test("dictation registers separately and sends G.711 audio to Athena Whisper", a await new Promise((resolve) => server.close(resolve)); } }); + +test("dictation reuses only a TTS key for the same Athena origin", () => { + let transcription; + plugin.register({ + registerRealtimeTranscriptionProvider: (value) => { transcription = value; }, + registerRealtimeVoiceProvider: () => {}, + registerHttpRoute: () => {}, + }); + const base = { + talk: { realtime: { providers: { "athena-talk": { modelProvider: "llama-cpp" } } } }, + models: { providers: { "llama-cpp": { baseUrl: "http://athena:8081/v1" } } }, + }; + const withTts = (baseUrl) => ({ ...base, tts: { providers: { openai: { + baseUrl, apiKey: "shared-key", + } } } }); + assert.equal(transcription.resolveConfig({ + cfg: withTts("http://athena:8081/v1"), rawConfig: {}, + }).apiKey, "shared-key"); + assert.equal(transcription.resolveConfig({ + cfg: withTts("http://other:8081/v1"), rawConfig: {}, + }).apiKey, ""); +});