Clean up transport fingerprint configuration

Remove legacy tls_profile handling, keep header fingerprint under transport profiles, and drop the duplicate auth_modules migration.
This commit is contained in:
fawney19
2026-05-06 13:54:33 +08:00
parent 6fbb867f5f
commit 68216bf868
53 changed files with 267 additions and 238 deletions

View File

@@ -1,3 +1,6 @@
use aether_contracts::{
TRANSPORT_BACKEND_REQWEST_RUSTLS, TRANSPORT_HTTP_MODE_AUTO, TRANSPORT_POOL_SCOPE_KEY,
};
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use uuid::Uuid;
@@ -53,6 +56,7 @@ const STAINLESS_PACKAGE_VERSIONS: &[&str] = &["0.68.0", "0.69.0", "0.70.0", "0.7
const NODE_VERSIONS: &[&str] = &["v20.18.1", "v22.12.0", "v22.14.0", "v24.13.0"];
const ELECTRON_VERSIONS: &[&str] = &["35.5.1", "36.7.1", "37.3.0", "38.7.0", "39.2.3"];
const STAINLESS_TIMEOUTS: &[&str] = &["600", "900"];
const CLAUDE_CODE_TRANSPORT_PROFILE_ID: &str = "claude_code_nodejs";
/// Deterministic hash-based index picker, compatible with Python implementation.
/// Each `slot` produces a different selection from the same seed.
@@ -122,9 +126,12 @@ fn resolve_platform_token(os: &str, arch: &str) -> &'static str {
}
}
/// Generate a complete fingerprint JSON from a seed (deterministic).
/// Compatible with the Python `generate_fingerprint(seed=...)` output.
/// Generate a complete Claude Code transport fingerprint from a seed.
pub fn generate_fingerprint(seed: &str) -> Value {
wrap_header_fingerprint(generate_header_fingerprint(seed))
}
fn generate_header_fingerprint(seed: &str) -> Value {
let picker = SeededPicker::new(seed);
let impersonate =
@@ -163,6 +170,26 @@ pub fn generate_fingerprint(seed: &str) -> Value {
})
}
fn wrap_header_fingerprint(header_fingerprint: Value) -> Value {
serde_json::json!({
"transport_profile": {
"profile_id": CLAUDE_CODE_TRANSPORT_PROFILE_ID,
"backend": TRANSPORT_BACKEND_REQWEST_RUSTLS,
"http_mode": TRANSPORT_HTTP_MODE_AUTO,
"pool_scope": TRANSPORT_POOL_SCOPE_KEY,
"header_fingerprint": header_fingerprint,
}
})
}
pub fn header_fingerprint_from_fingerprint(fingerprint: &Value) -> Option<&Map<String, Value>> {
fingerprint
.get("transport_profile")
.and_then(Value::as_object)
.and_then(|profile| profile.get("header_fingerprint"))
.and_then(Value::as_object)
}
/// Generate a random (non-deterministic) fingerprint.
pub fn generate_random_fingerprint() -> Value {
let random_seed = Uuid::new_v4().to_string();
@@ -172,20 +199,16 @@ pub fn generate_random_fingerprint() -> Value {
/// Sanitize an existing fingerprint JSON, filling missing fields with
/// deterministic fallbacks derived from `key_id`.
pub fn sanitize_fingerprint(raw: &Value, key_id: &str) -> Value {
let generated = generate_fingerprint(key_id);
let generated = generate_header_fingerprint(key_id);
let gen_map = generated.as_object().unwrap();
let raw_map = match raw.as_object() {
Some(m) => m,
None => return generated,
};
let raw_map = header_fingerprint_from_fingerprint(raw);
let mut out = Map::new();
// Start with generated values, then overlay non-empty raw values
for (key, gen_value) in gen_map {
let value = raw_map
.get(key)
.and_then(|raw_map| raw_map.get(key))
.and_then(Value::as_str)
.map(str::trim)
.filter(|v| !v.is_empty())
@@ -244,7 +267,7 @@ pub fn sanitize_fingerprint(raw: &Value, key_id: &str) -> Value {
);
}
Value::Object(out)
wrap_header_fingerprint(Value::Object(out))
}
#[cfg(test)]
@@ -283,7 +306,7 @@ mod tests {
"platform_info",
"user_agent",
];
let map = fp.as_object().unwrap();
let map = header_fingerprint_from_fingerprint(&fp).unwrap();
for key in expected_keys {
assert!(map.contains_key(key), "missing field: {key}");
let value = map[key].as_str().unwrap();
@@ -294,13 +317,18 @@ mod tests {
#[test]
fn sanitize_preserves_user_overrides() {
let raw = serde_json::json!({
"stainless_os": "MacOS",
"stainless_arch": "arm64",
"stainless_timeout": "900",
"user_agent": "Custom-Agent/1.0",
"transport_profile": {
"profile_id": "claude_code_nodejs",
"header_fingerprint": {
"stainless_os": "MacOS",
"stainless_arch": "arm64",
"stainless_timeout": "900",
"user_agent": "Custom-Agent/1.0"
}
}
});
let sanitized = sanitize_fingerprint(&raw, "test-key");
let map = sanitized.as_object().unwrap();
let map = header_fingerprint_from_fingerprint(&sanitized).unwrap();
assert_eq!(map["stainless_os"].as_str(), Some("MacOS"));
assert_eq!(map["stainless_arch"].as_str(), Some("arm64"));
assert_eq!(map["stainless_timeout"].as_str(), Some("900"));
@@ -314,9 +342,9 @@ mod tests {
fn sanitize_fills_missing_fields_from_seed() {
let raw = serde_json::json!({});
let sanitized = sanitize_fingerprint(&raw, "test-key");
let generated = generate_fingerprint("test-key");
let generated = generate_header_fingerprint("test-key");
// All fields should match generated since raw is empty
let s = sanitized.as_object().unwrap();
let s = header_fingerprint_from_fingerprint(&sanitized).unwrap();
let g = generated.as_object().unwrap();
for key in g.keys() {
assert!(s.contains_key(key), "sanitized missing key: {key}");
@@ -330,10 +358,17 @@ mod tests {
#[test]
fn sanitize_normalizes_unknown_impersonate_profile() {
let raw = serde_json::json!({
"impersonate": "firefox99",
"transport_profile": {
"profile_id": "claude_code_nodejs",
"header_fingerprint": {
"impersonate": "firefox99"
}
}
});
let sanitized = sanitize_fingerprint(&raw, "test-key");
let profile = sanitized["impersonate"].as_str().unwrap();
let profile = header_fingerprint_from_fingerprint(&sanitized).unwrap()["impersonate"]
.as_str()
.unwrap();
assert!(
CHROME_IMPERSONATE_PROFILES
.iter()

View File

@@ -2,7 +2,7 @@ use super::super::snapshot::GatewayProviderTransportSnapshot;
use super::super::{
body_rules_are_locally_supported, header_rules_are_locally_supported,
resolve_transport_profile, supports_local_oauth_request_auth_resolution,
transport_proxy_is_locally_supported,
transport_profile_is_configured, transport_proxy_is_locally_supported,
};
use super::auth::supports_local_claude_code_auth;
@@ -52,8 +52,9 @@ pub fn local_claude_code_transport_unsupported_reason_with_network(
if !transport_proxy_is_locally_supported(transport) {
return Some("transport_proxy_unsupported");
}
if transport.key.fingerprint.is_some() && resolve_transport_profile(transport).is_none() {
return Some("transport_tls_profile_unsupported");
if transport_profile_is_configured(transport) && resolve_transport_profile(transport).is_none()
{
return Some("transport_profile_unsupported");
}
None
@@ -128,7 +129,7 @@ mod tests {
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: Some(json!({"tls_profile":"chrome_136"})),
fingerprint: Some(json!({"transport_profile":"chrome_136"})),
decrypted_api_key: "sk-ant-123".to_string(),
decrypted_auth_config: None,
},
@@ -136,7 +137,7 @@ mod tests {
}
#[test]
fn supports_claude_code_transport_when_auth_and_tls_are_valid() {
fn supports_claude_code_transport_when_auth_and_profile_are_valid() {
assert!(supports_local_claude_code_transport_with_network(
&sample_transport(),
"claude:messages"

View File

@@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
use serde_json::{Map, Value};
use super::super::auth::build_openai_passthrough_headers;
use super::fingerprint::header_fingerprint_from_fingerprint;
const DEFAULT_ANTHROPIC_VERSION: &str = "2023-06-01";
const DEFAULT_ACCEPT: &str = "application/json";
@@ -92,8 +93,8 @@ pub fn build_claude_code_passthrough_headers(
out.remove("x-stainless-helper-method");
}
// Override from fingerprint: all mapped fields are applied uniformly.
if let Some(fp) = fingerprint.and_then(Value::as_object) {
// Override from the formal transport profile header fingerprint.
if let Some(fp) = fingerprint.and_then(header_fingerprint_from_fingerprint) {
for &(fp_key, header_key) in FINGERPRINT_HEADER_MAP {
if let Some(value) = fp
.get(fp_key)
@@ -225,7 +226,7 @@ mod tests {
use std::collections::BTreeMap;
#[test]
fn claude_code_headers_merge_required_betas_and_stream_helper() {
fn claude_code_headers_use_transport_profile_header_fingerprint_and_merge_required_betas() {
let mut headers = http::HeaderMap::new();
headers.insert(
"anthropic-beta",
@@ -242,10 +243,15 @@ mod tests {
&BTreeMap::new(),
true,
Some(&json!({
"user_agent":"Claude-Code/9.9",
"stainless_package_version":"1.0.5",
"stainless_runtime_version":"v22.12.0",
"stainless_timeout":"900"
"transport_profile": {
"profile_id": "claude_code_nodejs",
"header_fingerprint": {
"user_agent":"Claude-Code/9.9",
"stainless_package_version":"1.0.5",
"stainless_runtime_version":"v22.12.0",
"stainless_timeout":"900"
}
}
})),
);