Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions crates/codex-plus-core/src/ads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ pub async fn fetch_ad_list_from_urls<S>(urls: &[S]) -> anyhow::Result<Value>
where
S: AsRef<str>,
{
let client = crate::http_client::proxied_client("CodexPlusPlus")?;
let client = crate::http_client::proxied_client()?;
let cache_bust = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
Expand All @@ -139,7 +139,12 @@ where
for url in urls {
let url = cache_busted_ad_url(url.as_ref(), cache_bust);
let result = async {
let response = client.get(url).send().await?.error_for_status()?;
let response = client
.get(url)
.header("User-Agent", "CodexPlusPlus")
.send()
.await?
.error_for_status()?;
let payload = response.json::<Value>().await?;
Ok::<_, anyhow::Error>(normalize_ad_payload(payload))
}
Expand Down
25 changes: 18 additions & 7 deletions crates/codex-plus-core/src/http_client.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
pub fn proxied_client(user_agent: &str) -> anyhow::Result<reqwest::Client> {
let ua = if user_agent.trim().is_empty() {
format!("CodexPlusPlus/{}", env!("CARGO_PKG_VERSION"))
} else {
user_agent.trim().to_string()
};
Ok(reqwest::Client::builder().user_agent(ua).build()?)
use std::sync::OnceLock;

/// Get or create a globally cached `reqwest::Client`.
///
/// The client is lazily initialized on the first call and reused for all subsequent
/// requests. Connection pooling (TCP/TLS) is shared across the process.
///
/// NOTE: The client carries NO default User-Agent header.
/// Callers MUST set the User-Agent on each request builder.
pub fn proxied_client() -> anyhow::Result<reqwest::Client> {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();

if let Some(client) = CLIENT.get() {
return Ok(client.clone());
}

let client = reqwest::Client::builder().build()?;
Ok(CLIENT.get_or_init(|| client).clone())
}
16 changes: 11 additions & 5 deletions crates/codex-plus-core/src/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub async fn read_codex_model_catalog() -> Value {
}
}
let env = std::env::vars().collect::<HashMap<_, _>>();
let client = match crate::http_client::proxied_client("CodexPlusPlus/1.0") {
let client = match crate::http_client::proxied_client() {
Ok(client) => client,
Err(error) => {
return json!({
Expand All @@ -60,7 +60,7 @@ pub async fn read_codex_model_catalog() -> Value {
});
}
};
read_codex_model_catalog_from_home(&home, &env, client).await
read_codex_model_catalog_from_home(&home, &env, client, "CodexPlusPlus/1.0").await
}

fn relay_profile_model_catalog_value(home: &Path, profile: &RelayProfile) -> Value {
Expand Down Expand Up @@ -113,6 +113,7 @@ pub async fn read_codex_model_catalog_from_home(
home: &Path,
env: &HashMap<String, String>,
client: reqwest::Client,
user_agent: &str,
) -> Value {
let config_path = home.join("config.toml");
let auth_api_key = read_codex_auth_api_key(&home.join("auth.json"));
Expand Down Expand Up @@ -161,7 +162,8 @@ pub async fn read_codex_model_catalog_from_home(
let mut source_statuses = Vec::new();
let mut models = Vec::new();
for source in sources.iter() {
let (source_models, mut source_status) = fetch_models_from_source(&client, source).await;
let (source_models, mut source_status) =
fetch_models_from_source(&client, source, user_agent).await;
source_status["responses_api"] = responses_api_status("unknown", "", "");
models.extend(source_models);
source_statuses.push(source_status);
Expand Down Expand Up @@ -447,6 +449,7 @@ fn provider_api_key(
async fn fetch_models_from_source(
client: &reqwest::Client,
source: &ModelSource,
user_agent: &str,
) -> (Vec<String>, Value) {
let endpoint = models_endpoint(&source.base_url);
let mut safe_source = json!({
Expand All @@ -467,6 +470,9 @@ async fn fetch_models_from_source(
let mut request = client
.get(&endpoint)
.header(reqwest::header::ACCEPT, "application/json");
if !user_agent.is_empty() {
request = request.header("User-Agent", user_agent);
}
if !source.api_key.is_empty() {
request = request.bearer_auth(&source.api_key);
}
Expand Down Expand Up @@ -524,8 +530,8 @@ pub async fn fetch_relay_profile_model_ids(
anyhow::bail!("Base URL 不能为空");
}
let endpoint = models_endpoint(&source.base_url);
let client = crate::http_client::proxied_client(&profile.user_agent)?;
let (models, status) = fetch_models_from_source(&client, &source).await;
let client = crate::http_client::proxied_client()?;
let (models, status) = fetch_models_from_source(&client, &source, &profile.user_agent).await;
if models.is_empty() {
let message = status
.get("message")
Expand Down
5 changes: 3 additions & 2 deletions crates/codex-plus-core/src/plugin_marketplace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,12 @@ async fn initialize_openai_curated_marketplace_from_github(home: &Path) -> anyho
}

async fn download_openai_plugins_zip() -> anyhow::Result<Vec<u8>> {
let client =
crate::http_client::proxied_client(&format!("Codex++/{}", crate::version::VERSION))?;
let client = crate::http_client::proxied_client()?;
let ua = format!("Codex++/{}", crate::version::VERSION);
let bytes = client
.get(OPENAI_PLUGINS_ZIP_URL)
.header(reqwest::header::ACCEPT, "application/zip")
.header("User-Agent", &ua)
.send()
.await
.context("failed to download openai/plugins marketplace")?
Expand Down
61 changes: 30 additions & 31 deletions crates/codex-plus-core/src/protocol_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,20 +530,19 @@ async fn open_responses_proxy_request_with_settings_and_user_agent(
"headerTimeoutSeconds": header_timeout.as_secs()
}),
);
let upstream = match send_upstream_request_for_responses(
upstream_request_builder(
crate::http_client::proxied_client(&effective_user_agent(
&relay.user_agent,
original_user_agent,
))?,
&endpoint,
relay.api_key.trim(),
is_stream,
&upstream_body,
),
let ua_value = effective_user_agent(&relay.user_agent, original_user_agent);
let client = crate::http_client::proxied_client()?;
let mut request = upstream_request_builder(
client,
&endpoint,
relay.api_key.trim(),
is_stream,
)
.await
&upstream_body,
);
if !ua_value.is_empty() {
request = request.header("User-Agent", &ua_value);
}
let upstream = match send_upstream_request_for_responses(request, is_stream).await
{
Ok(upstream) => upstream,
Err(error) => {
Expand Down Expand Up @@ -648,15 +647,15 @@ pub async fn open_models_proxy_request(
"wireApi": UpstreamWireApi::Responses
}),
);
let upstream = send_upstream_request(
crate::http_client::proxied_client(&effective_user_agent(
&relay.user_agent,
original_user_agent,
))?
let ua_value = effective_user_agent(&relay.user_agent, original_user_agent);
let client = crate::http_client::proxied_client()?;
let mut request = client
.get(endpoint)
.bearer_auth(relay.api_key.trim()),
)
.await?;
.bearer_auth(relay.api_key.trim());
if !ua_value.is_empty() {
request = request.header("User-Agent", &ua_value);
}
let upstream = send_upstream_request(request).await?;
let status_code = upstream.status().as_u16();
let content_type = upstream
.headers()
Expand Down Expand Up @@ -695,16 +694,16 @@ pub async fn open_chat_completions_proxy_request(
.get("stream")
.and_then(Value::as_bool)
.unwrap_or(false);
let upstream = crate::http_client::proxied_client(&effective_user_agent(
&relay.user_agent,
original_user_agent,
))?
.post(chat_completions_url(&relay.base_url))
.bearer_auth(relay.api_key.trim())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.json(&request_json)
.send()
.await?;
let ua_value = effective_user_agent(&relay.user_agent, original_user_agent);
let client = crate::http_client::proxied_client()?;
let mut request = client
.post(chat_completions_url(&relay.base_url))
.bearer_auth(relay.api_key.trim())
.header(reqwest::header::CONTENT_TYPE, "application/json");
if !ua_value.is_empty() {
request = request.header("User-Agent", &ua_value);
}
let upstream = request.json(&request_json).send().await?;
let status_code = upstream.status().as_u16();
let content_type = upstream
.headers()
Expand Down
4 changes: 3 additions & 1 deletion crates/codex-plus-core/src/relay_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ pub async fn test_relay_profile(
anyhow::bail!("API Key 不能为空");
}

let client = crate::http_client::proxied_client("CodexPlusPlus/RelayTest")?;
let client = crate::http_client::proxied_client()?;
let endpoint = match profile.protocol {
RelayProtocol::Responses => format!("{base_url}/responses"),
RelayProtocol::ChatCompletions => format!("{base_url}/chat/completions"),
Expand All @@ -522,6 +522,7 @@ pub async fn test_relay_profile(
.post(&endpoint)
.bearer_auth(api_key)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header("User-Agent", "CodexPlusPlus/RelayTest")
.json(&payload)
.send()
.await?;
Comment on lines 524 to 528
Expand All @@ -540,6 +541,7 @@ pub async fn test_relay_profile(
.post(&v1_endpoint)
.bearer_auth(api_key)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header("User-Agent", "CodexPlusPlus/RelayTest")
.json(&payload)
.send()
.await?;
Expand Down
4 changes: 3 additions & 1 deletion crates/codex-plus-core/src/stepwise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ pub async fn generate(
}));
}

let client = crate::http_client::proxied_client("")?;
let client = crate::http_client::proxied_client()?;
let ua = format!("CodexPlusPlus/{}", env!("CARGO_PKG_VERSION"));
let timeout = Duration::from_millis(settings.codex_app_stepwise_timeout_ms);
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
Expand All @@ -186,6 +187,7 @@ pub async fn generate(
let response = client
.post(format!("{base_url}/chat/completions"))
.headers(headers)
.header("User-Agent", &ua)
.timeout(timeout)
.json(&json!({
"model": model,
Expand Down
22 changes: 12 additions & 10 deletions crates/codex-plus-core/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,12 @@ pub fn select_update_asset(assets: &[(String, String)]) -> Option<ReleaseAsset>
}

pub async fn fetch_latest_release(latest_json_url: &str) -> anyhow::Result<Release> {
let client =
crate::http_client::proxied_client(&format!("Codex++/{}", crate::version::VERSION))?;
let client = crate::http_client::proxied_client()?;
let ua = format!("Codex++/{}", crate::version::VERSION);
let payload = client
.get(latest_json_url)
.header(reqwest::header::ACCEPT, "application/json")
.header("User-Agent", &ua)
.send()
.await?
.error_for_status()?
Expand Down Expand Up @@ -201,14 +202,15 @@ pub async fn perform_update(
.asset_url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("没有可下载的 Release asset"))?;
let bytes =
crate::http_client::proxied_client(&format!("Codex++/{}", crate::version::VERSION))?
.get(url)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
let ua = format!("Codex++/{}", crate::version::VERSION);
let bytes = crate::http_client::proxied_client()?
.get(url)
.header("User-Agent", &ua)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
let installer_path = download_asset_to(release, &bytes, download_dir)?;
launch_installer(&installer_path)?;
Ok(UpdateInstall {
Expand Down
5 changes: 5 additions & 0 deletions crates/codex-plus-core/tests/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ experimental_bearer_token = "relay-key"
temp.path(),
&HashMap::new(),
reqwest::Client::builder().no_proxy().build().unwrap(),
"",
)
.await;

Expand Down Expand Up @@ -146,6 +147,7 @@ base_url = "{}/v1"
temp.path(),
&HashMap::new(),
reqwest::Client::builder().no_proxy().build().unwrap(),
"",
)
.await;

Expand Down Expand Up @@ -203,6 +205,7 @@ experimental_bearer_token = "relay-key"
temp.path(),
&HashMap::new(),
reqwest::Client::builder().no_proxy().build().unwrap(),
"",
)
.await;

Expand Down Expand Up @@ -255,6 +258,7 @@ model_catalog_json = '{}'
temp.path(),
&HashMap::new(),
reqwest::Client::builder().no_proxy().build().unwrap(),
"",
)
.await;

Expand Down Expand Up @@ -291,6 +295,7 @@ base_url = "{}"
temp.path(),
&HashMap::new(),
reqwest::Client::builder().no_proxy().build().unwrap(),
"",
)
.await;

Expand Down
2 changes: 2 additions & 0 deletions crates/codex-plus-core/tests/protocol_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,8 @@ fn aggregate_proxy_settings(
..BackendSettings::default()
}
}
/// Verify the configured user-agent takes precedence and empty UA fallbacks to reqwest default.
///
#[tokio::test]
async fn chat_completions_proxy_uses_configured_user_agent() {
let _lock = settings_path_test_lock().lock().unwrap();
Expand Down
Loading