From 342dcceb24646563d000703887eb56875e3f4294 Mon Sep 17 00:00:00 2001
From: ScriptSmith <ScriptSmith@users.noreply.github.com>
Date: Sun, 22 Mar 2026 14:56:36 +1000
Subject: [PATCH 1/2] Add optional OpenRouter attribution

---
 docs/content/docs/configuration/providers.mdx | 11 +++++++----
 src/bin/record_fixtures.rs                    |  4 ++--
 src/config/providers.rs                       |  3 ++-
 src/providers/open_ai/mod.rs                  | 18 ++++++++++++++++--
 4 files changed, 27 insertions(+), 9 deletions(-)

diff --git a/docs/content/docs/configuration/providers.mdx b/docs/content/docs/configuration/providers.mdx
index 59f70302..7c3ecdfa 100644
--- a/docs/content/docs/configuration/providers.mdx
+++ b/docs/content/docs/configuration/providers.mdx
@@ -45,10 +45,13 @@ type = "open_ai"
 api_key = "${OPENROUTER_API_KEY}"
 base_url = "https://openrouter.ai/api/v1"
 
-# OpenRouter-specific headers
-[providers.openrouter.headers]
-HTTP-Referer = "https://myapp.example.com"
-X-Title = "My Application"
+# App attribution headers are sent automatically:
+#   HTTP-Referer: https://hadriangateway.com
+#   X-OpenRouter-Title: Hadrian Gateway
+# Override to customize, or set to "" to opt out:
+# [providers.openrouter.headers]
+# HTTP-Referer = "https://myapp.example.com"
+# X-OpenRouter-Title = "My Application"
 ```
 
 **Ollama** (local, no API key needed):
diff --git a/src/bin/record_fixtures.rs b/src/bin/record_fixtures.rs
index a61a9bb9..bf2c130d 100644
--- a/src/bin/record_fixtures.rs
+++ b/src/bin/record_fixtures.rs
@@ -1919,8 +1919,8 @@ async fn record_fixture(
             // Add OpenRouter-specific headers
             if def.provider == "openrouter" {
                 request = request
-                    .header("HTTP-Referer", "https://github.com/ScriptSmith/hadrian")
-                    .header("X-Title", "Hadrian Gateway Fixture Recording");
+                    .header("HTTP-Referer", "https://hadriangateway.com")
+                    .header("X-OpenRouter-Title", "Hadrian Gateway");
             }
         }
 
diff --git a/src/config/providers.rs b/src/config/providers.rs
index 6f835d28..9c11b275 100644
--- a/src/config/providers.rs
+++ b/src/config/providers.rs
@@ -606,7 +606,8 @@ pub struct OpenAiProviderConfig {
     pub model_aliases: HashMap<String, String>,
 
     /// Custom headers to include in requests.
-    /// Useful for provider-specific headers like OpenRouter's HTTP-Referer.
+    /// For OpenRouter providers, `HTTP-Referer` and `X-OpenRouter-Title` are set
+    /// automatically for app attribution. Override here to customize or opt out.
     #[serde(default)]
     pub headers: HashMap<String, String>,
 
diff --git a/src/providers/open_ai/mod.rs b/src/providers/open_ai/mod.rs
index 3a519145..e0e7fa31 100644
--- a/src/providers/open_ai/mod.rs
+++ b/src/providers/open_ai/mod.rs
@@ -66,11 +66,25 @@ impl OpenAICompatibleProvider {
         registry: &CircuitBreakerRegistry,
     ) -> Self {
         let circuit_breaker = registry.get_or_create(provider_name, &config.circuit_breaker);
+        let base_url = config.base_url.trim_end_matches('/').to_string();
+
+        let mut headers = config.headers.clone();
+
+        // OpenRouter app attribution: send Hadrian metadata by default unless
+        // the user has explicitly set these headers (opt-out by overriding).
+        if base_url.contains("openrouter.ai") {
+            headers
+                .entry("HTTP-Referer".to_string())
+                .or_insert_with(|| "https://hadriangateway.com".to_string());
+            headers
+                .entry("X-OpenRouter-Title".to_string())
+                .or_insert_with(|| "Hadrian Gateway".to_string());
+        }
 
         Self {
             api_key: config.api_key.clone(),
-            base_url: config.base_url.trim_end_matches('/').to_string(),
-            headers: config.headers.clone(),
+            base_url,
+            headers,
             timeout: Duration::from_secs(config.timeout_secs),
             retry: config.retry.clone(),
             circuit_breaker_config: config.circuit_breaker.clone(),

From 1c57758cb1f392982aa81d12b408775fa9dff796 Mon Sep 17 00:00:00 2001
From: ScriptSmith <ScriptSmith@users.noreply.github.com>
Date: Sun, 22 Mar 2026 15:08:11 +1000
Subject: [PATCH 2/2] Review fixes

---
 src/providers/open_ai/mod.rs | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/src/providers/open_ai/mod.rs b/src/providers/open_ai/mod.rs
index e0e7fa31..0697c96a 100644
--- a/src/providers/open_ai/mod.rs
+++ b/src/providers/open_ai/mod.rs
@@ -100,9 +100,13 @@ impl OpenAICompatibleProvider {
             request
         };
 
-        let request = self.headers.iter().fold(request, |req, (key, value)| {
-            req.header(key.as_str(), value.as_str())
-        });
+        let request = self
+            .headers
+            .iter()
+            .filter(|(_, value)| !value.is_empty())
+            .fold(request, |req, (key, value)| {
+                req.header(key.as_str(), value.as_str())
+            });
 
         request.timeout(self.timeout)
     }
@@ -122,9 +126,13 @@ impl OpenAICompatibleProvider {
             request
         };
 
-        let request = self.headers.iter().fold(request, |req, (key, value)| {
-            req.header(key.as_str(), value.as_str())
-        });
+        let request = self
+            .headers
+            .iter()
+            .filter(|(_, value)| !value.is_empty())
+            .fold(request, |req, (key, value)| {
+                req.header(key.as_str(), value.as_str())
+            });
 
         request.timeout(self.timeout)
     }
