Skip to content

Commit b343420

Browse files
feat(openrouter): add with_app_identity and with_app_categories builders for app attribution (0xPlaygrounds#1806)
* feat(openrouter): add with_app_identity builder for X-Title and HTTP-Referer headers OpenRouter uses the X-Title and HTTP-Referer request headers to identify the calling application in the dashboard activity feed and public rankings page. Without them, all requests appear under the API key with no named app. Adds `ClientBuilder::with_app_identity(title, url)` that sets both headers on every request made by the client. The method is callable at any point in the builder chain (before or after api_key). Invalid header values (non-ASCII characters) are silently skipped rather than returning an error, matching the behaviour of header helpers elsewhere in the codebase. Three tests: headers present after with_app_identity, headers absent without it, and the existing client-initialization smoke-test. * fix(openrouter): use X-OpenRouter-Title header; add with_app_categories
1 parent 25987b4 commit b343420

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

  • crates/rig-core/src/providers/openrouter

crates/rig-core/src/providers/openrouter/client.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::{
88
completion::GetTokenUsage,
99
http_client,
1010
};
11+
use http::HeaderValue;
1112
use serde::{Deserialize, Serialize};
1213
use std::fmt::Debug;
1314

@@ -150,6 +151,48 @@ impl GetTokenUsage for Usage {
150151
})
151152
}
152153
}
154+
impl<ApiKey, H> client::ClientBuilder<OpenRouterExtBuilder, ApiKey, H> {
155+
/// Attach OpenRouter app-identification headers (`X-OpenRouter-Title` and `HTTP-Referer`)
156+
/// to every request made by this client. `title` appears in the dashboard activity feed
157+
/// and rankings page; `url` is the primary app identifier required to create an app page
158+
/// on OpenRouter. Invalid (non-ASCII) values are silently skipped.
159+
pub fn with_app_identity(mut self, title: impl AsRef<str>, url: impl AsRef<str>) -> Self {
160+
if let Ok(val) = HeaderValue::from_str(title.as_ref()) {
161+
self.headers_mut().insert(
162+
http::header::HeaderName::from_static("x-openrouter-title"),
163+
val,
164+
);
165+
}
166+
if let Ok(val) = HeaderValue::from_str(url.as_ref()) {
167+
self.headers_mut()
168+
.insert(http::header::HeaderName::from_static("http-referer"), val);
169+
}
170+
self
171+
}
172+
173+
/// Assign this app to one or more OpenRouter marketplace categories via the
174+
/// `X-OpenRouter-Categories` header. Categories must be lowercase and hyphen-separated
175+
/// (e.g. `"cli-agent"`, `"ide-extension"`). Unrecognized categories are silently ignored
176+
/// by OpenRouter. Invalid (non-ASCII) values are silently skipped.
177+
pub fn with_app_categories<S>(mut self, categories: &[S]) -> Self
178+
where
179+
S: AsRef<str>,
180+
{
181+
let joined = categories
182+
.iter()
183+
.map(|c| c.as_ref())
184+
.collect::<Vec<_>>()
185+
.join(",");
186+
if let Ok(val) = HeaderValue::from_str(&joined) {
187+
self.headers_mut().insert(
188+
http::header::HeaderName::from_static("x-openrouter-categories"),
189+
val,
190+
);
191+
}
192+
self
193+
}
194+
}
195+
153196
#[cfg(test)]
154197
mod tests {
155198
#[test]
@@ -161,4 +204,64 @@ mod tests {
161204
.build()
162205
.expect("Client::builder() failed");
163206
}
207+
208+
#[test]
209+
fn test_with_app_identity_sets_headers() {
210+
let client = crate::providers::openrouter::Client::builder()
211+
.with_app_identity("My App", "https://myapp.example.com")
212+
.api_key("dummy-key")
213+
.build()
214+
.expect("Client::builder() failed");
215+
216+
let headers = client.headers();
217+
assert_eq!(
218+
headers
219+
.get("x-openrouter-title")
220+
.and_then(|v| v.to_str().ok()),
221+
Some("My App"),
222+
);
223+
assert_eq!(
224+
headers.get("http-referer").and_then(|v| v.to_str().ok()),
225+
Some("https://myapp.example.com"),
226+
);
227+
}
228+
229+
#[test]
230+
fn test_without_app_identity_no_extra_headers() {
231+
let client = crate::providers::openrouter::Client::builder()
232+
.api_key("dummy-key")
233+
.build()
234+
.expect("Client::builder() failed");
235+
236+
let headers = client.headers();
237+
assert!(headers.get("x-openrouter-title").is_none());
238+
assert!(headers.get("http-referer").is_none());
239+
}
240+
241+
#[test]
242+
fn test_with_app_categories_sets_header() {
243+
let client = crate::providers::openrouter::Client::builder()
244+
.with_app_categories(&["cli-agent", "ide-extension"])
245+
.api_key("dummy-key")
246+
.build()
247+
.expect("Client::builder() failed");
248+
249+
assert_eq!(
250+
client
251+
.headers()
252+
.get("x-openrouter-categories")
253+
.and_then(|v| v.to_str().ok()),
254+
Some("cli-agent,ide-extension"),
255+
);
256+
}
257+
258+
#[test]
259+
fn test_without_app_categories_no_header() {
260+
let client = crate::providers::openrouter::Client::builder()
261+
.api_key("dummy-key")
262+
.build()
263+
.expect("Client::builder() failed");
264+
265+
assert!(client.headers().get("x-openrouter-categories").is_none());
266+
}
164267
}

0 commit comments

Comments
 (0)