Skip to content

Commit 879c88c

Browse files
committed
fix proxy handling in adaptors
1 parent d50c983 commit 879c88c

File tree

8 files changed

+237
-96
lines changed

8 files changed

+237
-96
lines changed

apps/api/src/stt/connection.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type QueuedPayload = { payload: WsPayload; size: number };
1717
export type WsProxyOptions = {
1818
headers?: Record<string, string>;
1919
controlMessageTypes?: ReadonlySet<string>;
20+
transformFirstMessage?: (payload: WsPayload) => WsPayload;
2021
};
2122

2223
const DEFAULT_CONTROL_MESSAGE_TYPES = new Set<string>();
@@ -37,6 +38,8 @@ export class WsProxyConnection {
3738

3839
private readonly headers?: Record<string, string>;
3940
private readonly controlMessageTypes: ReadonlySet<string>;
41+
private readonly transformFirstMessage?: (payload: WsPayload) => WsPayload;
42+
private hasTransformedFirst = false;
4043

4144
constructor(
4245
private upstreamUrl: string,
@@ -45,6 +48,7 @@ export class WsProxyConnection {
4548
this.headers = options.headers;
4649
this.controlMessageTypes =
4750
options.controlMessageTypes ?? DEFAULT_CONTROL_MESSAGE_TYPES;
51+
this.transformFirstMessage = options.transformFirstMessage;
4852
}
4953

5054
private clearErrorTimeout() {
@@ -343,18 +347,26 @@ export class WsProxyConnection {
343347
return;
344348
}
345349

350+
let finalPayload = payload;
351+
if (!this.hasTransformedFirst && this.transformFirstMessage) {
352+
finalPayload = this.transformFirstMessage(payload);
353+
this.hasTransformedFirst = true;
354+
} else if (!this.hasTransformedFirst) {
355+
this.hasTransformedFirst = true;
356+
}
357+
346358
const isControlPayload = payloadIsControlMessage(
347-
payload,
359+
finalPayload,
348360
this.controlMessageTypes,
349361
);
350362

351363
if (!this.upstreamReady) {
352-
this.enqueuePendingPayload(payload, isControlPayload);
364+
this.enqueuePendingPayload(finalPayload, isControlPayload);
353365
return;
354366
}
355367

356368
try {
357-
this.upstream.send(payload);
369+
this.upstream.send(finalPayload);
358370
} catch (error) {
359371
console.error(error);
360372
this.closeConnections(DEFAULT_CLOSE_CODE, "upstream_send_failed");

apps/api/src/stt/soniox.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,22 @@ export const createSonioxProxy = (): WsProxyConnection => {
1212
throw new Error("SONIOX_API_KEY not configured");
1313
}
1414

15+
const apiKey = env.SONIOX_API_KEY;
1516
const target = buildSonioxUrl();
17+
1618
return new WsProxyConnection(target.toString(), {
1719
controlMessageTypes: CONTROL_MESSAGE_TYPES,
20+
transformFirstMessage: (payload) => {
21+
if (typeof payload !== "string") {
22+
return payload;
23+
}
24+
try {
25+
const config = JSON.parse(payload);
26+
config.api_key = apiKey;
27+
return JSON.stringify(config);
28+
} catch {
29+
return payload;
30+
}
31+
},
1832
});
1933
};

owhisper/owhisper-client/src/adapter/assemblyai/mod.rs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ impl AssemblyAIAdapter {
2727
);
2828
}
2929

30+
if let Some(proxy_result) = super::build_proxy_ws_url(api_base) {
31+
return proxy_result;
32+
}
33+
3034
if api_base.contains(".eu.") || api_base.ends_with("-eu") {
3135
return (
3236
"wss://streaming.eu.assemblyai.com/v3/ws"
@@ -70,26 +74,25 @@ mod tests {
7074
}
7175

7276
#[test]
73-
fn test_streaming_ws_url_preserves_query_params() {
74-
let (url, params) =
75-
AssemblyAIAdapter::streaming_ws_url("https://api.hyprnote.com/v1?provider=assemblyai");
76-
assert_eq!(url.as_str(), "wss://api.hyprnote.com/v1/v3/ws");
77-
assert_eq!(params, vec![("provider".into(), "assemblyai".into())]);
77+
fn test_streaming_ws_url_empty_uses_default() {
78+
let (url, params) = AssemblyAIAdapter::streaming_ws_url("");
79+
assert_eq!(url.as_str(), "wss://streaming.assemblyai.com/v3/ws");
80+
assert!(params.is_empty());
7881
}
7982

8083
#[test]
81-
fn test_streaming_ws_url_no_double_v3_ws() {
82-
let (url, params) = AssemblyAIAdapter::streaming_ws_url(
83-
"https://api.hyprnote.com/v3/ws?provider=assemblyai",
84-
);
85-
assert_eq!(url.as_str(), "wss://api.hyprnote.com/v3/ws");
84+
fn test_streaming_ws_url_proxy() {
85+
let (url, params) =
86+
AssemblyAIAdapter::streaming_ws_url("https://api.hyprnote.com?provider=assemblyai");
87+
assert_eq!(url.as_str(), "wss://api.hyprnote.com/listen");
8688
assert_eq!(params, vec![("provider".into(), "assemblyai".into())]);
8789
}
8890

8991
#[test]
90-
fn test_streaming_ws_url_empty_uses_default() {
91-
let (url, params) = AssemblyAIAdapter::streaming_ws_url("");
92-
assert_eq!(url.as_str(), "wss://streaming.assemblyai.com/v3/ws");
93-
assert!(params.is_empty());
92+
fn test_streaming_ws_url_localhost() {
93+
let (url, params) =
94+
AssemblyAIAdapter::streaming_ws_url("http://localhost:8787?provider=assemblyai");
95+
assert_eq!(url.as_str(), "ws://localhost:8787/listen");
96+
assert_eq!(params, vec![("provider".into(), "assemblyai".into())]);
9497
}
9598
}

owhisper/owhisper-client/src/adapter/fireworks/mod.rs

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,21 @@ impl FireworksAdapter {
5656
return default_url();
5757
}
5858

59+
if let Some(proxy_result) = super::build_proxy_ws_url(api_base) {
60+
return proxy_result;
61+
}
62+
5963
let parsed: url::Url = match api_base.parse() {
6064
Ok(u) => u,
6165
Err(_) => return default_url(),
6266
};
6367

64-
let host = parsed.host_str().unwrap_or(DEFAULT_API_HOST);
6568
let existing_params = super::extract_query_params(&parsed);
6669

67-
if Self::is_fireworks_host(host) {
68-
let url: url::Url = format!("wss://{}{}", Self::ws_host(api_base), WS_PATH)
69-
.parse()
70-
.expect("invalid_ws_url");
71-
(url, existing_params)
72-
} else {
73-
let mut url = parsed;
74-
url.set_query(None);
75-
super::append_path_if_missing(&mut url, WS_PATH);
76-
super::set_scheme_from_host(&mut url);
77-
(url, existing_params)
78-
}
70+
let url: url::Url = format!("wss://{}{}", Self::ws_host(api_base), WS_PATH)
71+
.parse()
72+
.expect("invalid_ws_url");
73+
(url, existing_params)
7974
}
8075
}
8176

@@ -108,22 +103,16 @@ mod tests {
108103
let (url, params) = FireworksAdapter::build_ws_url_from_base(
109104
"https://api.hyprnote.com/listen?provider=fireworks",
110105
);
111-
assert_eq!(
112-
url.as_str(),
113-
"wss://api.hyprnote.com/listen/v1/audio/transcriptions/streaming"
114-
);
106+
assert_eq!(url.as_str(), "wss://api.hyprnote.com/listen");
115107
assert_eq!(params, vec![("provider".into(), "fireworks".into())]);
116108
}
117109

118110
#[test]
119-
fn test_build_ws_url_from_base_proxy_no_double_path() {
111+
fn test_build_ws_url_from_base_localhost() {
120112
let (url, params) = FireworksAdapter::build_ws_url_from_base(
121-
"https://api.hyprnote.com/v1/audio/transcriptions/streaming?provider=fireworks",
122-
);
123-
assert_eq!(
124-
url.as_str(),
125-
"wss://api.hyprnote.com/v1/audio/transcriptions/streaming"
113+
"http://localhost:8787/listen?provider=fireworks",
126114
);
115+
assert_eq!(url.as_str(), "ws://localhost:8787/listen");
127116
assert_eq!(params, vec![("provider".into(), "fireworks".into())]);
128117
}
129118
}

owhisper/owhisper-client/src/adapter/mod.rs

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,30 @@ pub fn is_hyprnote_cloud_host(base_url: &str) -> bool {
108108
host_matches(base_url, |h| h.contains("hyprnote.com"))
109109
}
110110

111-
pub fn append_provider_param(base_url: &str, provider: &str) -> String {
112-
if !is_hyprnote_cloud_host(base_url) {
113-
return base_url.to_string();
111+
pub fn build_proxy_ws_url(api_base: &str) -> Option<(url::Url, Vec<(String, String)>)> {
112+
const PROXY_PATH: &str = "/listen";
113+
114+
if api_base.is_empty() {
115+
return None;
114116
}
115117

118+
let parsed: url::Url = api_base.parse().ok()?;
119+
let host = parsed.host_str()?;
120+
121+
if !host.contains("hyprnote.com") && !is_local_host(host) {
122+
return None;
123+
}
124+
125+
let existing_params = extract_query_params(&parsed);
126+
let mut url = parsed;
127+
url.set_query(None);
128+
url.set_path(PROXY_PATH);
129+
set_scheme_from_host(&mut url);
130+
131+
Some((url, existing_params))
132+
}
133+
134+
pub fn append_provider_param(base_url: &str, provider: &str) -> String {
116135
match url::Url::parse(base_url) {
117136
Ok(mut url) => {
118137
url.query_pairs_mut().append_pair("provider", provider);
@@ -152,3 +171,80 @@ impl AdapterKind {
152171
}
153172
}
154173
}
174+
175+
#[cfg(test)]
176+
mod tests {
177+
use super::*;
178+
179+
#[test]
180+
fn test_build_proxy_ws_url() {
181+
let cases: &[(&str, Option<(&str, Vec<(&str, &str)>)>)] = &[
182+
("", None),
183+
("https://api.deepgram.com", None),
184+
("https://api.soniox.com", None),
185+
("https://api.fireworks.ai", None),
186+
("https://api.assemblyai.com", None),
187+
(
188+
"https://api.hyprnote.com?provider=soniox",
189+
Some((
190+
"wss://api.hyprnote.com/listen",
191+
vec![("provider", "soniox")],
192+
)),
193+
),
194+
(
195+
"https://api.hyprnote.com/listen?provider=deepgram",
196+
Some((
197+
"wss://api.hyprnote.com/listen",
198+
vec![("provider", "deepgram")],
199+
)),
200+
),
201+
(
202+
"https://api.hyprnote.com/some/path?provider=fireworks",
203+
Some((
204+
"wss://api.hyprnote.com/listen",
205+
vec![("provider", "fireworks")],
206+
)),
207+
),
208+
(
209+
"http://localhost:8787?provider=soniox",
210+
Some(("ws://localhost:8787/listen", vec![("provider", "soniox")])),
211+
),
212+
(
213+
"http://localhost:8787/listen?provider=deepgram",
214+
Some(("ws://localhost:8787/listen", vec![("provider", "deepgram")])),
215+
),
216+
(
217+
"http://127.0.0.1:8787?provider=assemblyai",
218+
Some((
219+
"ws://127.0.0.1:8787/listen",
220+
vec![("provider", "assemblyai")],
221+
)),
222+
),
223+
];
224+
225+
for (input, expected) in cases {
226+
let result = build_proxy_ws_url(input);
227+
match (result, expected) {
228+
(None, None) => {}
229+
(Some((url, params)), Some((expected_url, expected_params))) => {
230+
assert_eq!(url.as_str(), *expected_url, "input: {}", input);
231+
assert_eq!(
232+
params,
233+
expected_params
234+
.iter()
235+
.map(|(k, v)| (k.to_string(), v.to_string()))
236+
.collect::<Vec<_>>(),
237+
"input: {}",
238+
input
239+
);
240+
}
241+
(result, expected) => {
242+
panic!(
243+
"input: {}, expected: {:?}, got: {:?}",
244+
input, expected, result
245+
);
246+
}
247+
}
248+
}
249+
}
250+
}

owhisper/owhisper-client/src/adapter/soniox/live.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,7 @@ impl RealtimeSttAdapter for SonioxAdapter {
4444
params: &ListenParams,
4545
channels: u8,
4646
) -> Option<Message> {
47-
let api_key = match api_key {
48-
Some(key) => key,
49-
None => {
50-
tracing::warn!("soniox_api_key_missing");
51-
return None;
52-
}
53-
};
47+
let api_key = api_key.unwrap_or("");
5448

5549
let requested = params.model.as_deref().unwrap_or("stt-v3");
5650
let model = match requested {

0 commit comments

Comments
 (0)