Skip to content

Commit 3c9ef9f

Browse files
authored
refactor: use envy crate for structured environment configuration (#3662)
* refactor: use envy crate for structured environment configuration Extract environment configuration into dedicated `env.rs` modules for each proxy crate (llm-proxy, transcribe-proxy, api-subscription) and use serde deserialization via the envy crate. This is a pure refactor with no behavior changes expected. The env variables and their semantics remain identical. * fix
1 parent 0ab46bc commit 3c9ef9f

File tree

15 files changed

+180
-90
lines changed

15 files changed

+180
-90
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ clap_mangen = "0.2"
197197
codes-iso-639 = "0.1.5"
198198
dirs = "6.0.0"
199199
dotenvy = "0.15.7"
200+
envy = "0.4"
200201
include_url_macro = "0.1.0"
201202
indoc = "2"
202203
isolang = "2.4"

apps/ai/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ serde_json = { workspace = true }
2222
url = { workspace = true }
2323

2424
dotenvy = { workspace = true }
25+
envy = { workspace = true }
2526
jsonwebtoken = { workspace = true }
2627
sentry = { workspace = true, features = ["tower", "tower-axum-matched-path", "tracing"] }
2728
tower = { workspace = true }

apps/ai/src/env.rs

Lines changed: 23 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,41 @@
1-
use std::collections::HashMap;
21
use std::path::Path;
32
use std::sync::OnceLock;
43

5-
use owhisper_client::Provider;
4+
use serde::Deserialize;
65

6+
fn default_port() -> u16 {
7+
3001
8+
}
9+
10+
fn filter_empty<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
11+
where
12+
D: serde::Deserializer<'de>,
13+
{
14+
let s: Option<String> = Option::deserialize(deserializer)?;
15+
Ok(s.filter(|s| !s.is_empty()))
16+
}
17+
18+
#[derive(Deserialize)]
719
pub struct Env {
20+
#[serde(default = "default_port")]
821
pub port: u16,
22+
#[serde(default, deserialize_with = "filter_empty")]
923
pub sentry_dsn: Option<String>,
24+
#[serde(default, deserialize_with = "filter_empty")]
1025
pub posthog_api_key: Option<String>,
1126
pub supabase_url: String,
12-
pub openrouter_api_key: String,
13-
api_keys: HashMap<Provider, String>,
27+
28+
#[serde(flatten)]
29+
pub llm: hypr_llm_proxy::Env,
30+
#[serde(flatten)]
31+
pub stt: hypr_transcribe_proxy::Env,
1432
}
1533

1634
static ENV: OnceLock<Env> = OnceLock::new();
1735

1836
pub fn env() -> &'static Env {
1937
ENV.get_or_init(|| {
2038
let _ = dotenvy::from_path(Path::new(env!("CARGO_MANIFEST_DIR")).join(".env"));
21-
Env::from_env()
39+
envy::from_env().expect("Failed to load environment")
2240
})
2341
}
24-
25-
impl Env {
26-
fn from_env() -> Self {
27-
let providers = [
28-
Provider::Deepgram,
29-
Provider::AssemblyAI,
30-
Provider::Soniox,
31-
Provider::Fireworks,
32-
Provider::OpenAI,
33-
Provider::Gladia,
34-
];
35-
let api_keys: HashMap<Provider, String> = providers
36-
.into_iter()
37-
.filter_map(|p| optional(p.env_key_name()).map(|key| (p, key)))
38-
.collect();
39-
40-
Self {
41-
port: parse_or("PORT", 3001),
42-
sentry_dsn: optional("SENTRY_DSN"),
43-
posthog_api_key: optional("POSTHOG_API_KEY"),
44-
supabase_url: required("SUPABASE_URL"),
45-
openrouter_api_key: required("OPENROUTER_API_KEY"),
46-
api_keys,
47-
}
48-
}
49-
50-
pub fn api_keys(&self) -> HashMap<Provider, String> {
51-
self.api_keys.clone()
52-
}
53-
54-
pub fn configured_providers(&self) -> Vec<Provider> {
55-
self.api_keys.keys().copied().collect()
56-
}
57-
58-
pub fn log_configured_providers(&self) {
59-
let providers: Vec<_> = self.configured_providers();
60-
if providers.is_empty() {
61-
tracing::warn!("no_stt_providers_configured");
62-
} else {
63-
let names: Vec<_> = providers.iter().map(|p| format!("{:?}", p)).collect();
64-
tracing::info!(providers = ?names, "stt_providers_configured");
65-
}
66-
}
67-
}
68-
69-
fn required(key: &str) -> String {
70-
std::env::var(key).unwrap_or_else(|_| panic!("{key} is required"))
71-
}
72-
73-
fn optional(key: &str) -> Option<String> {
74-
std::env::var(key).ok().filter(|s| !s.is_empty())
75-
}
76-
77-
fn parse_or<T: std::str::FromStr>(key: &str, default: T) -> T {
78-
std::env::var(key)
79-
.ok()
80-
.and_then(|v| v.parse().ok())
81-
.unwrap_or(default)
82-
}

apps/ai/src/main.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@ use env::env;
2020
pub use auth::DEVICE_FINGERPRINT_HEADER;
2121

2222
fn app() -> Router {
23+
let env = env();
24+
2325
let analytics = {
2426
let mut builder = AnalyticsClientBuilder::default();
25-
if let Some(key) = &env().posthog_api_key {
27+
if let Some(key) = &env.posthog_api_key {
2628
builder = builder.with_posthog(key);
2729
}
2830
Arc::new(builder.build())
2931
};
3032

31-
let llm_config = hypr_llm_proxy::LlmProxyConfig::new(&env().openrouter_api_key)
32-
.with_analytics(analytics.clone());
33-
let stt_config =
34-
hypr_transcribe_proxy::SttProxyConfig::new(env().api_keys()).with_analytics(analytics);
35-
let auth_state = AuthState::new(&env().supabase_url);
33+
let llm_config =
34+
hypr_llm_proxy::LlmProxyConfig::new(&env.llm).with_analytics(analytics.clone());
35+
let stt_config = hypr_transcribe_proxy::SttProxyConfig::new(&env.stt).with_analytics(analytics);
36+
let auth_state = AuthState::new(&env.supabase_url);
3637

3738
let protected_routes = Router::new()
3839
.merge(hypr_transcribe_proxy::listen_router(stt_config.clone()))
@@ -170,7 +171,7 @@ fn main() -> std::io::Result<()> {
170171
.with(sentry::integrations::tracing::layer())
171172
.init();
172173

173-
env.log_configured_providers();
174+
hypr_transcribe_proxy::ApiKeys::from(&env.stt).log_configured_providers();
174175

175176
tokio::runtime::Builder::new_multi_thread()
176177
.enable_all()

crates/api-subscription/src/env.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use serde::Deserialize;
2+
3+
#[derive(Deserialize)]
4+
pub struct Env {
5+
pub supabase_url: String,
6+
pub supabase_anon_key: String,
7+
pub stripe_api_key: String,
8+
pub stripe_monthly_price_id: String,
9+
pub stripe_yearly_price_id: String,
10+
}

crates/api-subscription/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
mod config;
2+
mod env;
23
mod error;
34
mod routes;
45
mod state;
56
mod supabase;
67

78
pub use config::SubscriptionConfig;
9+
pub use env::Env;
810
pub use error::{Result, SubscriptionError};
911
pub use routes::{openapi, router};
1012
pub use state::AppState;

crates/llm-proxy/src/config.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::sync::Arc;
22
use std::time::Duration;
33

44
use crate::analytics::AnalyticsReporter;
5+
use crate::env::ApiKey;
56
use crate::provider::{OpenRouterProvider, Provider};
67

78
const DEFAULT_TIMEOUT_MS: u64 = 120_000;
@@ -35,9 +36,9 @@ pub struct LlmProxyConfig {
3536
}
3637

3738
impl LlmProxyConfig {
38-
pub fn new(api_key: impl Into<String>) -> Self {
39+
pub fn new(api_key: impl Into<ApiKey>) -> Self {
3940
Self {
40-
api_key: api_key.into(),
41+
api_key: api_key.into().0,
4142
timeout: Duration::from_millis(DEFAULT_TIMEOUT_MS),
4243
models_tool_calling: vec![
4344
"moonshotai/kimi-k2-0905:exacto".into(),

crates/llm-proxy/src/env.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use serde::Deserialize;
2+
3+
#[derive(Deserialize)]
4+
pub struct Env {
5+
pub openrouter_api_key: String,
6+
}
7+
8+
pub struct ApiKey(pub String);
9+
10+
impl From<&Env> for ApiKey {
11+
fn from(env: &Env) -> Self {
12+
Self(env.openrouter_api_key.clone())
13+
}
14+
}
15+
16+
impl From<&str> for ApiKey {
17+
fn from(s: &str) -> Self {
18+
Self(s.to_string())
19+
}
20+
}
21+
22+
impl From<String> for ApiKey {
23+
fn from(s: String) -> Self {
24+
Self(s)
25+
}
26+
}

crates/llm-proxy/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
mod analytics;
22
mod config;
3+
mod env;
34
mod handler;
45
mod openapi;
56
pub mod provider;
67
mod types;
78

89
pub use analytics::{AnalyticsReporter, GenerationEvent};
910
pub use config::*;
11+
pub use env::{ApiKey, Env};
1012
pub use handler::{chat_completions_router, router};
1113
pub use hypr_analytics::{AuthenticatedUserId, DeviceFingerprint};
1214
pub use openapi::openapi;

0 commit comments

Comments
 (0)