fix(provider): 修复 Windsurf 原生工具桥接

This commit is contained in:
Entropy.Xu
2026-05-21 01:02:02 +08:00
parent 82637ad882
commit 129c7c90c0
30 changed files with 9090 additions and 194 deletions
@@ -5,8 +5,11 @@ use super::local_monitoring_response;
use crate::data::GatewayDataState;
use crate::AppState;
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data_contracts::repository::candidates::RequestCandidateStatus;
use aether_data_contracts::repository::{
candidates::RequestCandidateStatus, usage::UsageBodyCaptureState,
};
use axum::body::to_bytes;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use serde_json::json;
use std::sync::Arc;
@@ -530,6 +533,89 @@ async fn admin_monitoring_trace_request_exposes_failed_candidate_upstream_respon
assert!(extra.get("provider_response").is_none());
}
#[tokio::test]
async fn admin_monitoring_trace_request_decodes_connect_json_response_body_refs() {
let mut candidate = sample_candidate(
"cand-used",
"request-connect",
0,
RequestCandidateStatus::Failed,
Some(101),
Some(33),
Some(429),
);
candidate.extra_data = Some(json!({"cache_1h": true}));
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![candidate]));
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider()],
vec![sample_endpoint()],
vec![sample_key()],
));
let mut usage = sample_usage(
"request-connect",
"provider-1",
"Windsurf",
0,
0.0,
"failed",
Some(429),
100,
);
usage.candidate_id = Some("cand-used".to_string());
usage.response_headers = Some(json!({
"content-type": "application/connect+json"
}));
let mut framed = Vec::new();
framed.push(2);
let payload = br#"{"error":{"code":"resource_exhausted","message":"quota exhausted"}}"#;
framed.extend_from_slice(&(payload.len() as u32).to_be_bytes());
framed.extend_from_slice(payload);
usage.response_body = Some(json!(BASE64_STANDARD.encode(framed)));
usage.response_body_ref = Some("usage://request/request-connect/response_body".to_string());
usage.response_body_state = Some(UsageBodyCaptureState::Inline);
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![usage]));
let data_state =
crate::data::GatewayDataState::with_request_candidate_and_usage_repository_for_tests(
request_candidates,
usage_repository,
)
.with_provider_catalog_reader(provider_catalog);
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(data_state);
let context = request_context(
http::Method::GET,
"/api/admin/monitoring/trace/request-connect",
);
let response = local_monitoring_response(&state, &context)
.await
.expect("handler should not error")
.expect("route should be handled locally");
assert_eq!(response.status(), http::StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body should read");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
let upstream_response = &payload["candidates"][0]["extra_data"]["upstream_response"];
assert_eq!(upstream_response["status_code"], json!(429));
assert_eq!(
upstream_response["body"]["error"]["code"],
json!("resource_exhausted")
);
assert_eq!(
upstream_response["body"]["error"]["message"],
json!("quota exhausted")
);
assert_eq!(
upstream_response["body_ref"],
json!("usage://request/request-connect/response_body")
);
assert_eq!(upstream_response["body_state"], json!("inline"));
}
#[tokio::test]
async fn admin_monitoring_trace_request_exposes_structured_ranking_metadata() {
let mut candidate = sample_candidate(
@@ -3158,17 +3158,22 @@ async fn provider_query_execute_windsurf_test_candidate(
}
let incoming_request_headers = provider_query_extract_request_headers(payload);
let mut request_body = original_request_body.clone();
if let Some(object) = request_body.as_object_mut() {
object.insert("stream".to_string(), Value::Bool(false));
}
let request_body = original_request_body.clone();
let request_model =
provider_query_request_body_model(&request_body, &candidate.effective_model);
let upstream_is_stream = provider_query_resolve_standard_test_upstream_is_stream(
transport.endpoint.config.as_ref(),
let client_is_stream = request_body
.get("stream")
.and_then(Value::as_bool)
.unwrap_or(false);
let hard_requires_streaming = crate::ai_serving::force_upstream_streaming_for_provider(
transport.provider.provider_type.as_str(),
candidate.endpoint.api_format.as_str(),
);
let upstream_is_stream = crate::ai_serving::resolve_upstream_is_stream_from_endpoint_config(
transport.endpoint.config.as_ref(),
client_is_stream,
hard_requires_streaming,
);
let Some((auth_header, auth_value)) =
crate::provider_transport::windsurf::resolve_windsurf_cascade_auth(&transport).or_else(
|| crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport),
@@ -42,9 +42,9 @@ pub(super) fn provider_query_test_attempt_payload(
"status_code": execution.status_code,
"latency_ms": execution.latency_ms,
"request_url": execution.request_url,
"request_headers": provider_query_redact_diagnostic_headers(&execution.request_headers),
"request_body": execution.request_body,
"response_headers": provider_query_redact_diagnostic_headers(&execution.response_headers),
"request_headers": redacted_provider_query_headers(&execution.request_headers),
"request_body": redacted_provider_query_value(&execution.request_body),
"response_headers": redacted_provider_query_headers(&execution.response_headers),
"response_body": execution.response_body,
})
}
@@ -172,34 +172,84 @@ fn provider_query_endpoint_route_payload(
})
}
fn provider_query_redact_diagnostic_headers(
headers: &BTreeMap<String, String>,
) -> BTreeMap<String, String> {
fn redacted_provider_query_headers(headers: &BTreeMap<String, String>) -> BTreeMap<String, String> {
headers
.iter()
.map(|(name, value)| {
if provider_query_header_is_sensitive(name) {
(name.clone(), "<redacted>".to_string())
.map(|(key, value)| {
if provider_query_field_is_sensitive(key) {
(key.clone(), "[REDACTED]".to_string())
} else {
(name.clone(), value.clone())
(key.clone(), value.clone())
}
})
.collect()
}
fn provider_query_header_is_sensitive(name: &str) -> bool {
fn redacted_provider_query_value(value: &Value) -> Value {
match value {
Value::Object(object) => Value::Object(
object
.iter()
.map(|(key, value)| {
if provider_query_field_is_sensitive(key) {
(key.clone(), Value::String("[REDACTED]".to_string()))
} else {
(key.clone(), redacted_provider_query_value(value))
}
})
.collect(),
),
Value::Array(items) => Value::Array(
items
.iter()
.map(redacted_provider_query_value)
.collect::<Vec<_>>(),
),
other => other.clone(),
}
}
fn provider_query_field_is_sensitive(key: &str) -> bool {
let key = key.trim().to_ascii_lowercase();
let normalized = key
.chars()
.filter(|ch| ch.is_ascii_alphanumeric())
.collect::<String>();
if matches!(
normalized.as_str(),
"maxtokens"
| "maxoutputtokens"
| "inputtokens"
| "outputtokens"
| "prompttokens"
| "completiontokens"
| "totaltokens"
) {
return false;
}
matches!(
name.trim().to_ascii_lowercase().as_str(),
key.as_str(),
"authorization"
| "proxy-authorization"
| "cookie"
| "set-cookie"
| "x-api-key"
| "api_key"
| "apikey"
| "api-key"
| "x-api-key"
| "x-goog-api-key"
| "anthropic-api-key"
| "openai-api-key"
)
| "x-codeium-csrf-token"
| "access_token"
| "refresh_token"
| "id_token"
| "password"
| "secret"
) || normalized.ends_with("token")
|| normalized.contains("secret")
|| normalized.contains("apikey")
|| normalized.contains("authorization")
}
pub(super) fn provider_query_candidate_summary_payload(
@@ -309,34 +359,93 @@ pub(super) fn provider_query_candidate_summary_payload(
#[cfg(test)]
mod tests {
use super::*;
use super::{redacted_provider_query_headers, redacted_provider_query_value};
use serde_json::json;
use std::collections::BTreeMap;
#[test]
fn provider_query_diagnostic_headers_redact_credentials() {
fn redacts_sensitive_provider_query_headers() {
let headers = BTreeMap::from([
("cookie".to_string(), "sso=secret".to_string()),
("authorization".to_string(), "Bearer secret".to_string()),
(
"authorization".to_string(),
"Bearer secret-token".to_string(),
),
("x-goog-api-key".to_string(), "secret".to_string()),
("content-type".to_string(), "application/json".to_string()),
(
"x-codeium-csrf-token".to_string(),
"csrf-secret".to_string(),
),
]);
let redacted = provider_query_redact_diagnostic_headers(&headers);
let redacted = redacted_provider_query_headers(&headers);
assert_eq!(
redacted.get("cookie").map(String::as_str),
Some("<redacted>")
Some("[REDACTED]")
);
assert_eq!(
redacted.get("authorization").map(String::as_str),
Some("<redacted>")
Some("[REDACTED]")
);
assert_eq!(
redacted.get("x-goog-api-key").map(String::as_str),
Some("<redacted>")
Some("[REDACTED]")
);
assert_eq!(
redacted.get("x-codeium-csrf-token").map(String::as_str),
Some("[REDACTED]")
);
assert_eq!(
redacted.get("content-type").map(String::as_str),
Some("application/json")
);
}
#[test]
fn redacts_sensitive_provider_query_request_body_fields() {
let body = json!({
"metadata": {
"apiKey": "devin-session-token$secret",
"ideName": "windsurf"
},
"messages": [{"role": "user", "content": "hello"}],
"stream": true
});
let redacted = redacted_provider_query_value(&body);
assert_eq!(
redacted.pointer("/metadata/apiKey"),
Some(&json!("[REDACTED]"))
);
assert_eq!(
redacted.pointer("/metadata/ideName"),
Some(&json!("windsurf"))
);
assert_eq!(redacted.pointer("/stream"), Some(&json!(true)));
}
#[test]
fn keeps_non_secret_token_count_fields_visible() {
let body = json!({
"maxTokens": 64,
"usage": {
"inputTokens": 10,
"outputTokens": 2,
"accessToken": "secret"
}
});
let redacted = redacted_provider_query_value(&body);
assert_eq!(redacted.pointer("/maxTokens"), Some(&json!(64)));
assert_eq!(redacted.pointer("/usage/inputTokens"), Some(&json!(10)));
assert_eq!(redacted.pointer("/usage/outputTokens"), Some(&json!(2)));
assert_eq!(
redacted.pointer("/usage/accessToken"),
Some(&json!("[REDACTED]"))
);
}
}