Skip to content

Commit e86eef6

Browse files
renal128cursoragent
andcommitted
refactor: make conversation endpoints configurable per session
Add Endpoints on ConversationConfig and wire them through TokenService and the connection managers. Drop ConnectionConstants and the global apiEndpoint/websocketUrl overrides. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 218b1b0 commit e86eef6

13 files changed

Lines changed: 103 additions & 76 deletions

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,18 @@ ElevenLabs.configure(
232232
)
233233
```
234234

235+
### Custom Endpoints
236+
237+
Override any of the three endpoints for a proxy, regional host, or staging:
238+
239+
```swift
240+
let config = ConversationConfig(
241+
endpoints: Endpoints(
242+
conversationToken: URL(string: "https://my-proxy.example.com/v1/convai/conversation/token")!
243+
)
244+
)
245+
```
246+
235247
### Fine-Grained Callbacks
236248

237249
Want to handle events without Combine? Use `ConversationCallbacks`:

Sources/ElevenLabs/Internal/Authorization/ConnectionConstants.swift

Lines changed: 0 additions & 9 deletions
This file was deleted.

Sources/ElevenLabs/Internal/Authorization/TokenServicing.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import Foundation
22

33
protocol TokenServicing: Sendable {
44
/// Fetch connection details for ElevenLabs conversation
5-
/// - Parameter configuration: The configuration to use for fetching connection details
6-
/// - Returns: The connection details for the ElevenLabs conversation
7-
func fetchConnectionDetails(configuration: ConversationCredentials) async throws -> TokenService.ConnectionDetails
5+
/// - Parameter credentials: The credentials to authenticate with
6+
func fetchConnectionDetails(
7+
credentials: ConversationCredentials
8+
) async throws -> TokenService.ConnectionDetails
89
}
910

1011
extension TokenService: TokenServicing {}

Sources/ElevenLabs/Internal/DI/Dependencies.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,14 @@ final class Dependencies: ConversationDependencyProvider {
1414
let webRTCConnectionManager: any WebRTCConnectionManaging
1515
let webSocketConnectionManager: any WebSocketConnectionManaging
1616

17-
init() {
17+
init(endpoints: Endpoints = .production) {
1818
let globalConfig = ElevenLabs.Global.shared.configuration
19-
let tokenService = TokenService(configuration: TokenService.Configuration(
20-
apiEndpoint: globalConfig.apiEndpoint?.absoluteString,
21-
websocketURL: globalConfig.websocketUrl
22-
))
2319
let logger = SDKLogger(logLevel: globalConfig.logLevel)
2420
self.logger = logger
25-
webRTCConnectionManager = WebRTCConnectionManager(logger: logger, tokenService: tokenService)
26-
webSocketConnectionManager = WebSocketConnectionManager(logger: logger)
21+
webRTCConnectionManager = WebRTCConnectionManager(
22+
logger: logger,
23+
tokenService: TokenService(endpoints: endpoints)
24+
)
25+
webSocketConnectionManager = WebSocketConnectionManager(logger: logger, endpoints: endpoints)
2726
}
2827
}

Sources/ElevenLabs/Internal/Networking/WebRTCConnectionManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ final class WebRTCConnectionManager: WebRTCConnectionManaging {
9797
let connectionDetails = try await runPhase(
9898
timing: \.tokenFetch, metrics: &metrics, startTime: startTime
9999
) {
100-
try await tokenService.fetchConnectionDetails(configuration: auth)
100+
try await tokenService.fetchConnectionDetails(credentials: auth)
101101
}
102102

103103
// 2. Request microphone permission (denial doesn't block startup).

Sources/ElevenLabs/Internal/Networking/WebSocketConnectionManager.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ final class WebSocketConnectionManager: WebSocketConnectionManaging {
1717

1818
private let urlSession: URLSession
1919
private let logger: any Logging
20+
private let endpoints: Endpoints
2021
private var task: URLSessionWebSocketTask?
2122
private var receiveTask: Task<Void, Never>?
2223
private var initiationMetadataWaiter: ConversationInitiationMetadataWaiter?
2324

24-
init(logger: any Logging) {
25+
init(logger: any Logging, endpoints: Endpoints = .production) {
2526
self.logger = logger
27+
self.endpoints = endpoints
2628
urlSession = URLSession(configuration: .default)
2729
}
2830

@@ -46,7 +48,7 @@ final class WebSocketConnectionManager: WebSocketConnectionManaging {
4648

4749
let url: URL
4850
do {
49-
url = try Self.url(for: auth)
51+
url = try Self.url(for: auth, endpoints: endpoints)
5052
} catch {
5153
metrics.total = Date().timeIntervalSince(startTime)
5254
let convError = error as? ConversationError ?? .authenticationFailed(error.localizedDescription)
@@ -147,10 +149,10 @@ final class WebSocketConnectionManager: WebSocketConnectionManaging {
147149
}
148150
}
149151

150-
static func url(for auth: ConversationCredentials) throws -> URL {
152+
static func url(for auth: ConversationCredentials, endpoints: Endpoints) throws -> URL {
151153
switch auth.authSource {
152154
case let .publicAgentId(agentId):
153-
var components = URLComponents(string: ConnectionConstants.textConversationUrl)
155+
var components = URLComponents(url: endpoints.textWebSocket, resolvingAgainstBaseURL: false)
154156
components?.queryItems = [URLQueryItem(name: "agent_id", value: agentId)]
155157
guard let url = components?.url else {
156158
throw ConversationError.authenticationFailed("Invalid conversation URL")

Sources/ElevenLabs/Public/Authorization/TokenService.swift

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,7 @@ public struct TokenService: Sendable {
2424
public let participantToken: String
2525
}
2626

27-
/// Optional configuration for advanced use cases
28-
public struct Configuration: Sendable {
29-
/// Custom API endpoint (for testing or enterprise deployments)
30-
public let apiEndpoint: String?
31-
/// Custom WebSocket URL (for testing or enterprise deployments)
32-
public let websocketURL: String?
33-
34-
public init(apiEndpoint: String? = nil, websocketURL: String? = nil) {
35-
self.apiEndpoint = apiEndpoint
36-
self.websocketURL = websocketURL
37-
}
38-
39-
public static let `default` = Configuration()
40-
}
41-
42-
private let configuration: Configuration
27+
private let endpoints: Endpoints
4328
private let urlSession: URLSession
4429

4530
// Development-only API key for testing private agents
@@ -48,20 +33,20 @@ public struct TokenService: Sendable {
4833
public let debugApiKey: String?
4934

5035
public init(
51-
configuration: Configuration = .default,
36+
endpoints: Endpoints = .production,
5237
urlSession: URLSession = .shared,
5338
debugApiKey: String? = nil
5439
) {
55-
self.configuration = configuration
40+
self.endpoints = endpoints
5641
self.urlSession = urlSession
5742
self.debugApiKey = debugApiKey
5843
}
5944
#else
6045
public init(
61-
configuration: Configuration = .default,
46+
endpoints: Endpoints = .production,
6247
urlSession: URLSession = .shared
6348
) {
64-
self.configuration = configuration
49+
self.endpoints = endpoints
6550
self.urlSession = urlSession
6651
}
6752
#endif
@@ -70,11 +55,16 @@ public struct TokenService: Sendable {
7055
///
7156
/// Translates internal `TokenError`s into public `ConversationError`s so
7257
/// callers only ever deal with one error type.
73-
public func fetchConnectionDetails(configuration: ConversationCredentials) async throws -> ConnectionDetails {
58+
public func fetchConnectionDetails(
59+
credentials: ConversationCredentials
60+
) async throws -> ConnectionDetails {
7461
do {
75-
let token: String = switch configuration.authSource {
62+
let token: String = switch credentials.authSource {
7663
case let .publicAgentId(agentId):
77-
try await fetchTokenFromAPI(agentId: agentId, environment: configuration.environment)
64+
try await fetchTokenFromAPI(
65+
agentId: agentId,
66+
environment: credentials.environment
67+
)
7868
case let .conversationToken(conversationToken):
7969
conversationToken
8070
case .signedWebSocketURL:
@@ -85,12 +75,10 @@ public struct TokenService: Sendable {
8575
try await provider()
8676
}
8777

88-
let websocketURL = self.configuration.websocketURL ?? ConnectionConstants.voiceConversationUrl
89-
9078
// ElevenLabs tokens contain room name and participant identity in the JWT
9179
// LiveKit will extract these automatically, so we provide empty values
9280
return ConnectionDetails(
93-
serverUrl: websocketURL,
81+
serverUrl: endpoints.voiceWebSocket.absoluteString,
9482
roomName: "", // LiveKit extracts from JWT
9583
participantName: "", // LiveKit extracts from JWT
9684
participantToken: token
@@ -104,11 +92,14 @@ public struct TokenService: Sendable {
10492
}
10593
}
10694

107-
private func fetchTokenFromAPI(agentId: String, environment: String? = nil) async throws -> String {
108-
// Build URL with agent ID as query parameter
109-
let apiUrl = configuration.apiEndpoint ?? ConnectionConstants.tokenUrl
110-
111-
guard var components = URLComponents(string: apiUrl) else {
95+
private func fetchTokenFromAPI(
96+
agentId: String,
97+
environment: String? = nil
98+
) async throws -> String {
99+
guard var components = URLComponents(
100+
url: endpoints.conversationToken,
101+
resolvingAgainstBaseURL: false
102+
) else {
112103
throw TokenError.invalidURL
113104
}
114105
var queryItems = [

Sources/ElevenLabs/Public/Conversation/ConversationClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public final class ConversationClient: ObservableObject {
104104
) async throws -> ConversationStartResult {
105105
let previousConversation = session
106106
let conversation = Conversation(
107-
dependencyProvider: dependencyProvider ?? Dependencies(),
107+
dependencyProvider: dependencyProvider ?? Dependencies(endpoints: config.endpoints),
108108
config: config,
109109
callbacks: callbacks,
110110
initialMicMuted: isMicMuted

Sources/ElevenLabs/Public/Conversation/ConversationConfig.swift

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public struct ConversationConfig: Sendable {
3535
/// instead of relying on LiveKit's isSpeaking detection.
3636
public var agentStateConfiguration: AgentStateConfiguration?
3737

38+
/// Network endpoints to connect to. Override for proxies, regional hosts, or staging.
39+
public var endpoints: Endpoints
40+
3841
public init(
3942
agentOverrides: AgentOverrides? = nil,
4043
ttsOverrides: TTSOverrides? = nil,
@@ -47,7 +50,8 @@ public struct ConversationConfig: Sendable {
4750
startupConfiguration: ConversationStartupConfiguration = .default,
4851
audioConfiguration: AudioPipelineConfiguration? = nil,
4952
networkConfiguration: WebRTCConfiguration = .default,
50-
agentStateConfiguration: AgentStateConfiguration? = nil
53+
agentStateConfiguration: AgentStateConfiguration? = nil,
54+
endpoints: Endpoints = .production
5155
) {
5256
self.agentOverrides = agentOverrides
5357
self.ttsOverrides = ttsOverrides
@@ -61,6 +65,7 @@ public struct ConversationConfig: Sendable {
6165
self.audioConfiguration = audioConfiguration
6266
self.networkConfiguration = networkConfiguration
6367
self.agentStateConfiguration = agentStateConfiguration
68+
self.endpoints = endpoints
6469
}
6570
}
6671

@@ -114,3 +119,33 @@ public struct ConversationOverrides: Sendable {
114119
self.clientEvents = clientEvents
115120
}
116121
}
122+
123+
/// The network endpoints the SDK talks to. Defaults to ``production``.
124+
///
125+
/// Override for proxies, regional hosts, or staging. Credentials follow these
126+
/// endpoints — conversation tokens are sent to whichever `conversationToken`
127+
/// URL is configured.
128+
public struct Endpoints: Sendable, Equatable {
129+
/// LiveKit signaling endpoint for voice conversations.
130+
public var voiceWebSocket: URL
131+
/// WebSocket endpoint for text-only conversations.
132+
public var textWebSocket: URL
133+
/// HTTP endpoint that issues conversation tokens.
134+
public var conversationToken: URL
135+
136+
public init(
137+
voiceWebSocket: URL = Endpoints.production.voiceWebSocket,
138+
textWebSocket: URL = Endpoints.production.textWebSocket,
139+
conversationToken: URL = Endpoints.production.conversationToken
140+
) {
141+
self.voiceWebSocket = voiceWebSocket
142+
self.textWebSocket = textWebSocket
143+
self.conversationToken = conversationToken
144+
}
145+
146+
public static let production = Endpoints(
147+
voiceWebSocket: URL(string: "wss://livekit.rtc.elevenlabs.io")!,
148+
textWebSocket: URL(string: "wss://api.elevenlabs.io/v1/convai/conversation")!,
149+
conversationToken: URL(string: "https://api.elevenlabs.io/v1/convai/conversation/token")!
150+
)
151+
}

Sources/ElevenLabs/Public/ElevenLabs/ElevenLabs+Configuration.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,13 @@ import Foundation
33
extension ElevenLabs {
44
/// Global SDK configuration.
55
public struct Configuration: Sendable {
6-
public let apiEndpoint: URL?
7-
public let websocketUrl: String?
86
public let logLevel: LogLevel
97
public let debugMode: Bool
108

119
public init(
12-
apiEndpoint: URL? = nil,
13-
websocketUrl: String? = nil,
1410
logLevel: LogLevel = .warning,
1511
debugMode: Bool = false
1612
) {
17-
self.apiEndpoint = apiEndpoint
18-
self.websocketUrl = websocketUrl
1913
self.logLevel = logLevel
2014
self.debugMode = debugMode
2115
}
@@ -24,14 +18,10 @@ extension ElevenLabs {
2418

2519
/// Create a new configuration with updated values (builder pattern)
2620
public func with(
27-
apiEndpoint: URL? = nil,
28-
websocketUrl: String? = nil,
2921
logLevel: LogLevel? = nil,
3022
debugMode: Bool? = nil
3123
) -> Configuration {
3224
Configuration(
33-
apiEndpoint: apiEndpoint ?? self.apiEndpoint,
34-
websocketUrl: websocketUrl ?? self.websocketUrl,
3525
logLevel: logLevel ?? self.logLevel,
3626
debugMode: debugMode ?? self.debugMode
3727
)

0 commit comments

Comments
 (0)