@swarmclawai/swarmclaw 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -1,12 +1,61 @@
|
|
|
1
1
|
import { describe, it } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import { requestElevenLabsMp3Stream, synthesizeElevenLabsMp3 } from './elevenlabs'
|
|
3
|
+
import { requestElevenLabsMp3Stream, resolveElevenLabsConfig, synthesizeElevenLabsMp3 } from './elevenlabs'
|
|
4
|
+
import { encryptKey, loadSecrets, loadSettings, saveSecrets, saveSettings } from './storage'
|
|
4
5
|
|
|
5
6
|
describe('elevenlabs helpers', () => {
|
|
7
|
+
it('prefers agent override first, then settings default, then env fallback', () => {
|
|
8
|
+
const originalSettings = loadSettings()
|
|
9
|
+
const originalKey = process.env.ELEVENLABS_API_KEY
|
|
10
|
+
const originalVoice = process.env.ELEVENLABS_VOICE
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
saveSettings({
|
|
14
|
+
...originalSettings,
|
|
15
|
+
elevenLabsApiKey: 'settings-key',
|
|
16
|
+
elevenLabsVoiceId: 'settings-voice',
|
|
17
|
+
})
|
|
18
|
+
process.env.ELEVENLABS_API_KEY = 'env-key'
|
|
19
|
+
process.env.ELEVENLABS_VOICE = 'env-voice'
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(resolveElevenLabsConfig('agent-voice'), {
|
|
22
|
+
apiKey: 'settings-key',
|
|
23
|
+
voiceId: 'agent-voice',
|
|
24
|
+
})
|
|
25
|
+
assert.deepEqual(resolveElevenLabsConfig(null), {
|
|
26
|
+
apiKey: 'settings-key',
|
|
27
|
+
voiceId: 'settings-voice',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
saveSettings({
|
|
31
|
+
...originalSettings,
|
|
32
|
+
elevenLabsApiKey: 'settings-key',
|
|
33
|
+
elevenLabsVoiceId: null,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(resolveElevenLabsConfig(undefined), {
|
|
37
|
+
apiKey: 'settings-key',
|
|
38
|
+
voiceId: 'env-voice',
|
|
39
|
+
})
|
|
40
|
+
} finally {
|
|
41
|
+
saveSettings(originalSettings)
|
|
42
|
+
if (originalKey === undefined) delete process.env.ELEVENLABS_API_KEY
|
|
43
|
+
else process.env.ELEVENLABS_API_KEY = originalKey
|
|
44
|
+
if (originalVoice === undefined) delete process.env.ELEVENLABS_VOICE
|
|
45
|
+
else process.env.ELEVENLABS_VOICE = originalVoice
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
6
49
|
it('synthesizeElevenLabsMp3 posts TTS request and returns audio bytes', async () => {
|
|
7
50
|
const originalFetch = global.fetch
|
|
51
|
+
const originalSettings = loadSettings()
|
|
8
52
|
const originalKey = process.env.ELEVENLABS_API_KEY
|
|
9
53
|
const originalVoice = process.env.ELEVENLABS_VOICE
|
|
54
|
+
saveSettings({
|
|
55
|
+
...originalSettings,
|
|
56
|
+
elevenLabsApiKey: null,
|
|
57
|
+
elevenLabsVoiceId: null,
|
|
58
|
+
})
|
|
10
59
|
process.env.ELEVENLABS_API_KEY = 'test-key'
|
|
11
60
|
process.env.ELEVENLABS_VOICE = 'voice-123'
|
|
12
61
|
|
|
@@ -25,6 +74,150 @@ describe('elevenlabs helpers', () => {
|
|
|
25
74
|
assert.equal(out.toString('utf8'), 'abc')
|
|
26
75
|
} finally {
|
|
27
76
|
global.fetch = originalFetch
|
|
77
|
+
saveSettings(originalSettings)
|
|
78
|
+
if (originalKey === undefined) delete process.env.ELEVENLABS_API_KEY
|
|
79
|
+
else process.env.ELEVENLABS_API_KEY = originalKey
|
|
80
|
+
if (originalVoice === undefined) delete process.env.ELEVENLABS_VOICE
|
|
81
|
+
else process.env.ELEVENLABS_VOICE = originalVoice
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('synthesizeElevenLabsMp3 retries with the built-in fallback voice when the configured voice is paid-only', async () => {
|
|
86
|
+
const originalFetch = global.fetch
|
|
87
|
+
const originalSettings = loadSettings()
|
|
88
|
+
const originalKey = process.env.ELEVENLABS_API_KEY
|
|
89
|
+
const originalVoice = process.env.ELEVENLABS_VOICE
|
|
90
|
+
saveSettings({
|
|
91
|
+
...originalSettings,
|
|
92
|
+
elevenLabsApiKey: 'settings-key',
|
|
93
|
+
elevenLabsVoiceId: 'paid-only-voice',
|
|
94
|
+
})
|
|
95
|
+
delete process.env.ELEVENLABS_API_KEY
|
|
96
|
+
delete process.env.ELEVENLABS_VOICE
|
|
97
|
+
|
|
98
|
+
const calls: string[] = []
|
|
99
|
+
global.fetch = (async (input: RequestInfo | URL) => {
|
|
100
|
+
const url = String(input)
|
|
101
|
+
calls.push(url)
|
|
102
|
+
if (url.endsWith('/paid-only-voice')) {
|
|
103
|
+
return new Response(
|
|
104
|
+
'{"detail":{"type":"payment_required","code":"paid_plan_required","message":"Free users cannot use library voices via the API."}}',
|
|
105
|
+
{ status: 402, headers: { 'Content-Type': 'application/json' } },
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
assert.equal(url, 'https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb')
|
|
109
|
+
return new Response(Buffer.from('fallback-audio'), { status: 200, headers: { 'Content-Type': 'audio/mpeg' } })
|
|
110
|
+
}) as typeof fetch
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const out = await synthesizeElevenLabsMp3({ text: 'hello world' })
|
|
114
|
+
assert.equal(out.toString('utf8'), 'fallback-audio')
|
|
115
|
+
assert.deepEqual(calls, [
|
|
116
|
+
'https://api.elevenlabs.io/v1/text-to-speech/paid-only-voice',
|
|
117
|
+
'https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb',
|
|
118
|
+
])
|
|
119
|
+
} finally {
|
|
120
|
+
global.fetch = originalFetch
|
|
121
|
+
saveSettings(originalSettings)
|
|
122
|
+
if (originalKey === undefined) delete process.env.ELEVENLABS_API_KEY
|
|
123
|
+
else process.env.ELEVENLABS_API_KEY = originalKey
|
|
124
|
+
if (originalVoice === undefined) delete process.env.ELEVENLABS_VOICE
|
|
125
|
+
else process.env.ELEVENLABS_VOICE = originalVoice
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('synthesizeElevenLabsMp3 retries with the built-in fallback voice when the configured voice id is missing', async () => {
|
|
130
|
+
const originalFetch = global.fetch
|
|
131
|
+
const originalSettings = loadSettings()
|
|
132
|
+
const originalKey = process.env.ELEVENLABS_API_KEY
|
|
133
|
+
const originalVoice = process.env.ELEVENLABS_VOICE
|
|
134
|
+
saveSettings({
|
|
135
|
+
...originalSettings,
|
|
136
|
+
elevenLabsApiKey: 'settings-key',
|
|
137
|
+
elevenLabsVoiceId: 'missing-voice',
|
|
138
|
+
})
|
|
139
|
+
delete process.env.ELEVENLABS_API_KEY
|
|
140
|
+
delete process.env.ELEVENLABS_VOICE
|
|
141
|
+
|
|
142
|
+
const calls: string[] = []
|
|
143
|
+
global.fetch = (async (input: RequestInfo | URL) => {
|
|
144
|
+
const url = String(input)
|
|
145
|
+
calls.push(url)
|
|
146
|
+
if (url.endsWith('/missing-voice')) {
|
|
147
|
+
return new Response(
|
|
148
|
+
'{"detail":{"type":"not_found","code":"voice_not_found","message":"A voice with voice_id \'missing-voice\' was not found."}}',
|
|
149
|
+
{ status: 404, headers: { 'Content-Type': 'application/json' } },
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
assert.equal(url, 'https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb')
|
|
153
|
+
return new Response(Buffer.from('fallback-audio'), { status: 200, headers: { 'Content-Type': 'audio/mpeg' } })
|
|
154
|
+
}) as typeof fetch
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const out = await synthesizeElevenLabsMp3({ text: 'hello world' })
|
|
158
|
+
assert.equal(out.toString('utf8'), 'fallback-audio')
|
|
159
|
+
assert.deepEqual(calls, [
|
|
160
|
+
'https://api.elevenlabs.io/v1/text-to-speech/missing-voice',
|
|
161
|
+
'https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb',
|
|
162
|
+
])
|
|
163
|
+
} finally {
|
|
164
|
+
global.fetch = originalFetch
|
|
165
|
+
saveSettings(originalSettings)
|
|
166
|
+
if (originalKey === undefined) delete process.env.ELEVENLABS_API_KEY
|
|
167
|
+
else process.env.ELEVENLABS_API_KEY = originalKey
|
|
168
|
+
if (originalVoice === undefined) delete process.env.ELEVENLABS_VOICE
|
|
169
|
+
else process.env.ELEVENLABS_VOICE = originalVoice
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('synthesizeElevenLabsMp3 falls back from an invalid settings key to a stored ElevenLabs secret', async () => {
|
|
174
|
+
const originalFetch = global.fetch
|
|
175
|
+
const originalSettings = loadSettings()
|
|
176
|
+
const originalSecrets = loadSecrets()
|
|
177
|
+
const originalKey = process.env.ELEVENLABS_API_KEY
|
|
178
|
+
const originalVoice = process.env.ELEVENLABS_VOICE
|
|
179
|
+
saveSettings({
|
|
180
|
+
...originalSettings,
|
|
181
|
+
elevenLabsApiKey: 'invalid-settings-key',
|
|
182
|
+
elevenLabsVoiceId: 'voice-123',
|
|
183
|
+
})
|
|
184
|
+
saveSecrets({
|
|
185
|
+
eleven_secret: {
|
|
186
|
+
id: 'eleven_secret',
|
|
187
|
+
name: 'ElevenLabs API Key',
|
|
188
|
+
service: 'custom',
|
|
189
|
+
scope: 'global',
|
|
190
|
+
agentIds: [],
|
|
191
|
+
createdAt: Date.now(),
|
|
192
|
+
updatedAt: Date.now(),
|
|
193
|
+
encryptedValue: encryptKey('secret-fallback-key'),
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
delete process.env.ELEVENLABS_API_KEY
|
|
197
|
+
delete process.env.ELEVENLABS_VOICE
|
|
198
|
+
|
|
199
|
+
const calls: string[] = []
|
|
200
|
+
global.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
201
|
+
const key = String((init?.headers as Record<string, string>)['xi-api-key'] || '')
|
|
202
|
+
calls.push(key)
|
|
203
|
+
if (key === 'invalid-settings-key') {
|
|
204
|
+
return new Response(
|
|
205
|
+
'{"detail":{"status":"invalid_api_key","message":"Invalid API key"}}',
|
|
206
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
assert.equal(key, 'secret-fallback-key')
|
|
210
|
+
return new Response(Buffer.from('secret-audio'), { status: 200, headers: { 'Content-Type': 'audio/mpeg' } })
|
|
211
|
+
}) as typeof fetch
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const out = await synthesizeElevenLabsMp3({ text: 'hello world' })
|
|
215
|
+
assert.equal(out.toString('utf8'), 'secret-audio')
|
|
216
|
+
assert.deepEqual(calls, ['invalid-settings-key', 'secret-fallback-key'])
|
|
217
|
+
} finally {
|
|
218
|
+
global.fetch = originalFetch
|
|
219
|
+
saveSettings(originalSettings)
|
|
220
|
+
saveSecrets(originalSecrets)
|
|
28
221
|
if (originalKey === undefined) delete process.env.ELEVENLABS_API_KEY
|
|
29
222
|
else process.env.ELEVENLABS_API_KEY = originalKey
|
|
30
223
|
if (originalVoice === undefined) delete process.env.ELEVENLABS_VOICE
|
|
@@ -34,14 +227,21 @@ describe('elevenlabs helpers', () => {
|
|
|
34
227
|
|
|
35
228
|
it('requestElevenLabsMp3Stream calls streaming endpoint', async () => {
|
|
36
229
|
const originalFetch = global.fetch
|
|
230
|
+
const originalSettings = loadSettings()
|
|
37
231
|
const originalKey = process.env.ELEVENLABS_API_KEY
|
|
38
232
|
const originalVoice = process.env.ELEVENLABS_VOICE
|
|
233
|
+
saveSettings({
|
|
234
|
+
...originalSettings,
|
|
235
|
+
elevenLabsApiKey: null,
|
|
236
|
+
elevenLabsVoiceId: null,
|
|
237
|
+
})
|
|
39
238
|
process.env.ELEVENLABS_API_KEY = 'test-key'
|
|
40
239
|
process.env.ELEVENLABS_VOICE = 'voice-xyz'
|
|
41
240
|
|
|
42
241
|
global.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
43
242
|
assert.equal(String(input), 'https://api.elevenlabs.io/v1/text-to-speech/voice-xyz/stream')
|
|
44
243
|
assert.equal(init?.method, 'POST')
|
|
244
|
+
assert.equal((init?.headers as Record<string, string>)['xi-api-key'], 'test-key')
|
|
45
245
|
return new Response('stream', { status: 200, headers: { 'Content-Type': 'audio/mpeg' } })
|
|
46
246
|
}) as typeof fetch
|
|
47
247
|
|
|
@@ -51,6 +251,52 @@ describe('elevenlabs helpers', () => {
|
|
|
51
251
|
assert.equal(await res.text(), 'stream')
|
|
52
252
|
} finally {
|
|
53
253
|
global.fetch = originalFetch
|
|
254
|
+
saveSettings(originalSettings)
|
|
255
|
+
if (originalKey === undefined) delete process.env.ELEVENLABS_API_KEY
|
|
256
|
+
else process.env.ELEVENLABS_API_KEY = originalKey
|
|
257
|
+
if (originalVoice === undefined) delete process.env.ELEVENLABS_VOICE
|
|
258
|
+
else process.env.ELEVENLABS_VOICE = originalVoice
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('requestElevenLabsMp3Stream retries with the built-in fallback voice when the configured voice is paid-only', async () => {
|
|
263
|
+
const originalFetch = global.fetch
|
|
264
|
+
const originalSettings = loadSettings()
|
|
265
|
+
const originalKey = process.env.ELEVENLABS_API_KEY
|
|
266
|
+
const originalVoice = process.env.ELEVENLABS_VOICE
|
|
267
|
+
saveSettings({
|
|
268
|
+
...originalSettings,
|
|
269
|
+
elevenLabsApiKey: 'settings-key',
|
|
270
|
+
elevenLabsVoiceId: 'paid-only-voice',
|
|
271
|
+
})
|
|
272
|
+
delete process.env.ELEVENLABS_API_KEY
|
|
273
|
+
delete process.env.ELEVENLABS_VOICE
|
|
274
|
+
|
|
275
|
+
const calls: string[] = []
|
|
276
|
+
global.fetch = (async (input: RequestInfo | URL) => {
|
|
277
|
+
const url = String(input)
|
|
278
|
+
calls.push(url)
|
|
279
|
+
if (url.endsWith('/paid-only-voice/stream')) {
|
|
280
|
+
return new Response(
|
|
281
|
+
'{"detail":{"type":"payment_required","code":"paid_plan_required","message":"Free users cannot use library voices via the API."}}',
|
|
282
|
+
{ status: 402, headers: { 'Content-Type': 'application/json' } },
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
assert.equal(url, 'https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream')
|
|
286
|
+
return new Response('fallback-stream', { status: 200, headers: { 'Content-Type': 'audio/mpeg' } })
|
|
287
|
+
}) as typeof fetch
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const res = await requestElevenLabsMp3Stream({ text: 'streaming text' })
|
|
291
|
+
assert.equal(res.status, 200)
|
|
292
|
+
assert.equal(await res.text(), 'fallback-stream')
|
|
293
|
+
assert.deepEqual(calls, [
|
|
294
|
+
'https://api.elevenlabs.io/v1/text-to-speech/paid-only-voice/stream',
|
|
295
|
+
'https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream',
|
|
296
|
+
])
|
|
297
|
+
} finally {
|
|
298
|
+
global.fetch = originalFetch
|
|
299
|
+
saveSettings(originalSettings)
|
|
54
300
|
if (originalKey === undefined) delete process.env.ELEVENLABS_API_KEY
|
|
55
301
|
else process.env.ELEVENLABS_API_KEY = originalKey
|
|
56
302
|
if (originalVoice === undefined) delete process.env.ELEVENLABS_VOICE
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadSettings } from './storage'
|
|
1
|
+
import { decryptKey, loadSettings, loadSecrets } from './storage'
|
|
2
2
|
|
|
3
3
|
const DEFAULT_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
4
4
|
const DEFAULT_MODEL_ID = 'eleven_multilingual_v2'
|
|
@@ -12,12 +12,12 @@ export function resolveElevenLabsConfig(voiceId?: string | null): {
|
|
|
12
12
|
apiKey: string
|
|
13
13
|
voiceId: string
|
|
14
14
|
} {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (!apiKey) {
|
|
15
|
+
const apiKeys = resolveElevenLabsApiKeyCandidates()
|
|
16
|
+
if (!apiKeys.length) {
|
|
18
17
|
throw new Error('No ElevenLabs API key. Set one in Settings > Voice.')
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
const settings = loadSettings()
|
|
21
21
|
const resolvedVoiceId = String(
|
|
22
22
|
voiceId
|
|
23
23
|
|| settings.elevenLabsVoiceId
|
|
@@ -25,44 +25,161 @@ export function resolveElevenLabsConfig(voiceId?: string | null): {
|
|
|
25
25
|
|| DEFAULT_VOICE_ID,
|
|
26
26
|
).trim()
|
|
27
27
|
|
|
28
|
-
return { apiKey, voiceId: resolvedVoiceId || DEFAULT_VOICE_ID }
|
|
28
|
+
return { apiKey: apiKeys[0], voiceId: resolvedVoiceId || DEFAULT_VOICE_ID }
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
stability?: number
|
|
35
|
-
similarityBoost?: number
|
|
36
|
-
}): Promise<Buffer> {
|
|
37
|
-
const text = params.text.trim()
|
|
38
|
-
if (!text) throw new Error('No text provided for ElevenLabs synthesis.')
|
|
31
|
+
function resolveElevenLabsApiKeyCandidates(): string[] {
|
|
32
|
+
const settings = loadSettings()
|
|
33
|
+
const candidates: string[] = []
|
|
39
34
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
const pushCandidate = (value: unknown) => {
|
|
36
|
+
if (typeof value !== 'string') return
|
|
37
|
+
const trimmed = value.trim()
|
|
38
|
+
if (trimmed) candidates.push(trimmed)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pushCandidate(settings.elevenLabsApiKey)
|
|
42
|
+
pushCandidate(process.env.ELEVENLABS_API_KEY)
|
|
43
|
+
|
|
44
|
+
for (const secret of Object.values(loadSecrets()) as Array<Record<string, unknown>>) {
|
|
45
|
+
const label = [
|
|
46
|
+
typeof secret.id === 'string' ? secret.id : '',
|
|
47
|
+
typeof secret.name === 'string' ? secret.name : '',
|
|
48
|
+
typeof secret.service === 'string' ? secret.service : '',
|
|
49
|
+
].join(' ').toLowerCase()
|
|
50
|
+
if (!label.includes('eleven')) continue
|
|
51
|
+
if (typeof secret.encryptedValue !== 'string' || !secret.encryptedValue) continue
|
|
52
|
+
try {
|
|
53
|
+
pushCandidate(decryptKey(secret.encryptedValue))
|
|
54
|
+
} catch {
|
|
55
|
+
// Ignore undecryptable secrets and continue.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
return [...new Set(candidates)]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shouldRetryWithFallbackVoice(voiceId: string, errBody: string): boolean {
|
|
63
|
+
if (!voiceId || voiceId === DEFAULT_VOICE_ID) return false
|
|
64
|
+
return /paid_plan_required|library voices via the api|voice_not_found|voice with voice_id .* was not found/i.test(errBody)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function postElevenLabsTts(params: {
|
|
68
|
+
apiKey: string
|
|
69
|
+
voiceId: string
|
|
70
|
+
text: string
|
|
71
|
+
stability: number
|
|
72
|
+
similarityBoost: number
|
|
73
|
+
stream?: boolean
|
|
74
|
+
}): Promise<Response> {
|
|
75
|
+
const endpoint = params.stream
|
|
76
|
+
? `https://api.elevenlabs.io/v1/text-to-speech/${params.voiceId}/stream`
|
|
77
|
+
: `https://api.elevenlabs.io/v1/text-to-speech/${params.voiceId}`
|
|
78
|
+
return fetch(endpoint, {
|
|
45
79
|
method: 'POST',
|
|
46
80
|
headers: {
|
|
47
|
-
'xi-api-key': apiKey,
|
|
81
|
+
'xi-api-key': params.apiKey,
|
|
48
82
|
'Content-Type': 'application/json',
|
|
49
83
|
Accept: 'audio/mpeg',
|
|
50
84
|
},
|
|
51
85
|
body: JSON.stringify({
|
|
52
|
-
text,
|
|
86
|
+
text: params.stream ? params.text.slice(0, 2000) : params.text,
|
|
53
87
|
model_id: DEFAULT_MODEL_ID,
|
|
54
88
|
voice_settings: {
|
|
55
|
-
stability,
|
|
56
|
-
similarity_boost: similarityBoost,
|
|
89
|
+
stability: params.stability,
|
|
90
|
+
similarity_boost: params.similarityBoost,
|
|
57
91
|
},
|
|
92
|
+
...(params.stream ? { output_format: 'mp3_22050_32' } : {}),
|
|
58
93
|
}),
|
|
59
94
|
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function requestElevenLabsAudioWithFallback(params: {
|
|
98
|
+
text: string
|
|
99
|
+
voiceId?: string | null
|
|
100
|
+
stability: number
|
|
101
|
+
similarityBoost: number
|
|
102
|
+
stream?: boolean
|
|
103
|
+
}): Promise<Response> {
|
|
104
|
+
const settings = loadSettings()
|
|
105
|
+
const voiceId = String(
|
|
106
|
+
params.voiceId
|
|
107
|
+
|| settings.elevenLabsVoiceId
|
|
108
|
+
|| process.env.ELEVENLABS_VOICE
|
|
109
|
+
|| DEFAULT_VOICE_ID,
|
|
110
|
+
).trim() || DEFAULT_VOICE_ID
|
|
111
|
+
const apiKeys = resolveElevenLabsApiKeyCandidates()
|
|
112
|
+
if (!apiKeys.length) {
|
|
113
|
+
throw new Error('No ElevenLabs API key. Set one in Settings > Voice.')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let lastError = ''
|
|
117
|
+
for (const apiKey of apiKeys) {
|
|
118
|
+
const firstRes = await postElevenLabsTts({
|
|
119
|
+
apiKey,
|
|
120
|
+
voiceId,
|
|
121
|
+
text: params.text,
|
|
122
|
+
stability: params.stability,
|
|
123
|
+
similarityBoost: params.similarityBoost,
|
|
124
|
+
stream: params.stream,
|
|
125
|
+
})
|
|
126
|
+
if (firstRes.ok) return firstRes
|
|
127
|
+
|
|
128
|
+
const firstErr = await firstRes.text().catch(() => '')
|
|
129
|
+
lastError = firstErr || `ElevenLabs request failed (${firstRes.status})`
|
|
130
|
+
|
|
131
|
+
if (shouldRetryWithFallbackVoice(voiceId, firstErr)) {
|
|
132
|
+
const fallbackRes = await postElevenLabsTts({
|
|
133
|
+
apiKey,
|
|
134
|
+
voiceId: DEFAULT_VOICE_ID,
|
|
135
|
+
text: params.text,
|
|
136
|
+
stability: params.stability,
|
|
137
|
+
similarityBoost: params.similarityBoost,
|
|
138
|
+
stream: params.stream,
|
|
139
|
+
})
|
|
140
|
+
if (fallbackRes.ok) return fallbackRes
|
|
60
141
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
142
|
+
const fallbackErr = await fallbackRes.text().catch(() => '')
|
|
143
|
+
lastError = fallbackErr
|
|
144
|
+
? `${lastError} | fallback voice failed: ${fallbackErr}`
|
|
145
|
+
: lastError
|
|
146
|
+
if (!shouldTryNextApiKey(lastError)) {
|
|
147
|
+
throw new Error(lastError)
|
|
148
|
+
}
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!shouldTryNextApiKey(firstErr)) {
|
|
153
|
+
throw new Error(lastError)
|
|
154
|
+
}
|
|
64
155
|
}
|
|
65
156
|
|
|
157
|
+
throw new Error(lastError || 'ElevenLabs request failed')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function shouldTryNextApiKey(errBody: string): boolean {
|
|
161
|
+
return /invalid_api_key|missing_permissions|detected_unusual_activity/i.test(errBody)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function synthesizeElevenLabsMp3(params: {
|
|
165
|
+
text: string
|
|
166
|
+
voiceId?: string | null
|
|
167
|
+
stability?: number
|
|
168
|
+
similarityBoost?: number
|
|
169
|
+
}): Promise<Buffer> {
|
|
170
|
+
const text = params.text.trim()
|
|
171
|
+
if (!text) throw new Error('No text provided for ElevenLabs synthesis.')
|
|
172
|
+
|
|
173
|
+
const stability = Number.isFinite(params.stability) ? Math.max(0, Math.min(1, Number(params.stability))) : 0.5
|
|
174
|
+
const similarityBoost = Number.isFinite(params.similarityBoost) ? Math.max(0, Math.min(1, Number(params.similarityBoost))) : 0.75
|
|
175
|
+
|
|
176
|
+
const apiRes = await requestElevenLabsAudioWithFallback({
|
|
177
|
+
text,
|
|
178
|
+
voiceId: params.voiceId,
|
|
179
|
+
stability,
|
|
180
|
+
similarityBoost,
|
|
181
|
+
})
|
|
182
|
+
|
|
66
183
|
const audioBuffer = await apiRes.arrayBuffer()
|
|
67
184
|
return Buffer.from(audioBuffer)
|
|
68
185
|
}
|
|
@@ -74,27 +191,14 @@ export async function requestElevenLabsMp3Stream(params: {
|
|
|
74
191
|
const text = params.text.trim()
|
|
75
192
|
if (!text) throw new Error('No text provided for ElevenLabs stream.')
|
|
76
193
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Accept: 'audio/mpeg',
|
|
84
|
-
},
|
|
85
|
-
body: JSON.stringify({
|
|
86
|
-
text: text.slice(0, 2000),
|
|
87
|
-
model_id: DEFAULT_MODEL_ID,
|
|
88
|
-
voice_settings: { stability: 0.5, similarity_boost: 0.75 },
|
|
89
|
-
output_format: 'mp3_22050_32',
|
|
90
|
-
}),
|
|
194
|
+
const apiRes = await requestElevenLabsAudioWithFallback({
|
|
195
|
+
text,
|
|
196
|
+
voiceId: params.voiceId,
|
|
197
|
+
stability: 0.5,
|
|
198
|
+
similarityBoost: 0.75,
|
|
199
|
+
stream: true,
|
|
91
200
|
})
|
|
92
201
|
|
|
93
|
-
if (!apiRes.ok) {
|
|
94
|
-
const errBody = await apiRes.text().catch(() => '')
|
|
95
|
-
throw new Error(errBody || `ElevenLabs streaming request failed (${apiRes.status})`)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
202
|
return apiRes
|
|
99
203
|
}
|
|
100
204
|
|