Skip to content
This repository was archived by the owner on Oct 8, 2026. It is now read-only.
Merged
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ let client = tinyjevclient::Client::new(config)?;
For a Tiny Humans API key, use `ClientConfig::tinyhumans_openrouter("<key>")`,
which explicitly targets
`https://api.tinyhumans.ai/agent-integrations/openrouter/systemone`.
Hosts can call `.with_sdk_name("openhuman")` to attach sanitized `x-sdk-name`
product attribution. The header is sent only to that exact Tiny Humans HTTPS
endpoint, even when an endpoint override is configured.

Remote API roots must use HTTPS; HTTP is reserved for literal loopback IPs.
Failed evaluations retain their classified error, attempt count, and elapsed
Expand Down
3 changes: 3 additions & 0 deletions crates/tinyjevclient/src/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ are terminal, and automatic redirects are disabled.

`ClientConfig::tinyhumans_openrouter` targets the Tiny Humans OpenRouter proxy
at `https://api.tinyhumans.ai/agent-integrations/openrouter/systemone`.
`ClientConfig::with_sdk_name` sanitizes product attribution and sends
`x-sdk-name` only to this exact HTTPS endpoint. OpenRouter, TypeSafe, and
other endpoint overrides do not receive it.
47 changes: 36 additions & 11 deletions crates/tinyjevclient/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,8 @@ impl Client {
&self,
request: &EvaluationRequest,
) -> std::result::Result<(EvaluationResponse, Option<String>), Failure> {
let url = self.config.endpoint_url.clone().unwrap_or_else(|| {
format!(
"{}/{}",
self.config.base_url.trim_end_matches('/'),
self.config.system_one_path
)
});
let response = self
.http
.post(url)
.bearer_auth(self.config.api_key.expose())
.json(request)
.evaluation_request(request)
.send()
.await
.map_err(classify_transport)?;
Expand All @@ -140,6 +130,27 @@ impl Client {
Ok((decoded, request_id))
}

fn evaluation_request(&self, request: &EvaluationRequest) -> reqwest::RequestBuilder {
let url = self.config.endpoint_url.clone().unwrap_or_else(|| {
format!(
"{}/{}",
self.config.base_url.trim_end_matches('/'),
self.config.system_one_path
)
});
let mut builder = self
.http
.post(&url)
.bearer_auth(self.config.api_key.expose())
.json(request);
if let Some(name) = self.config.sdk_name.as_deref()
&& is_tinyhumans_proxy_endpoint(&url)
{
builder = builder.header("x-sdk-name", name);
}
builder
}

fn validate_response(
&self,
response: &EvaluationResponse,
Expand All @@ -152,6 +163,20 @@ impl Client {
}
}

fn is_tinyhumans_proxy_endpoint(raw: &str) -> bool {
let Ok(url) = reqwest::Url::parse(raw) else {
return false;
};
url.scheme() == "https"
&& url.host_str() == Some("api.tinyhumans.ai")
&& url.port_or_known_default() == Some(443)
&& url.username().is_empty()
&& url.password().is_none()
&& url.path() == "/agent-integrations/openrouter/systemone"
&& url.query().is_none()
&& url.fragment().is_none()
}

impl ClientConfig {
fn validate(&self) -> Result<()> {
if self.api_key.expose().trim().is_empty() {
Expand Down
47 changes: 47 additions & 0 deletions crates/tinyjevclient/src/client/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,53 @@ async fn tinyhumans_proxy_uses_the_direct_system_one_path() {
);
}

#[test]
fn sdk_name_is_sanitized_and_sent_only_to_the_exact_tinyhumans_proxy() {
let config = ClientConfig::tinyhumans_openrouter("key")
.with_sdk_name(" OpenCompany\r\nInjected: yes / test ");
let client = Client::new(config.clone()).unwrap();
let outgoing = client.evaluation_request(&request()).build().unwrap();
assert_eq!(
outgoing.headers().get("x-sdk-name").unwrap(),
"opencompanyinjectedyestest"
);

let direct = Client::new(ClientConfig::openrouter("key").with_sdk_name("openhuman")).unwrap();
assert!(
direct
.evaluation_request(&request())
.build()
.unwrap()
.headers()
.get("x-sdk-name")
.is_none()
);

for endpoint in [
"https://api.tinyhumans.ai.evil.example/agent-integrations/openrouter/systemone",
"https://api.tinyhumans.ai:444/agent-integrations/openrouter/systemone",
"https://example.com/agent-integrations/openrouter/systemone",
] {
let other = Client::new(config.clone().with_endpoint_url(endpoint)).unwrap();
assert!(
other
.evaluation_request(&request())
.build()
.unwrap()
.headers()
.get("x-sdk-name")
.is_none(),
"must not attribute {endpoint}"
);
}
assert!(
Client::new(config.with_endpoint_url(
"https://api.tinyhumans.ai/agent-integrations/openrouter/systemone?redirect=1"
))
.is_err()
);
}

#[tokio::test]
async fn retries_rate_limits_and_reports_attempts() {
let (base_url, requests) =
Expand Down
24 changes: 24 additions & 0 deletions crates/tinyjevclient/src/client/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub struct ClientConfig {
pub timeout: Duration,
/// Transient failure retry policy.
pub retry: RetryPolicy,
/// Sanitized product attribution for the exact `TinyHumans` proxy endpoint.
pub(super) sdk_name: Option<String>,
}

impl ClientConfig {
Expand All @@ -62,6 +64,7 @@ impl ClientConfig {
provider: Provider::TypeSafe,
timeout: Duration::from_secs(30),
retry: RetryPolicy::default(),
sdk_name: None,
}
}

Expand All @@ -76,6 +79,7 @@ impl ClientConfig {
provider: Provider::OpenRouter,
timeout: Duration::from_secs(30),
retry: RetryPolicy::default(),
sdk_name: None,
}
}

Expand All @@ -93,6 +97,7 @@ impl ClientConfig {
provider: Provider::OpenRouter,
timeout: Duration::from_secs(30),
retry: RetryPolicy::default(),
sdk_name: None,
}
}

Expand All @@ -111,6 +116,24 @@ impl ClientConfig {
self
}

/// Attribute `TinyHumans` proxy requests to a product. Invalid characters
/// are dropped, the value is lowercased and capped at 64 ASCII bytes.
/// Other endpoints never receive this header.
#[must_use]
pub fn with_sdk_name(mut self, raw: &str) -> Self {
let name = raw
.trim()
.chars()
.filter(|character| {
character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-')
})
.take(64)
.map(|character| character.to_ascii_lowercase())
.collect::<String>();
self.sdk_name = (!name.is_empty()).then_some(name);
self
}

/// Returns the exact endpoint override, when configured.
#[must_use]
pub fn endpoint_url(&self) -> Option<&str> {
Expand All @@ -128,6 +151,7 @@ impl fmt::Debug for ClientConfig {
.field("provider", &self.provider)
.field("timeout", &self.timeout)
.field("retry", &self.retry)
.field("sdk_name", &self.sdk_name)
.finish()
}
}
Expand Down
Loading