mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
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:
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user