Merge remote-tracking branch 'origin/pr/498'

This commit is contained in:
fawney19
2026-05-21 22:56:43 +08:00
89 changed files with 18396 additions and 382 deletions

View File

@@ -44,11 +44,11 @@ pub(crate) use aether_ai_formats::api::{
build_core_error_body_for_client_format, convert_standard_chat_response,
core_error_background_report_kind, core_error_default_client_api_format,
core_success_background_report_kind, encode_kiro_sse_events,
implicit_sync_finalize_report_kind, is_core_error_finalize_kind,
normalize_provider_private_report_context, normalize_provider_private_response_value,
provider_private_response_allows_sync_finalize, resolve_claude_stream_spec,
resolve_claude_sync_spec, resolve_gemini_stream_spec, resolve_gemini_sync_spec,
resolve_local_image_stream_spec, resolve_local_image_sync_spec,
extract_provider_private_stream_error_body, implicit_sync_finalize_report_kind,
is_core_error_finalize_kind, normalize_provider_private_report_context,
normalize_provider_private_response_value, provider_private_response_allows_sync_finalize,
resolve_claude_stream_spec, resolve_claude_sync_spec, resolve_gemini_stream_spec,
resolve_gemini_sync_spec, resolve_local_image_stream_spec, resolve_local_image_sync_spec,
resolve_local_same_format_stream_spec, resolve_local_same_format_sync_spec,
resolve_openai_embedding_sync_spec, sanitize_request_path_and_query, AiControlPlanRequest,
CanonicalContentPart, CanonicalStreamEvent, CanonicalStreamFrame, ClaudeClientEmitter,

View File

@@ -24,9 +24,13 @@ use crate::ai_serving::transport::kiro::{
use crate::ai_serving::transport::{
build_grok_browser_headers, build_grok_upstream_url, build_kiro_cross_format_upstream_url,
build_openai_image_headers, build_openai_image_upstream_url,
build_standard_provider_request_headers, openai_image_transport_unsupported_reason,
resolve_grok_session_auth, resolve_openai_image_auth, GrokHeaderInput,
ProviderOpenAiImageHeadersInput, StandardProviderRequestHeadersInput, GROK_CHAT_PATH,
build_standard_provider_request_headers, build_windsurf_cascade_headers,
build_windsurf_cascade_request_body, build_windsurf_cascade_upstream_url,
is_windsurf_provider_transport,
local_windsurf_request_transport_unsupported_reason_with_network,
openai_image_transport_unsupported_reason, resolve_grok_session_auth,
resolve_openai_image_auth, GrokHeaderInput, ProviderOpenAiImageHeadersInput,
StandardProviderRequestHeadersInput, GROK_CHAT_PATH, WINDSURF_ENVELOPE_NAME,
};
use crate::ai_serving::{
build_openai_image_request_body_from_gemini_image_request, gemini_request_is_image_generation,
@@ -198,11 +202,18 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
return None;
}
if let Some(skip_reason) = crate::ai_serving::request_pair_transport_unsupported_reason(
transport,
spec_metadata.api_format,
provider_api_format,
) {
let is_windsurf_cascade =
provider_api_format == "openai:chat" && is_windsurf_provider_transport(transport);
let transport_unsupported_reason = if is_windsurf_cascade {
local_windsurf_request_transport_unsupported_reason_with_network(transport)
} else {
crate::ai_serving::request_pair_transport_unsupported_reason(
transport,
spec_metadata.api_format,
provider_api_format,
)
};
if let Some(skip_reason) = transport_unsupported_reason {
mark_skipped_local_standard_candidate(
state,
input,
@@ -321,7 +332,7 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
provider_api_format,
parts.uri.path(),
upstream_is_stream,
if is_kiro_claude_cli {
if is_kiro_claude_cli || is_windsurf_cascade {
None
} else {
transport.endpoint.body_rules.as_ref()
@@ -443,6 +454,24 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
)
.await;
}
if is_windsurf_cascade {
return build_windsurf_cross_format_payload_parts(
state,
parts,
trace_id,
body_json,
input,
attempt,
transport,
provider_api_format,
prepared_candidate.mapped_model,
prepared_candidate.auth_header,
prepared_candidate.auth_value,
provider_request_body,
upstream_is_stream,
)
.await;
}
let upstream_url = match crate::ai_serving::planner::standard::build_standard_upstream_url(
parts,
@@ -542,6 +571,121 @@ fn apply_transport_request_body_semantics(
)
}
#[allow(clippy::too_many_arguments)]
async fn build_windsurf_cross_format_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
original_body_json: &serde_json::Value,
input: &LocalStandardDecisionInput,
attempt: &LocalStandardCandidateAttempt,
transport: &Arc<GatewayProviderTransportSnapshot>,
provider_api_format: &str,
mapped_model: String,
auth_header: String,
auth_value: String,
openai_chat_request_body: Value,
upstream_is_stream: bool,
) -> Option<LocalStandardCandidatePayloadParts> {
let candidate = &attempt.eligible.candidate;
let effective_headers = input.effective_headers(&parts.headers);
let provider_request_body = match build_windsurf_cascade_request_body(
&openai_chat_request_body,
&mapped_model,
&auth_value,
transport.endpoint.body_rules.as_ref(),
Some(effective_headers),
upstream_is_stream,
) {
Some(body) => body,
None => {
mark_skipped_local_standard_candidate_with_extra_data(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"provider_request_body_build_failed",
request_body_build_failure_extra_data(
&openai_chat_request_body,
provider_api_format,
provider_api_format,
),
)
.await;
return None;
}
};
let upstream_url = match build_windsurf_cascade_upstream_url(
transport.endpoint.base_url.as_str(),
parts.uri.query(),
) {
Some(url) => url,
None => {
mark_skipped_local_standard_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"upstream_url_missing",
CandidateFailureDiagnostic::upstream_url_missing(
provider_api_format,
provider_api_format,
"standard_family_windsurf_url",
),
)
.await;
return None;
}
};
let provider_request_headers = match build_windsurf_cascade_headers(
effective_headers,
&provider_request_body,
original_body_json,
transport.endpoint.header_rules.as_ref(),
&auth_header,
&auth_value,
upstream_is_stream,
) {
Some(headers) => headers,
None => {
mark_skipped_local_standard_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"transport_header_rules_apply_failed",
CandidateFailureDiagnostic::header_rules_apply_failed(
provider_api_format,
provider_api_format,
"standard_family_windsurf_headers",
),
)
.await;
return None;
}
};
Some(LocalStandardCandidatePayloadParts {
auth_header,
auth_value,
mapped_model,
provider_api_format: provider_api_format.to_string(),
provider_request_body,
provider_request_headers,
upstream_url,
upstream_is_stream,
envelope_name: Some(WINDSURF_ENVELOPE_NAME),
transport: Arc::clone(transport),
transport_profile: None,
})
}
async fn resolve_local_gemini_image_to_openai_image_candidate_payload_parts(
state: &AppState,
parts: &http::request::Parts,

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use aether_contracts::ResolvedTransportProfile;
use serde_json::Value;
use serde_json::{json, Value};
use crate::ai_serving::planner::candidate_preparation::{
prepare_header_authenticated_candidate, prepare_header_authenticated_candidate_from_auth,
@@ -16,9 +16,10 @@ use crate::ai_serving::planner::common::{
request_requires_body_stream_field, OPENAI_CHAT_STREAM_PLAN_KIND,
};
use crate::ai_serving::planner::standard::{
apply_codex_openai_responses_special_headers, build_cross_format_openai_chat_request_body,
build_cross_format_openai_chat_upstream_url, build_local_openai_chat_request_body,
build_local_openai_chat_upstream_url, request_body_build_failure_extra_data,
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
build_cross_format_openai_chat_request_body, build_cross_format_openai_chat_upstream_url,
build_local_openai_chat_request_body, build_local_openai_chat_upstream_url,
request_body_build_failure_extra_data,
};
use crate::ai_serving::transport::auth::resolve_local_openai_bearer_auth;
use crate::ai_serving::transport::kiro::{
@@ -27,10 +28,18 @@ use crate::ai_serving::transport::kiro::{
KIRO_ENVELOPE_NAME,
};
use crate::ai_serving::transport::local_openai_chat_transport_unsupported_reason;
use crate::ai_serving::transport::windsurf::{
build_windsurf_cascade_headers, build_windsurf_cascade_request_body,
build_windsurf_cascade_upstream_url, is_windsurf_provider_transport,
local_windsurf_request_transport_unsupported_reason_with_network,
resolve_windsurf_cascade_auth, WINDSURF_ENVELOPE_NAME,
};
use crate::ai_serving::transport::{
build_grok_browser_headers, build_grok_upstream_url, build_kiro_cross_format_upstream_url,
build_standard_provider_request_headers, GrokHeaderInput, StandardProviderRequestHeadersInput,
GROK_CHAT_PATH,
build_openai_image_headers, build_openai_image_upstream_url,
build_standard_provider_request_headers, openai_image_transport_unsupported_reason,
resolve_openai_image_auth, GrokHeaderInput, ProviderOpenAiImageHeadersInput,
StandardProviderRequestHeadersInput, GROK_CHAT_PATH,
};
use crate::ai_serving::{
ai_local_execution_contract_for_formats, request_conversion_direct_auth,
@@ -331,6 +340,25 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
}));
}
if provider_api_format == "openai:chat" && is_windsurf_provider_transport(transport) {
return build_windsurf_openai_chat_payload_parts(
state,
parts,
trace_id,
body_json,
input,
eligible,
candidate_index,
candidate_id,
decision_kind,
report_kind,
transport,
upstream_is_stream,
redaction.redacted,
)
.await;
}
if provider_api_format == "openai:chat" {
if let Some(skip_reason) = local_openai_chat_transport_unsupported_reason(transport) {
mark_skipped_local_openai_chat_candidate(
@@ -498,6 +526,20 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
};
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
if provider_api_format == "openai:image" {
return resolve_openai_chat_to_openai_image_payload_parts(
state,
parts,
trace_id,
body_json,
input,
eligible,
candidate_index,
candidate_id,
upstream_is_stream,
)
.await;
}
let Some(conversion_kind) =
request_conversion_kind("openai:chat", provider_api_format.as_str())
@@ -789,6 +831,628 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
}))
}
#[allow(clippy::too_many_arguments)]
async fn resolve_openai_chat_to_openai_image_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
body_json: &serde_json::Value,
input: &LocalOpenAiChatDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
upstream_is_stream: bool,
) -> Result<Option<LocalOpenAiChatCandidatePayloadParts>, GatewayError> {
let candidate = &eligible.candidate;
let transport = &eligible.transport;
let provider_api_format = "openai:image";
if let Some(skip_reason) =
openai_image_transport_unsupported_reason(transport, provider_api_format)
{
mark_skipped_local_openai_chat_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
skip_reason,
)
.await;
return Ok(None);
}
let prepared_candidate = match prepare_header_authenticated_candidate(
crate::ai_serving::PlannerAppState::new(state),
transport,
candidate,
resolve_openai_image_auth(transport),
OauthPreparationContext {
trace_id,
api_format: provider_api_format,
operation: "openai_chat_image_bridge",
},
)
.await
{
Ok(prepared) => prepared,
Err(skip_reason) => {
mark_skipped_local_openai_chat_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
skip_reason,
)
.await;
return Ok(None);
}
};
let is_chatgpt_web = transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("chatgpt_web");
let Some((mut provider_request_body, image_request_summary)) = (if is_chatgpt_web {
build_chatgpt_web_image_provider_body_from_openai_chat_body(
body_json,
&input.requested_model,
)
} else {
build_openai_image_provider_body_from_openai_chat_body(
body_json,
&input.requested_model,
upstream_is_stream,
)
}) else {
mark_skipped_local_openai_chat_candidate_with_extra_data(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
request_body_build_failure_extra_data(body_json, "openai:chat", provider_api_format),
)
.await;
return Ok(None);
};
if !is_chatgpt_web {
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
transport.provider.provider_type.as_str(),
provider_api_format,
transport.endpoint.body_rules.as_ref(),
Some(candidate.key_id.as_str()),
);
}
let upstream_url = if is_chatgpt_web {
chatgpt_web_image_internal_url(&transport.endpoint.base_url)
} else {
build_openai_image_upstream_url(
transport,
Some("/v1/images/generations"),
parts.uri.query(),
)
};
let Some(mut provider_request_headers) =
build_openai_image_headers(ProviderOpenAiImageHeadersInput {
headers: &parts.headers,
auth_header: &prepared_candidate.auth_header,
auth_value: &prepared_candidate.auth_value,
header_rules: transport.endpoint.header_rules.as_ref(),
provider_request_body: &provider_request_body,
original_request_body: body_json,
})
else {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_header_rules_apply_failed",
CandidateFailureDiagnostic::header_rules_apply_failed(
"openai:chat",
provider_api_format,
"openai_chat_image_bridge_headers",
),
)
.await;
return Ok(None);
};
if is_chatgpt_web {
provider_request_headers.insert("x-aether-chatgpt-web-image".to_string(), "1".to_string());
} else {
apply_codex_openai_responses_special_headers(
&mut provider_request_headers,
&provider_request_body,
&parts.headers,
transport.provider.provider_type.as_str(),
provider_api_format,
Some(trace_id),
transport.key.decrypted_auth_config.as_deref(),
);
}
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats("openai:chat", provider_api_format);
Ok(Some(LocalOpenAiChatCandidatePayloadParts {
client_api_format: "openai:chat".to_string(),
auth_header: prepared_candidate.auth_header,
auth_value: prepared_candidate.auth_value,
mapped_model: prepared_candidate.mapped_model,
provider_api_format: provider_api_format.to_string(),
provider_request_body,
provider_request_headers,
upstream_url,
execution_strategy,
conversion_mode,
report_kind: "openai_chat_stream_success".to_string(),
envelope_name: None,
transport: Arc::clone(transport),
request_redacted: false,
transport_profile: None,
image_request_summary: Some(image_request_summary),
}))
}
fn build_openai_image_provider_body_from_openai_chat_body(
body_json: &Value,
requested_model: &str,
upstream_is_stream: bool,
) -> Option<(Value, Value)> {
let (prompt, images) = collect_openai_chat_image_prompt_and_images(body_json)?;
let operation = if images.is_empty() {
"generate"
} else {
"edit"
};
let mut image_options = serde_json::Map::new();
copy_openai_chat_image_option(body_json, &mut image_options, "size");
copy_openai_chat_image_option(body_json, &mut image_options, "quality");
copy_openai_chat_image_option(body_json, &mut image_options, "background");
copy_openai_chat_image_option(body_json, &mut image_options, "output_format");
copy_openai_chat_image_option(body_json, &mut image_options, "output_compression");
copy_openai_chat_image_option(body_json, &mut image_options, "moderation");
copy_openai_chat_image_option(body_json, &mut image_options, "input_fidelity");
copy_openai_chat_image_option(body_json, &mut image_options, "partial_images");
let input = if images.is_empty() {
serde_json::json!([{
"role": "user",
"content": prompt,
}])
} else {
let mut content = vec![serde_json::json!({
"type": "input_text",
"text": prompt,
})];
content.extend(images);
serde_json::json!([{
"role": "user",
"content": content,
}])
};
let mut body = serde_json::Map::new();
if let Some(model) = body_json
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| {
let requested_model = requested_model.trim();
(!requested_model.is_empty()).then_some(requested_model)
})
{
body.insert("model".to_string(), Value::String(model.to_string()));
}
body.insert("input".to_string(), input);
let mut image_tool = image_options.clone();
image_tool.insert(
"type".to_string(),
Value::String("image_generation".to_string()),
);
body.insert(
"tools".to_string(),
Value::Array(vec![Value::Object(image_tool)]),
);
if upstream_is_stream {
body.insert("stream".to_string(), Value::Bool(true));
}
if let Some(user) = body_json
.get("user")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
body.insert("user".to_string(), Value::String(user.to_string()));
}
let mut summary = serde_json::Map::new();
summary.insert(
"operation".to_string(),
Value::String(operation.to_string()),
);
for key in ["output_format", "partial_images", "size", "quality"] {
if let Some(value) = image_options.get(key) {
summary.insert(key.to_string(), value.clone());
}
}
Some((Value::Object(body), Value::Object(summary)))
}
fn build_chatgpt_web_image_provider_body_from_openai_chat_body(
body_json: &Value,
requested_model: &str,
) -> Option<(Value, Value)> {
let (prompt, images) = collect_openai_chat_image_prompt_and_images(body_json)?;
let operation = if images.is_empty() {
"generate"
} else {
"edit"
};
let size = body_json
.get("size")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("1024x1024");
let output_format = body_json
.get("output_format")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("png");
let quality = body_json
.get("quality")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("medium");
let model = body_json
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or_else(|| requested_model.trim());
let web_model = body_json
.get("web_model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("gpt-5-5-thinking");
let image_urls = openai_image_inputs_as_urls(&images);
let body = json!({
"operation": operation,
"model": if model.is_empty() { "gpt-image-2" } else { model },
"web_model": web_model,
"prompt": prompt,
"size": size,
"ratio": chatgpt_web_ratio_for_size(size),
"output_format": output_format,
"images": image_urls,
});
let summary = json!({
"operation": operation,
"output_format": output_format,
"size": size,
"quality": quality,
});
Some((body, summary))
}
fn copy_openai_chat_image_option(
body_json: &Value,
image_options: &mut serde_json::Map<String, Value>,
key: &str,
) {
if let Some(value) = body_json.get(key) {
image_options.insert(key.to_string(), value.clone());
}
}
fn collect_openai_chat_image_prompt_and_images(body_json: &Value) -> Option<(String, Vec<Value>)> {
let messages = body_json.get("messages").and_then(Value::as_array)?;
let mut prompt_parts = Vec::new();
let mut images = Vec::new();
for message in messages.iter().filter_map(Value::as_object) {
let role = message
.get("role")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
let content = message.get("content");
if matches!(role, "system" | "developer" | "user") {
if let Some(text) = crate::ai_serving::extract_openai_text_content(content)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
{
prompt_parts.push(text);
}
}
if role == "user" {
collect_openai_chat_image_inputs(content, &mut images);
}
}
let prompt = prompt_parts.join("\n").trim().to_string();
(!prompt.is_empty()).then_some((prompt, images))
}
fn collect_openai_chat_image_inputs(content: Option<&Value>, images: &mut Vec<Value>) {
let Some(parts) = content.and_then(Value::as_array) else {
return;
};
for part in parts.iter().filter_map(Value::as_object) {
let part_type = part
.get("type")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
if matches!(part_type, "image_url" | "input_image") {
if let Some(url) = part
.get("image_url")
.and_then(|value| {
value
.as_str()
.or_else(|| value.get("url").and_then(Value::as_str))
})
.map(str::trim)
.filter(|value| !value.is_empty())
{
images.push(serde_json::json!({
"type": "input_image",
"image_url": url,
}));
} else if let Some(file_id) = part
.get("file_id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
images.push(serde_json::json!({
"type": "input_image",
"file_id": file_id,
}));
}
}
}
}
fn openai_image_inputs_as_urls(images: &[Value]) -> Vec<Value> {
images
.iter()
.filter_map(|image| {
image
.get("image_url")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| Value::String(value.to_string()))
})
.collect()
}
fn chatgpt_web_ratio_for_size(size: &str) -> String {
let Some((width, height)) = size.split_once('x') else {
return "1:1".to_string();
};
let Ok(width) = width.trim().parse::<u64>() else {
return "1:1".to_string();
};
let Ok(height) = height.trim().parse::<u64>() else {
return "1:1".to_string();
};
if width == 0 || height == 0 {
return "1:1".to_string();
}
let divisor = gcd(width, height);
format!("{}:{}", width / divisor, height / divisor)
}
fn gcd(mut left: u64, mut right: u64) -> u64 {
while right != 0 {
let next = left % right;
left = right;
right = next;
}
left.max(1)
}
fn chatgpt_web_image_internal_url(base_url: &str) -> String {
let base_url = base_url.trim().trim_end_matches('/');
let base_url = if base_url.is_empty() {
"https://chatgpt.com"
} else {
base_url
};
format!("{base_url}/__aether/chatgpt-web-image")
}
#[allow(clippy::too_many_arguments)]
async fn build_windsurf_openai_chat_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
body_json: &serde_json::Value,
input: &LocalOpenAiChatDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
decision_kind: &str,
report_kind: &str,
transport: &Arc<GatewayProviderTransportSnapshot>,
upstream_is_stream: bool,
request_redacted: bool,
) -> Result<Option<LocalOpenAiChatCandidatePayloadParts>, GatewayError> {
let planner_state = crate::ai_serving::PlannerAppState::new(state);
let candidate = &eligible.candidate;
if let Some(skip_reason) =
local_windsurf_request_transport_unsupported_reason_with_network(transport)
{
mark_skipped_local_openai_chat_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
skip_reason,
)
.await;
return Ok(None);
}
let prepared_candidate = match prepare_header_authenticated_candidate(
planner_state,
transport,
candidate,
resolve_windsurf_cascade_auth(transport)
.or_else(|| resolve_local_openai_bearer_auth(transport)),
OauthPreparationContext {
trace_id,
api_format: "openai:chat",
operation: "openai_chat_windsurf_cascade",
},
)
.await
{
Ok(prepared) => prepared,
Err(skip_reason) => {
mark_skipped_local_openai_chat_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
skip_reason,
)
.await;
return Ok(None);
}
};
let Some(provider_request_body) = build_windsurf_cascade_request_body(
body_json,
&prepared_candidate.mapped_model,
&prepared_candidate.auth_value,
transport.endpoint.body_rules.as_ref(),
Some(&parts.headers),
upstream_is_stream,
) else {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
"openai:chat",
"openai:chat",
"openai_chat_windsurf_cascade",
),
)
.await;
return Ok(None);
};
let Some(upstream_url) = build_windsurf_cascade_upstream_url(
transport.endpoint.base_url.as_str(),
parts.uri.query(),
) else {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"upstream_url_missing",
CandidateFailureDiagnostic::upstream_url_missing(
"openai:chat",
"openai:chat",
"openai_chat_windsurf_url",
),
)
.await;
return Ok(None);
};
let mut provider_request_headers = match build_windsurf_cascade_headers(
&parts.headers,
&provider_request_body,
body_json,
transport.endpoint.header_rules.as_ref(),
&prepared_candidate.auth_header,
&prepared_candidate.auth_value,
upstream_is_stream,
) {
Some(headers) => headers,
None => {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_header_rules_apply_failed",
CandidateFailureDiagnostic::header_rules_apply_failed(
"openai:chat",
"openai:chat",
"openai_chat_windsurf_headers",
),
)
.await;
return Ok(None);
}
};
request_identity_response_encoding_when_redacted(
&mut provider_request_headers,
request_redacted,
);
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats("openai:chat", "openai:chat");
let resolved_report_kind =
if decision_kind == OPENAI_CHAT_STREAM_PLAN_KIND || !upstream_is_stream {
report_kind.to_string()
} else {
"openai_chat_sync_finalize".to_string()
};
Ok(Some(LocalOpenAiChatCandidatePayloadParts {
client_api_format: "openai:chat".to_string(),
auth_header: prepared_candidate.auth_header,
auth_value: prepared_candidate.auth_value,
mapped_model: prepared_candidate.mapped_model,
provider_api_format: "openai:chat".to_string(),
provider_request_body,
provider_request_headers,
upstream_url,
execution_strategy,
conversion_mode,
report_kind: resolved_report_kind,
envelope_name: Some(WINDSURF_ENVELOPE_NAME),
transport: Arc::clone(transport),
request_redacted,
transport_profile: None,
image_request_summary: None,
}))
}
#[allow(clippy::too_many_arguments)]
async fn build_kiro_openai_chat_cross_format_payload_parts(
state: &AppState,
@@ -1012,3 +1676,71 @@ fn redaction_mask_error_to_gateway_error(error: RedactionMaskError) -> GatewayEr
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chatgpt_web_chat_image_bridge_body_uses_internal_web_shape() {
let body_json = json!({
"model": "gpt-image-2",
"messages": [
{"role": "system", "content": "Use crisp vector-like shapes."},
{
"role": "user",
"content": [
{"type": "text", "text": "Draw a glass city"},
{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}}
]
}
],
"size": "1536x1024",
"output_format": "webp",
"web_model": "gpt-5-image-test"
});
let (provider_body, summary) =
build_chatgpt_web_image_provider_body_from_openai_chat_body(&body_json, "gpt-image-2")
.expect("chat image body should convert");
assert_eq!(provider_body["operation"], "edit");
assert_eq!(provider_body["model"], "gpt-image-2");
assert_eq!(provider_body["web_model"], "gpt-5-image-test");
assert_eq!(
provider_body["prompt"],
"Use crisp vector-like shapes.\nDraw a glass city"
);
assert_eq!(provider_body["size"], "1536x1024");
assert_eq!(provider_body["ratio"], "3:2");
assert_eq!(provider_body["output_format"], "webp");
assert_eq!(provider_body["images"][0], "https://example.com/ref.png");
assert_eq!(summary["operation"], "edit");
assert_eq!(summary["output_format"], "webp");
}
#[test]
fn openai_chat_image_bridge_body_injects_image_generation_tool() {
let body_json = json!({
"model": "gpt-image-2",
"messages": [
{"role": "user", "content": "Draw a glass city"}
],
"size": "1024x1024",
"output_format": "png"
});
let (provider_body, summary) =
build_openai_image_provider_body_from_openai_chat_body(&body_json, "gpt-image-2", true)
.expect("chat image body should convert");
assert_eq!(provider_body["tools"][0]["type"], "image_generation");
assert_eq!(provider_body["tools"][0]["size"], "1024x1024");
assert_eq!(provider_body["tools"][0]["output_format"], "png");
assert_eq!(provider_body["model"], "gpt-image-2");
assert_eq!(provider_body["stream"], true);
assert_eq!(provider_body["input"][0]["content"], "Draw a glass city");
assert_eq!(summary["operation"], "generate");
assert_eq!(summary["output_format"], "png");
}
}

View File

@@ -39,10 +39,13 @@ use crate::ai_serving::transport::kiro::{
use crate::ai_serving::transport::{
build_grok_browser_headers, build_grok_upstream_url, build_kiro_cross_format_upstream_url,
build_openai_image_headers, build_openai_image_upstream_url,
build_standard_provider_request_headers,
local_standard_transport_unsupported_reason_with_network,
build_standard_provider_request_headers, build_windsurf_cascade_headers,
build_windsurf_cascade_request_body, build_windsurf_cascade_upstream_url,
is_windsurf_provider_transport, local_standard_transport_unsupported_reason_with_network,
local_windsurf_request_transport_unsupported_reason_with_network,
openai_image_transport_unsupported_reason, resolve_openai_image_auth, GrokHeaderInput,
ProviderOpenAiImageHeadersInput, StandardProviderRequestHeadersInput, GROK_CHAT_PATH,
WINDSURF_ENVELOPE_NAME,
};
use crate::ai_serving::{
ai_local_execution_contract_for_formats, request_conversion_direct_auth,
@@ -128,6 +131,8 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
)
.await;
}
let is_windsurf_cascade =
provider_api_format == "openai:chat" && is_windsurf_provider_transport(transport);
let same_format = api_format_alias_matches(provider_api_format, &client_api_format);
let conversion_kind = request_conversion_kind(spec_metadata.api_format, provider_api_format);
@@ -139,6 +144,8 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
local_kiro_request_transport_unsupported_reason_with_network(transport)
} else if same_format {
local_standard_transport_unsupported_reason_with_network(transport, provider_api_format)
} else if is_windsurf_cascade {
local_windsurf_request_transport_unsupported_reason_with_network(transport)
} else {
match conversion_kind {
Some(_) if is_antigravity && provider_api_format == "gemini:generate_content" => None,
@@ -302,7 +309,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
upstream_is_stream,
force_body_stream_field,
transport.provider.provider_type.as_str(),
if is_kiro_claude_cli {
if is_kiro_claude_cli || is_windsurf_cascade {
None
} else {
transport.endpoint.body_rules.as_ref()
@@ -319,7 +326,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
force_body_stream_field,
transport.provider.provider_type.as_str(),
provider_api_format,
if is_kiro_claude_cli {
if is_kiro_claude_cli || is_windsurf_cascade {
None
} else {
transport.endpoint.body_rules.as_ref()
@@ -447,6 +454,27 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
)
.await;
}
if is_windsurf_cascade {
return build_windsurf_openai_responses_payload_parts(
state,
parts,
trace_id,
body_json,
input,
eligible,
candidate_index,
candidate_id,
spec_metadata.api_format,
transport,
provider_api_format,
mapped_model,
auth_header,
auth_value,
provider_request_body,
upstream_is_stream,
)
.await;
}
let Some(upstream_url) = (if is_grok && is_grok_text_provider_api_format(provider_api_format) {
Some(build_grok_upstream_url(transport, GROK_CHAT_PATH))
@@ -619,6 +647,130 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
})
}
#[allow(clippy::too_many_arguments)]
async fn build_windsurf_openai_responses_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
original_body_json: &serde_json::Value,
input: &LocalOpenAiResponsesDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
client_api_format: &str,
transport: &Arc<GatewayProviderTransportSnapshot>,
provider_api_format: &str,
mapped_model: String,
auth_header: String,
auth_value: String,
openai_chat_request_body: Value,
upstream_is_stream: bool,
) -> Option<LocalOpenAiResponsesCandidatePayloadParts> {
let candidate = &eligible.candidate;
let effective_headers = input.effective_headers(&parts.headers);
let provider_request_body = match build_windsurf_cascade_request_body(
&openai_chat_request_body,
&mapped_model,
&auth_value,
transport.endpoint.body_rules.as_ref(),
Some(effective_headers),
upstream_is_stream,
) {
Some(body) => body,
None => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
client_api_format,
provider_api_format,
"openai_responses_windsurf_cascade",
),
)
.await;
return None;
}
};
let upstream_url = match build_windsurf_cascade_upstream_url(
transport.endpoint.base_url.as_str(),
parts.uri.query(),
) {
Some(url) => url,
None => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"upstream_url_missing",
CandidateFailureDiagnostic::upstream_url_missing(
client_api_format,
provider_api_format,
"openai_responses_windsurf_url",
),
)
.await;
return None;
}
};
let provider_request_headers = match build_windsurf_cascade_headers(
effective_headers,
&provider_request_body,
original_body_json,
transport.endpoint.header_rules.as_ref(),
&auth_header,
&auth_value,
upstream_is_stream,
) {
Some(headers) => headers,
None => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_header_rules_apply_failed",
CandidateFailureDiagnostic::header_rules_apply_failed(
client_api_format,
provider_api_format,
"openai_responses_windsurf_headers",
),
)
.await;
return None;
}
};
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats(client_api_format, provider_api_format);
Some(LocalOpenAiResponsesCandidatePayloadParts {
auth_header,
auth_value,
mapped_model,
provider_api_format: provider_api_format.to_string(),
provider_request_body,
provider_request_headers,
upstream_url,
execution_strategy,
conversion_mode,
is_antigravity: false,
envelope_name: Some(WINDSURF_ENVELOPE_NAME),
upstream_is_stream,
transport: Arc::clone(transport),
transport_profile: None,
image_request_summary: None,
})
}
fn api_format_alias_matches(left: &str, right: &str) -> bool {
crate::ai_serving::api_format_alias_matches(left, right)
}

View File

@@ -50,6 +50,10 @@ pub(crate) mod vertex {
pub(crate) use aether_provider_transport::vertex::*;
}
pub(crate) mod windsurf {
pub(crate) use aether_provider_transport::windsurf::*;
}
pub(crate) use aether_provider_transport::{
append_transport_diagnostics_to_value, apply_local_body_rules,
apply_local_body_rules_with_request_headers, apply_local_header_rules,
@@ -69,12 +73,15 @@ pub(crate) use aether_provider_transport::{
build_standard_plan_fallback_openai_responses_url, build_standard_provider_request_headers,
build_transport_request_url, build_transport_request_url_for_request_body,
build_video_create_headers, build_video_create_request_body, build_video_create_upstream_url,
candidate_common_transport_skip_reason, candidate_transport_pair_skip_reason,
classify_same_format_provider_request_behavior, ensure_upstream_auth_header,
gemini_files_transport_unsupported_reason, header_rules_are_locally_supported,
header_rules_have_enabled_rules, local_gemini_transport_unsupported_reason_with_network,
build_windsurf_cascade_headers, build_windsurf_cascade_request_body,
build_windsurf_cascade_upstream_url, candidate_common_transport_skip_reason,
candidate_transport_pair_skip_reason, classify_same_format_provider_request_behavior,
ensure_upstream_auth_header, gemini_files_transport_unsupported_reason,
header_rules_are_locally_supported, header_rules_have_enabled_rules,
is_windsurf_provider_transport, local_gemini_transport_unsupported_reason_with_network,
local_openai_chat_transport_unsupported_reason,
local_standard_transport_unsupported_reason_with_network,
local_windsurf_request_transport_unsupported_reason_with_network,
openai_image_transport_unsupported_reason, request_conversion_direct_auth,
request_conversion_enabled_for_transport, request_conversion_transport_supported,
request_conversion_transport_unsupported_reason, request_pair_allowed_for_transport,
@@ -97,4 +104,5 @@ pub(crate) use aether_provider_transport::{
StandardPlanFallbackHeadersInput, StandardProviderRequestHeaders,
StandardProviderRequestHeadersInput, TransportRequestBodySemanticsError,
TransportRequestUrlParams, GROK_CHAT_PATH, GROK_INTERNAL_HEADER, GROK_RATE_LIMITS_PATH,
WINDSURF_ENVELOPE_NAME,
};

View File

@@ -20,6 +20,7 @@ mod stream_pump;
pub(crate) mod submission;
pub(crate) mod sync;
pub(crate) mod transport;
mod windsurf;
pub(crate) use self::chatgpt_web_image::maybe_execute_chatgpt_web_image_sync;
pub(crate) use self::constants::{

View File

@@ -44,13 +44,14 @@ use super::error::{
#[path = "execution_failures.rs"]
mod execution_failures;
use self::execution_failures::{
build_stream_failure_from_execution_error, build_stream_failure_report,
build_stream_failure_from_execution_error, build_stream_failure_from_provider_error_body,
build_stream_failure_report, handle_prefetch_provider_private_stream_error,
handle_prefetch_stream_failure, submit_midstream_stream_failure, StreamFailureReport,
};
use crate::ai_serving::api::{
maybe_bridge_standard_sync_json_to_stream, maybe_build_provider_private_stream_normalizer,
maybe_build_stream_response_rewriter, normalize_provider_private_report_context,
StreamingStandardTerminalObserver,
extract_provider_private_stream_error_body, maybe_bridge_standard_sync_json_to_stream,
maybe_build_provider_private_stream_normalizer, maybe_build_stream_response_rewriter,
normalize_provider_private_report_context, StreamingStandardTerminalObserver,
};
use crate::api::response::{
attach_control_metadata_headers, build_client_response, build_client_response_from_parts,
@@ -73,14 +74,15 @@ use crate::execution_runtime::oauth_retry::refresh_oauth_plan_auth_for_retry;
#[cfg(test)]
use crate::execution_runtime::remote_compat::post_stream_plan_to_remote_execution_runtime;
use crate::execution_runtime::submission::{
resolve_core_error_background_report_kind, strip_utf8_bom_and_ws,
submit_local_core_error_or_sync_finalize,
resolve_core_error_background_report_kind, resolve_local_sync_error_status_code,
strip_utf8_bom_and_ws, submit_local_core_error_or_sync_finalize,
};
use crate::execution_runtime::transport::{
execute_stream_plan_via_local_tunnel, record_manual_proxy_request_failure,
record_manual_proxy_request_success, record_manual_proxy_stream_error,
DirectSyncExecutionRuntime, DirectUpstreamStreamExecution, ExecutionRuntimeTransportError,
};
use crate::execution_runtime::windsurf::maybe_execute_windsurf_stream;
use crate::execution_runtime::{
apply_endpoint_response_header_rules, attach_provider_response_headers_to_report_context,
local_failover_response_text, resolve_core_stream_direct_finalize_report_kind,
@@ -849,6 +851,58 @@ pub(crate) async fn execute_execution_runtime_stream(
return Ok(None);
}
}
match maybe_execute_windsurf_stream(state, &plan, report_context.as_ref()).await {
Ok(Some(windsurf_stream)) => {
return execute_stream_from_frame_stream(
state,
plan,
trace_id,
decision,
plan_kind,
report_kind,
windsurf_stream.report_context.or(report_context),
candidate_started_unix_secs,
stream_started_at,
windsurf_stream.frame_stream,
provider_pool_in_flight_guard.take(),
)
.await;
}
Ok(None) => {}
Err(err) => {
info!(
event_name = "windsurf_native_execution_unavailable",
log_type = "ops",
trace_id = %trace_id,
request_id = %plan_request_id_for_log,
candidate_id = ?plan.candidate_id,
provider_name = provider_name.as_str(),
endpoint_id = %endpoint_id,
key_id = %key_id,
model_name = model_name.as_str(),
candidate_index = candidate_index.as_str(),
error = %err,
"gateway native Windsurf stream execution unavailable"
);
let terminal_unix_secs = current_request_candidate_unix_ms();
record_local_request_candidate_status(
state,
&plan,
report_context.as_ref(),
SchedulerRequestCandidateStatusUpdate {
status: RequestCandidateStatus::Failed,
status_code: None,
error_type: Some("windsurf_native_execution_unavailable".to_string()),
error_message: Some(err.to_string()),
latency_ms: None,
started_at_unix_ms: Some(candidate_started_unix_secs),
finished_at_unix_ms: Some(terminal_unix_secs),
},
)
.await;
return Ok(None);
}
}
match maybe_execute_kiro_web_search_stream(state, &plan, report_context.as_ref()).await {
Ok(Some(kiro_web_search)) => {
return execute_stream_from_frame_stream(
@@ -1197,13 +1251,13 @@ fn encode_terminal_sse_error_event(failure: &StreamFailureReport) -> Result<Byte
let payload = failure
.to_json_string()
.map_err(|err| IoError::other(err.to_string()))?;
let mut event = String::from("event: aether.error\n");
let mut event = String::new();
for line in payload.lines() {
event.push_str("data: ");
event.push_str(line);
event.push('\n');
}
event.push('\n');
event.push_str("\ndata: [DONE]\n\n");
Ok(Bytes::from(event))
}
@@ -1693,24 +1747,46 @@ async fn execute_stream_from_frame_stream(
if !(200..300).contains(&status_code) {
let provider_error_body = collect_error_body(&mut lines).await?;
let synthetic_body_json =
should_synthesize_non_success_stream_error_body(status_code, &provider_error_body)
.then(|| build_synthetic_non_success_stream_error_body(status_code, &headers));
let (provider_body_json, provider_body_base64) =
decode_stream_error_body(&headers, &provider_error_body);
let client_status_code = stream_client_error_status_code_for_upstream_status(status_code);
let wrapped_binary_body_json = wrap_non_json_binary_stream_error_for_client(
plan_kind,
&headers,
let private_error_body_json = extract_provider_private_stream_error_body(
report_context.as_ref(),
&provider_error_body,
)?;
let (client_body_json, client_error_body) =
);
let provider_private_error_decoded = private_error_body_json.is_some();
let synthetic_body_json = (!provider_private_error_decoded
&& should_synthesize_non_success_stream_error_body(status_code, &provider_error_body))
.then(|| build_synthetic_non_success_stream_error_body(status_code, &headers));
let (provider_body_json, provider_body_base64) =
if let Some(error_body_json) = private_error_body_json {
(Some(error_body_json), None)
} else {
decode_stream_error_body(&headers, &provider_error_body)
};
let client_status_code = stream_client_error_status_code_for_upstream_status(status_code);
let wrapped_binary_body_json = if provider_private_error_decoded {
None
} else {
wrap_non_json_binary_stream_error_for_client(plan_kind, &headers, &provider_error_body)?
};
let (client_body_json, client_error_body, payload_client_body_json) =
if let Some(body_json) = synthetic_body_json.or(wrapped_binary_body_json) {
let body_bytes = serde_json::to_vec(&body_json)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
(Some(body_json), body_bytes)
(Some(body_json.clone()), body_bytes, Some(body_json))
} else if provider_private_error_decoded {
let body_json = provider_body_json.clone().ok_or_else(|| {
GatewayError::Internal(
"decoded provider private stream error body is missing".to_string(),
)
})?;
let body_bytes = serde_json::to_vec(&body_json)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
(Some(body_json), body_bytes, None)
} else {
(provider_body_json.clone(), provider_error_body.clone())
(
provider_body_json.clone(),
provider_error_body.clone(),
provider_body_json.clone(),
)
};
let error_response_text =
local_failover_response_text(client_body_json.as_ref(), &client_error_body, None);
@@ -1897,6 +1973,11 @@ async fn execute_stream_from_frame_stream(
} else {
headers.clone()
};
if provider_private_error_decoded {
client_headers.remove("content-encoding");
client_headers.remove("content-length");
client_headers.insert("content-type".to_string(), "application/json".to_string());
}
apply_endpoint_response_header_rules(
state,
&plan,
@@ -1928,7 +2009,7 @@ async fn execute_stream_from_frame_stream(
provider_body_json,
provider_body_base64,
client_headers,
client_body_json,
payload_client_body_json,
None,
);
record_sync_terminal_usage(state, &plan, payload.report_context.as_ref(), &payload);
@@ -2127,6 +2208,30 @@ async fn execute_stream_from_frame_stream(
provider_prefetched_body.extend_from_slice(&chunk);
prefetched_inspection_body.extend_from_slice(&chunk);
if let Some(error_body_json) = extract_provider_private_stream_error_body(
report_context.as_ref(),
&prefetched_inspection_body,
) {
let error_status_code =
resolve_local_sync_error_status_code(status_code, &error_body_json);
return handle_prefetch_provider_private_stream_error(
state,
trace_id,
decision,
&plan,
report_context,
request_id,
candidate_id,
report_kind,
headers,
prefetched_telemetry,
&provider_prefetched_body,
error_status_code,
error_body_json,
)
.await;
}
let inspection = inspect_prefetched_stream_body(
&upstream_headers,
&prefetched_inspection_body,
@@ -2835,6 +2940,11 @@ async fn execute_stream_from_frame_stream(
} else {
chunk
};
let provider_private_error_body_json =
extract_provider_private_stream_error_body(
stream_usage_report_context.as_ref(),
&normalized_chunk,
);
if let (Some(observer), Some(report_context)) = (
stream_usage_observer.as_mut(),
stream_usage_report_context.as_ref(),
@@ -2873,6 +2983,18 @@ async fn execute_stream_from_frame_stream(
};
if rewritten_chunk.is_empty() {
if let Some(error_body_json) = provider_private_error_body_json {
let error_status_code = resolve_local_sync_error_status_code(
status_code,
&error_body_json,
);
terminal_failure =
Some(build_stream_failure_from_provider_error_body(
error_status_code,
&error_body_json,
));
break;
}
continue;
}
@@ -2935,6 +3057,15 @@ async fn execute_stream_from_frame_stream(
Ordering::Relaxed,
);
}
if let Some(error_body_json) = provider_private_error_body_json {
let error_status_code =
resolve_local_sync_error_status_code(status_code, &error_body_json);
terminal_failure = Some(build_stream_failure_from_provider_error_body(
error_status_code,
&error_body_json,
));
break;
}
}
StreamFramePayload::Telemetry {
telemetry: frame_telemetry,
@@ -2992,6 +3123,11 @@ async fn execute_stream_from_frame_stream(
if let Some(normalizer) = private_stream_normalizer.as_mut() {
match normalizer.finish() {
Ok(normalized_chunk) if !normalized_chunk.is_empty() => {
let provider_private_error_body_json =
extract_provider_private_stream_error_body(
stream_usage_report_context.as_ref(),
&normalized_chunk,
);
if let (Some(observer), Some(report_context)) = (
stream_usage_observer.as_mut(),
stream_usage_report_context.as_ref(),
@@ -3065,6 +3201,16 @@ async fn execute_stream_from_frame_stream(
);
}
}
if let Some(error_body_json) = provider_private_error_body_json {
let error_status_code =
resolve_local_sync_error_status_code(status_code, &error_body_json);
terminal_failure.get_or_insert_with(|| {
build_stream_failure_from_provider_error_body(
error_status_code,
&error_body_json,
)
});
}
}
Ok(_) => {}
Err(err) => {
@@ -3478,6 +3624,7 @@ mod tests {
use axum::extract::Request;
use axum::routing::any;
use axum::{http::header, http::HeaderValue, Router};
use base64::Engine as _;
use futures_util::StreamExt as _;
use serde_json::{json, Value};
use tokio::sync::{mpsc, watch, Notify};
@@ -3528,6 +3675,14 @@ mod tests {
}
}
fn connect_json_frame(flags: u8, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(5 + payload.len());
out.push(flags);
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
out.extend_from_slice(payload);
out
}
#[test]
fn merge_stream_terminal_summary_prefers_more_complete_observed_usage() {
let mut runtime_usage = StandardizedUsage::new();
@@ -4412,6 +4567,268 @@ mod tests {
assert!(text.contains("\"type\":\"image_stream_total_timeout\""));
}
#[tokio::test]
async fn execute_stream_from_frame_stream_treats_windsurf_connect_trailer_error_as_failure() {
let usage_repository = Arc::new(InMemoryUsageReadRepository::default());
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let state = AppState::new()
.expect("app state should build")
.with_data_state_for_tests(
crate::data::GatewayDataState::with_request_candidate_and_usage_repository_for_tests(
Arc::clone(&request_candidate_repository),
Arc::clone(&usage_repository),
),
)
.with_usage_runtime_for_tests(UsageRuntimeConfig {
enabled: true,
..UsageRuntimeConfig::default()
});
let plan = ExecutionPlan {
request_id: "req-windsurf-connect-error".into(),
candidate_id: Some("cand-windsurf-connect-error".into()),
provider_name: Some("windsurf".into()),
provider_id: "provider-windsurf".into(),
endpoint_id: "endpoint-windsurf-chat".into(),
key_id: "key-windsurf".into(),
method: "POST".into(),
url: "https://server.codeium.com/exa.api_server_pb.ApiServerService/GetChatMessage?beta=true".into(),
headers: BTreeMap::from([
("content-type".into(), "application/connect+json".into()),
("accept".into(), "application/connect+json".into()),
]),
content_type: Some("application/connect+json".into()),
content_encoding: None,
body: RequestBody::from_json(json!({
"model": "claude-sonnet-4",
"messages": [],
"stream": true
})),
stream: true,
client_api_format: "claude:messages".into(),
provider_api_format: "openai:chat".into(),
model_name: Some("claude-sonnet-4".into()),
proxy: None,
transport_profile: None,
timeouts: None,
};
let trailer_error = connect_json_frame(
2,
br#"{"error":{"code":"resource_exhausted","message":"an internal error occurred"}}"#,
);
let trailer_error_b64 = base64::engine::general_purpose::STANDARD.encode(trailer_error);
let frame = format!(
"{{\"type\":\"data\",\"payload\":{{\"kind\":\"data\",\"chunk_b64\":\"{trailer_error_b64}\"}}}}\n"
);
let frame_stream = stream! {
yield Ok::<Bytes, std::io::Error>(Bytes::from_static(
b"{\"type\":\"headers\",\"payload\":{\"kind\":\"headers\",\"status_code\":200,\"headers\":{\"content-type\":\"application/connect+json\"}}}\n",
));
yield Ok::<Bytes, std::io::Error>(Bytes::from(frame));
yield Ok::<Bytes, std::io::Error>(Bytes::from_static(
b"{\"type\":\"eof\",\"payload\":{\"kind\":\"eof\"}}\n",
));
}
.boxed();
let response = execute_stream_from_frame_stream(
&state,
plan,
"trace-windsurf-connect-error",
&test_decision(),
"claude_chat_stream",
Some("claude_chat_stream_success".to_string()),
Some(json!({
"request_id": "req-windsurf-connect-error",
"candidate_id": "cand-windsurf-connect-error",
"candidate_index": 0,
"retry_index": 0,
"provider_api_format": "openai:chat",
"client_api_format": "claude:messages",
"needs_conversion": true,
"has_envelope": true,
"envelope_name": "windsurf:GetChatMessage",
"local_failover_policy": {
"stop_status_codes": [429]
}
})),
crate::clock::current_unix_ms(),
Instant::now(),
frame_stream,
None,
)
.await
.expect("execution should succeed")
.expect("execution should return a client response");
let status = response.status();
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("response body should read");
let body_json: Value =
serde_json::from_slice(&body).expect("response body should decode as json");
assert_eq!(status.as_u16(), 429);
assert_eq!(body_json["type"], json!("error"));
assert_eq!(body_json["error"]["type"], json!("rate_limit_error"));
assert_eq!(body_json["error"]["code"], json!("resource_exhausted"));
assert_eq!(
body_json["error"]["message"],
json!("an internal error occurred")
);
let candidates = tokio::time::timeout(Duration::from_secs(1), async {
loop {
let candidates = request_candidate_repository
.list_by_request_id("req-windsurf-connect-error")
.await
.expect("request candidates should read");
if candidates
.first()
.is_some_and(|candidate| candidate.status == RequestCandidateStatus::Failed)
{
break candidates;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("candidate should be marked failed");
assert_eq!(candidates[0].status_code, Some(429));
assert_eq!(
candidates[0].error_type.as_deref(),
Some("resource_exhausted")
);
}
#[tokio::test]
async fn execute_stream_from_frame_stream_decodes_non_success_windsurf_connect_error_body() {
let usage_repository = Arc::new(InMemoryUsageReadRepository::default());
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let state = AppState::new()
.expect("app state should build")
.with_data_state_for_tests(
crate::data::GatewayDataState::with_request_candidate_and_usage_repository_for_tests(
Arc::clone(&request_candidate_repository),
Arc::clone(&usage_repository),
),
)
.with_usage_runtime_for_tests(UsageRuntimeConfig {
enabled: true,
..UsageRuntimeConfig::default()
});
let plan = ExecutionPlan {
request_id: "req-windsurf-connect-429".into(),
candidate_id: Some("cand-windsurf-connect-429".into()),
provider_name: Some("windsurf".into()),
provider_id: "provider-windsurf".into(),
endpoint_id: "endpoint-windsurf-chat".into(),
key_id: "key-windsurf".into(),
method: "POST".into(),
url: "https://server.codeium.com/exa.api_server_pb.ApiServerService/GetChatMessage?beta=true".into(),
headers: BTreeMap::from([
("content-type".into(), "application/connect+json".into()),
("accept".into(), "application/connect+json".into()),
]),
content_type: Some("application/connect+json".into()),
content_encoding: None,
body: RequestBody::from_json(json!({
"model": "claude-sonnet-4",
"messages": [],
"stream": true
})),
stream: true,
client_api_format: "claude:messages".into(),
provider_api_format: "openai:chat".into(),
model_name: Some("claude-sonnet-4".into()),
proxy: None,
transport_profile: None,
timeouts: None,
};
let connect_error = connect_json_frame(
2,
br#"{"error":{"code":"resource_exhausted","message":"quota exhausted"}}"#,
);
let connect_error_b64 = base64::engine::general_purpose::STANDARD.encode(connect_error);
let frame_stream = stream! {
yield Ok::<Bytes, std::io::Error>(Bytes::from_static(
b"{\"type\":\"headers\",\"payload\":{\"kind\":\"headers\",\"status_code\":429,\"headers\":{\"content-type\":\"application/connect+json\"}}}\n",
));
yield Ok::<Bytes, std::io::Error>(Bytes::from(format!(
"{{\"type\":\"data\",\"payload\":{{\"kind\":\"data\",\"chunk_b64\":\"{connect_error_b64}\"}}}}\n"
)));
yield Ok::<Bytes, std::io::Error>(Bytes::from_static(
b"{\"type\":\"eof\",\"payload\":{\"kind\":\"eof\"}}\n",
));
}
.boxed();
let response = execute_stream_from_frame_stream(
&state,
plan,
"trace-windsurf-connect-429",
&test_decision(),
"claude_chat_stream",
Some("claude_chat_stream_success".to_string()),
Some(json!({
"request_id": "req-windsurf-connect-429",
"candidate_id": "cand-windsurf-connect-429",
"candidate_index": 0,
"retry_index": 0,
"provider_api_format": "openai:chat",
"client_api_format": "claude:messages",
"needs_conversion": true,
"has_envelope": true,
"envelope_name": "windsurf:GetChatMessage",
"local_failover_policy": {
"stop_status_codes": [429]
}
})),
crate::clock::current_unix_ms(),
Instant::now(),
frame_stream,
None,
)
.await
.expect("execution should succeed")
.expect("execution should return a client response");
assert_eq!(response.status().as_u16(), 429);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("response body should read");
let body_json: Value =
serde_json::from_slice(&body).expect("response body should decode as json");
assert_eq!(body_json["type"], json!("error"));
assert_eq!(body_json["error"]["type"], json!("rate_limit_error"));
assert_eq!(body_json["error"]["code"], json!("resource_exhausted"));
assert_eq!(body_json["error"]["message"], json!("quota exhausted"));
let record = tokio::time::timeout(Duration::from_secs(2), async {
loop {
if let Some(usage) = usage_repository
.find_by_request_id("req-windsurf-connect-429")
.await
.expect("usage should read")
.filter(|usage| usage.status == "failed")
{
break usage;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("usage should be written");
assert_eq!(record.status_code, Some(429));
assert_eq!(
record
.response_body
.as_ref()
.and_then(|body| body.get("error"))
.and_then(|error| error.get("code")),
Some(&json!("resource_exhausted"))
);
assert!(record.response_body_ref.is_none());
}
#[tokio::test]
async fn execute_stream_from_frame_stream_stops_upstream_when_client_drops_body() {
let usage_repository = Arc::new(InMemoryUsageReadRepository::default());
@@ -5560,8 +5977,9 @@ mod tests {
let body = body_task.await.expect("body task should complete");
assert!(body.contains("data: hello\n\n"));
assert!(body.contains("event: aether.error\n"));
assert!(body.contains("data: {\"error\":"));
assert!(body.contains(original_error));
assert!(body.contains("data: [DONE]\n\n"));
assert!(
!body.contains("unexpected EOF during chunk size line"),
"same-format SSE path should surface the original terminal error event"

View File

@@ -126,6 +126,54 @@ pub(super) fn build_stream_failure_from_execution_error(
}
}
pub(super) fn build_stream_failure_from_provider_error_body(
status_code: u16,
body_json: &Value,
) -> StreamFailureReport {
let body_object = body_json.as_object();
let error_object = body_object
.and_then(|object| object.get("error"))
.and_then(Value::as_object);
let error_type =
first_non_empty_error_text(error_object, body_object, &["type", "code", "status"])
.unwrap_or_else(|| "upstream_error".to_string());
let error_message = first_non_empty_error_text(
error_object,
body_object,
&["message", "detail", "reason", "status", "type", "code"],
)
.unwrap_or_else(|| format!("upstream stream returned error status {status_code}"));
StreamFailureReport {
status_code,
error_type,
error_message,
extra_error_fields: Map::new(),
}
}
fn first_non_empty_error_text(
error_object: Option<&Map<String, Value>>,
body_object: Option<&Map<String, Value>>,
keys: &[&str],
) -> Option<String> {
for object in [error_object, body_object].into_iter().flatten() {
for key in keys {
let Some(value) = object.get(*key) else {
continue;
};
match value {
Value::String(text) if !text.trim().is_empty() => {
return Some(text.trim().to_string());
}
Value::Number(number) => return Some(number.to_string()),
_ => {}
}
}
}
None
}
fn build_stream_failure_sync_payload(
trace_id: &str,
report_kind: String,
@@ -296,6 +344,49 @@ async fn record_stream_sync_failure(
.await;
}
#[allow(clippy::too_many_arguments)] // internal helper for prefetch error handling
pub(super) async fn handle_prefetch_provider_private_stream_error(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
plan: &ExecutionPlan,
report_context: Option<Value>,
request_id: &str,
candidate_id: Option<&str>,
report_kind: &str,
mut headers: std::collections::BTreeMap<String, String>,
telemetry: Option<ExecutionTelemetry>,
buffered_body: &[u8],
status_code: u16,
body_json: Value,
) -> Result<Option<Response<Body>>, GatewayError> {
headers.remove("content-encoding");
headers.remove("content-length");
headers.insert("content-type".to_string(), "application/json".to_string());
let payload = GatewaySyncReportRequest {
trace_id: trace_id.to_string(),
report_kind: report_kind.to_string(),
report_context,
status_code,
headers,
body_json: Some(body_json),
client_body_json: None,
body_base64: (!buffered_body.is_empty())
.then(|| base64::engine::general_purpose::STANDARD.encode(buffered_body)),
telemetry,
};
record_stream_sync_failure(state, plan, payload.report_context.as_ref(), &payload, None).await;
let response =
submit_local_core_error_or_sync_finalize(state, trace_id, decision, payload).await?;
Ok(Some(attach_control_metadata_headers(
response,
Some(request_id),
candidate_id,
)?))
}
#[allow(clippy::too_many_arguments)] // internal helper for prefetch error handling
pub(super) async fn handle_prefetch_stream_failure(
state: &AppState,

View File

@@ -372,7 +372,10 @@ pub(crate) fn resolve_core_success_background_report_kind(report_kind: &str) ->
core_success_background_report_kind(report_kind).map(ToOwned::to_owned)
}
fn resolve_local_sync_error_status_code(status_code: u16, body_json: &serde_json::Value) -> u16 {
pub(crate) fn resolve_local_sync_error_status_code(
status_code: u16,
body_json: &serde_json::Value,
) -> u16 {
if (400..600).contains(&status_code) {
return status_code;
}

View File

@@ -20,7 +20,6 @@ use async_stream::stream;
use axum::body::{to_bytes, Body, Bytes};
use axum::http::header::{CACHE_CONTROL, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE};
use axum::http::{HeaderName, HeaderValue, Response, StatusCode};
use base64::Engine as _;
use futures_util::StreamExt;
use serde_json::{json, Value};
use tokio::sync::mpsc;
@@ -29,8 +28,9 @@ use tokio::time::MissedTickBehavior;
use tracing::{debug, warn};
use crate::ai_serving::api::{
build_core_error_body_for_client_format, implicit_sync_finalize_report_kind,
maybe_build_sync_finalize_outcome, LocalCoreSyncErrorKind, LocalCoreSyncFinalizeOutcome,
build_core_error_body_for_client_format, extract_provider_private_stream_error_body,
implicit_sync_finalize_report_kind, maybe_build_sync_finalize_outcome, LocalCoreSyncErrorKind,
LocalCoreSyncFinalizeOutcome,
};
use crate::api::response::{
attach_control_metadata_headers, build_client_response, build_client_response_from_parts,
@@ -43,12 +43,16 @@ use crate::execution_runtime::grok::maybe_execute_grok_sync;
use crate::execution_runtime::oauth_retry::refresh_oauth_plan_auth_for_retry;
#[cfg(test)]
use crate::execution_runtime::remote_compat::post_sync_plan_to_remote_execution_runtime;
use crate::execution_runtime::submission::submit_local_core_error_or_sync_finalize;
use crate::execution_runtime::transport::{
build_request_body, collect_response_headers, decode_response_body_bytes,
format_upstream_request_error, format_wreq_upstream_request_error, response_body_is_json,
send_request, DirectHttpResponse, DirectSyncExecutionRuntime, ExecutionRuntimeTransportError,
use crate::execution_runtime::submission::{
resolve_local_sync_error_status_code, submit_local_core_error_or_sync_finalize,
};
use crate::execution_runtime::transport::{
build_execution_response_body, build_request_body, collect_response_headers,
decode_response_body_bytes, format_upstream_request_error, format_wreq_upstream_request_error,
response_body_is_json, send_request, DirectHttpResponse, DirectSyncExecutionRuntime,
ExecutionRuntimeTransportError,
};
use crate::execution_runtime::windsurf::maybe_execute_windsurf_sync;
use crate::execution_runtime::{
analyze_local_candidate_failover_sync, apply_endpoint_response_header_rules,
attach_provider_response_headers_to_report_context, local_failover_response_text,
@@ -398,6 +402,43 @@ fn build_invalid_provider_success_body(
)
}
fn provider_private_error_details(body_json: &Value) -> (Option<String>, Option<String>) {
let body_object = body_json.as_object();
let error_object = body_object
.and_then(|object| object.get("error"))
.and_then(Value::as_object);
let error_type =
first_non_empty_error_text(error_object, body_object, &["type", "code", "status"]);
let error_message = first_non_empty_error_text(
error_object,
body_object,
&["message", "detail", "reason", "status", "type", "code"],
);
(error_type, error_message)
}
fn first_non_empty_error_text(
error_object: Option<&serde_json::Map<String, Value>>,
body_object: Option<&serde_json::Map<String, Value>>,
keys: &[&str],
) -> Option<String> {
for object in [error_object, body_object].into_iter().flatten() {
for key in keys {
let Some(value) = object.get(*key) else {
continue;
};
match value {
Value::String(text) if !text.trim().is_empty() => {
return Some(text.trim().to_string());
}
Value::Number(number) => return Some(number.to_string()),
_ => {}
}
}
}
None
}
#[derive(Debug, Clone)]
struct OpenAiImageSyncProgressSnapshot {
phase: &'static str,
@@ -788,6 +829,12 @@ async fn execute_direct_sync_runtime_candidate(
candidate_index: &str,
progress_snapshot: Option<Arc<Mutex<OpenAiImageSyncProgressSnapshot>>>,
) -> Result<ExecutionResult, SyncExecutionFailure> {
if let Some(result) = maybe_execute_windsurf_sync(state, plan, report_context)
.await
.map_err(SyncExecutionFailure::from_transport)?
{
return Ok(result);
}
if !should_track_openai_image_sync_upstream_sse(plan_kind, plan, report_context) {
return DirectSyncExecutionRuntime::new()
.execute_sync(plan)
@@ -945,27 +992,9 @@ async fn execute_openai_image_sync_upstream_sse_candidate(
let upstream_bytes = body_bytes.len() as u64;
progress.finish(status_code, elapsed_ms).await;
let body = if body_bytes.is_empty() {
None
} else if plan.stream {
Some(aether_contracts::ResponseBody {
json_body: None,
body_bytes_b64: Some(base64::engine::general_purpose::STANDARD.encode(&body_bytes)),
})
} else if response_body_is_json(&headers, &decoded_body_bytes) {
let body_json: Value = serde_json::from_slice(&decoded_body_bytes)
.map_err(ExecutionRuntimeTransportError::InvalidJson)
let body =
build_execution_response_body(&headers, &body_bytes, &decoded_body_bytes, plan.stream)
.map_err(SyncExecutionFailure::from_transport)?;
Some(aether_contracts::ResponseBody {
json_body: Some(body_json),
body_bytes_b64: None,
})
} else {
Some(aether_contracts::ResponseBody {
json_body: None,
body_bytes_b64: Some(base64::engine::general_purpose::STANDARD.encode(&body_bytes)),
})
};
Ok(ExecutionResult {
request_id: plan.request_id.clone(),
@@ -1696,8 +1725,21 @@ async fn execute_execution_runtime_sync_impl(
headers.insert("content-type".to_string(), "application/json".to_string());
}
}
let (result_error_type, result_error_message) =
let (mut result_error_type, mut result_error_message) =
execution_error_details(result.error.as_ref(), body_json.as_ref());
if result.status_code < 400 && body_json.is_none() {
if let Some(error_body_json) =
extract_provider_private_stream_error_body(report_context.as_ref(), &body_bytes)
{
result.status_code =
resolve_local_sync_error_status_code(result.status_code, &error_body_json);
let (private_error_type, private_error_message) =
provider_private_error_details(&error_body_json);
result_error_type = private_error_type.or(result_error_type);
result_error_message = private_error_message.or(result_error_message);
body_json = Some(error_body_json);
}
}
let local_failover_response_text = local_failover_response_text(
body_json.as_ref(),
&body_bytes,

View File

@@ -25,8 +25,10 @@ use serde_json::json;
use serde_json::Value;
use thiserror::Error;
use crate::ai_serving::api::extract_provider_private_stream_error_body;
#[cfg(test)]
use crate::execution_runtime::remote_compat::execute_sync_plan_via_remote_execution_runtime;
use crate::execution_runtime::windsurf::maybe_execute_windsurf_sync;
use crate::frontdoor_loop_guard::{
configured_gateway_frontdoor_base_url, gateway_frontdoor_self_loop_guard_error,
};
@@ -232,26 +234,8 @@ impl DirectSyncExecutionRuntime {
let elapsed_ms = started_at.elapsed().as_millis() as u64;
let upstream_bytes = body_bytes.len() as u64;
let body = if body_bytes.is_empty() {
None
} else if plan.stream {
Some(ResponseBody {
json_body: None,
body_bytes_b64: Some(base64::engine::general_purpose::STANDARD.encode(&body_bytes)),
})
} else if response_body_is_json(&headers, &decoded_body_bytes) {
let body_json: Value = serde_json::from_slice(&decoded_body_bytes)
.map_err(ExecutionRuntimeTransportError::InvalidJson)?;
Some(ResponseBody {
json_body: Some(body_json),
body_bytes_b64: None,
})
} else {
Some(ResponseBody {
json_body: None,
body_bytes_b64: Some(base64::engine::general_purpose::STANDARD.encode(&body_bytes)),
})
};
let body =
build_execution_response_body(&headers, &body_bytes, &decoded_body_bytes, plan.stream)?;
Ok(ExecutionResult {
request_id: plan.request_id.clone(),
@@ -347,6 +331,11 @@ pub(crate) async fn execute_sync_plan_with_report_context(
}
let _ = trace_id;
match maybe_execute_windsurf_sync(state, plan, None).await {
Ok(Some(result)) => return Ok(result),
Ok(None) => {}
Err(err) => return Err(GatewayError::Internal(err.to_string())),
}
match DirectSyncExecutionRuntime::new().execute_sync(plan).await {
Ok(result) => {
record_manual_proxy_request_outcome(state, plan, result.status_code).await;
@@ -567,26 +556,8 @@ async fn execute_sync_plan_via_local_tunnel(
);
}
let body = if body_bytes.is_empty() {
None
} else if plan.stream {
Some(ResponseBody {
json_body: None,
body_bytes_b64: Some(base64::engine::general_purpose::STANDARD.encode(&body_bytes)),
})
} else if response_body_is_json(&headers, &decoded_body_bytes) {
let body_json: Value = serde_json::from_slice(&decoded_body_bytes)
.map_err(ExecutionRuntimeTransportError::InvalidJson)?;
Some(ResponseBody {
json_body: Some(body_json),
body_bytes_b64: None,
})
} else {
Some(ResponseBody {
json_body: None,
body_bytes_b64: Some(base64::engine::general_purpose::STANDARD.encode(&body_bytes)),
})
};
let body =
build_execution_response_body(&headers, &body_bytes, &decoded_body_bytes, plan.stream)?;
Ok(ExecutionResult {
request_id: plan.request_id.clone(),
@@ -1470,17 +1441,63 @@ pub(crate) fn decode_response_body_bytes(
}
pub(crate) fn response_body_is_json(headers: &BTreeMap<String, String>, body_bytes: &[u8]) -> bool {
if headers
let content_type = headers
.get("content-type")
.map(|value| value.to_ascii_lowercase())
.is_some_and(|value| value.contains("json"))
.unwrap_or_default();
if content_type.contains("application/connect+json")
|| content_type.contains("application/connect+proto")
{
return false;
}
if content_type.contains("json") {
return true;
}
serde_json::from_slice::<Value>(body_bytes).is_ok()
}
pub(crate) fn build_execution_response_body(
headers: &BTreeMap<String, String>,
body_bytes: &[u8],
decoded_body_bytes: &[u8],
stream: bool,
) -> Result<Option<ResponseBody>, ExecutionRuntimeTransportError> {
if body_bytes.is_empty() {
return Ok(None);
}
if let Some(body_json) = extract_provider_private_stream_error_body(None, decoded_body_bytes)
.or_else(|| extract_provider_private_stream_error_body(None, body_bytes))
{
return Ok(Some(ResponseBody {
json_body: Some(body_json),
body_bytes_b64: None,
}));
}
if stream {
return Ok(Some(ResponseBody {
json_body: None,
body_bytes_b64: Some(base64::engine::general_purpose::STANDARD.encode(body_bytes)),
}));
}
if response_body_is_json(headers, decoded_body_bytes) {
let body_json: Value = serde_json::from_slice(decoded_body_bytes)
.map_err(ExecutionRuntimeTransportError::InvalidJson)?;
return Ok(Some(ResponseBody {
json_body: Some(body_json),
body_bytes_b64: None,
}));
}
Ok(Some(ResponseBody {
json_body: None,
body_bytes_b64: Some(base64::engine::general_purpose::STANDARD.encode(body_bytes)),
}))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
@@ -1505,11 +1522,12 @@ mod tests {
use tokio::sync::watch;
use super::{
build_browser_wreq_client, build_client, build_request_headers, execute_sync_plan,
record_manual_proxy_request_failure, record_manual_proxy_request_outcome,
record_manual_proxy_request_success, record_manual_proxy_stream_error,
resolve_execution_transport_controls, DirectSyncExecutionRuntime,
ExecutionRuntimeTransportError, ExecutionTransportControls,
build_browser_wreq_client, build_client, build_execution_response_body,
build_request_headers, execute_sync_plan, record_manual_proxy_request_failure,
record_manual_proxy_request_outcome, record_manual_proxy_request_success,
record_manual_proxy_stream_error, resolve_execution_transport_controls,
response_body_is_json, DirectSyncExecutionRuntime, ExecutionRuntimeTransportError,
ExecutionTransportControls,
};
use crate::constants::{
EXECUTION_RUNTIME_LOOP_GUARD_HEADER, EXECUTION_RUNTIME_LOOP_GUARD_VIA_TOKEN,
@@ -2764,6 +2782,41 @@ mod tests {
));
}
#[test]
fn connect_json_response_is_not_treated_as_plain_json() {
let headers = BTreeMap::from([(
"content-type".to_string(),
"application/connect+json".to_string(),
)]);
let body = [2, 0, 0, 0, 2, b'{', b'}'];
assert!(!response_body_is_json(&headers, &body));
}
#[test]
fn connect_json_error_response_is_decoded_for_stream_sync_body() {
let headers = BTreeMap::from([(
"content-type".to_string(),
"application/connect+json".to_string(),
)]);
let payload = br#"{"error":{"code":"resource_exhausted","message":"quota exhausted"}}"#;
let mut body_bytes = vec![2];
body_bytes.extend_from_slice(&(payload.len() as u32).to_be_bytes());
body_bytes.extend_from_slice(payload);
let body = build_execution_response_body(&headers, &body_bytes, &body_bytes, true)
.expect("body should build")
.expect("body should be present");
assert_eq!(
body.json_body
.as_ref()
.and_then(|value| value.pointer("/error/code")),
Some(&json!("resource_exhausted"))
);
assert!(body.body_bytes_b64.is_none());
}
#[tokio::test]
async fn direct_sync_execution_runtime_compresses_json_body_when_requested() {
let listener = crate::test_support::bind_loopback_listener()

File diff suppressed because it is too large Load Diff

View File

@@ -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(

View File

@@ -1,6 +1,6 @@
use crate::handlers::admin::request::AdminAppState;
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
};
use std::time::{SystemTime, UNIX_EPOCH};

View File

@@ -25,10 +25,15 @@ use crate::handlers::admin::provider::oauth::runtime::{
use crate::handlers::admin::provider::oauth::state::{
admin_provider_oauth_template, exchange_admin_provider_oauth_refresh_token,
};
use crate::handlers::admin::provider::shared::support::ADMIN_PROVIDER_OAUTH_DATA_UNAVAILABLE_DETAIL;
use crate::handlers::admin::request::{AdminAppState, AdminProviderOAuthTemplate};
use crate::GatewayError;
use aether_admin::provider::oauth::parse_admin_provider_oauth_kiro_batch_import_entries;
use aether_contracts::ProxySnapshot;
use aether_oauth::core::OAuthError;
use aether_oauth::provider::{
ProviderOAuthImportInput, ProviderOAuthService, ProviderOAuthTransportContext,
};
use serde_json::{json, Map, Value};
struct AdminProviderOAuthResolvedBatchImport {
@@ -37,6 +42,16 @@ struct AdminProviderOAuthResolvedBatchImport {
expires_at: Option<u64>,
}
fn sanitize_windsurf_batch_import_error(error: &OAuthError) -> String {
match error {
OAuthError::InvalidRequest(_) => "Windsurf 凭据验证失败: 请求参数无效".to_string(),
OAuthError::HttpStatus { status_code, .. } => {
format!("Windsurf 凭据验证失败: HTTP {status_code}")
}
_ => "Windsurf 凭据验证失败".to_string(),
}
}
pub(super) fn estimate_admin_provider_oauth_batch_import_total(
provider_type: &str,
raw_credentials: &str,
@@ -98,6 +113,61 @@ async fn resolve_admin_provider_oauth_batch_import_tokens(
.map(str::trim)
.filter(|value| !value.is_empty());
if provider_type.eq_ignore_ascii_case("windsurf") {
let token_for_import = refresh_token.or(access_token);
let ctx = ProviderOAuthTransportContext {
provider_id: String::new(),
provider_type: provider_type.to_string(),
endpoint_id: None,
key_id: None,
auth_type: Some("oauth".to_string()),
decrypted_api_key: None,
decrypted_auth_config: None,
provider_config: None,
endpoint_config: None,
key_config: None,
network: aether_oauth::network::OAuthNetworkContext::provider_operation(
request_proxy.clone(),
),
};
let executor = crate::oauth::GatewayOAuthHttpExecutor::new(*state);
let result = ProviderOAuthService::with_builtin_adapters()
.import_credentials(
&executor,
&ctx,
ProviderOAuthImportInput {
provider_type: provider_type.to_string(),
name: entry
.raw_credentials
.as_ref()
.and_then(|raw| raw.get("name"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
refresh_token: token_for_import.map(ToOwned::to_owned),
raw_credentials: entry.raw_credentials.clone(),
network: ctx.network.clone(),
},
)
.await
.map_err(|error| sanitize_windsurf_batch_import_error(&error))?;
let access_token = result.token_set.access_token.trim().to_string();
if access_token.is_empty() {
return Err("Windsurf 凭据验证返回缺少 apiKey/sessionToken".to_string());
}
let auth_config = result
.auth_config
.as_object()
.cloned()
.ok_or_else(|| "Windsurf 凭据验证返回缺少 auth_config".to_string())?;
return Ok(AdminProviderOAuthResolvedBatchImport {
access_token,
auth_config,
expires_at: result.token_set.expires_at_unix_secs,
});
}
if let Some(refresh_token) = refresh_token {
let Some(template) = template else {
if provider_type_supports_access_token_import(provider_type) {
@@ -228,6 +298,28 @@ pub(super) async fn execute_admin_provider_oauth_batch_import(
};
let template = admin_provider_oauth_template(provider_type);
if template.is_none()
&& !provider_type.eq_ignore_ascii_case("windsurf")
&& !provider_type_supports_access_token_import(provider_type)
{
return Ok(AdminProviderOAuthBatchImportOutcome {
total: entries.len(),
success: 0,
failed: entries.len(),
results: entries
.iter()
.enumerate()
.map(|(index, _)| {
json!({
"index": index,
"status": "error",
"error": ADMIN_PROVIDER_OAUTH_DATA_UNAVAILABLE_DETAIL,
"replaced": false,
})
})
.collect(),
});
}
let endpoint_resolution =
resolve_provider_oauth_runtime_endpoints(state, &provider, provider_type).await?;
@@ -251,6 +343,25 @@ pub(super) async fn execute_admin_provider_oauth_batch_import(
let mut failed = 0usize;
for (index, entry) in entries.iter().enumerate() {
if let Some(error) = entry.parse_error.as_ref() {
failed += 1;
results.push(json!({
"index": index,
"status": "error",
"error": error,
"replaced": false,
}));
maybe_report_admin_provider_oauth_batch_import_progress(
&mut progress,
entries.len(),
success,
failed,
&results,
)
.await;
continue;
}
let resolved_import = match resolve_admin_provider_oauth_batch_import_tokens(
state,
template,
@@ -418,3 +529,33 @@ pub(super) async fn execute_admin_provider_oauth_batch_import(
results,
})
}
#[cfg(test)]
mod tests {
use super::sanitize_windsurf_batch_import_error;
use aether_oauth::core::OAuthError;
#[test]
fn windsurf_batch_import_error_redacts_http_body() {
let error = OAuthError::HttpStatus {
status_code: 401,
body_excerpt: "sessionToken=devin-session-token$secret".to_string(),
};
let detail = sanitize_windsurf_batch_import_error(&error);
assert_eq!(detail, "Windsurf 凭据验证失败: HTTP 401");
assert!(!detail.contains("devin-session-token$secret"));
}
#[test]
fn windsurf_batch_import_error_redacts_provider_detail() {
let error = OAuthError::invalid_response("apiKey=sk-secret token=secret-token");
let detail = sanitize_windsurf_batch_import_error(&error);
assert_eq!(detail, "Windsurf 凭据验证失败");
assert!(!detail.contains("sk-secret"));
assert!(!detail.contains("secret-token"));
}
}

View File

@@ -8,7 +8,7 @@ use super::parse::{
};
use crate::handlers::admin::provider::oauth::errors::build_internal_control_error_response;
use crate::handlers::admin::provider::oauth::state::{
build_admin_provider_oauth_backend_unavailable_response,
admin_provider_oauth_template, build_admin_provider_oauth_backend_unavailable_response,
is_fixed_provider_type_for_provider_oauth,
};
use crate::handlers::admin::provider::shared::paths::admin_provider_oauth_batch_import_provider_id;
@@ -60,6 +60,13 @@ pub(in super::super) async fn handle_admin_provider_oauth_batch_import(
"该 Provider 不是固定类型,无法使用 provider-oauth",
));
}
if provider_type != "kiro"
&& provider_type != "windsurf"
&& admin_provider_oauth_template(&provider_type).is_none()
{
return Ok(build_admin_provider_oauth_backend_unavailable_response());
}
let total = estimate_admin_provider_oauth_batch_import_total(
&provider_type,
payload.credentials.as_str(),

View File

@@ -19,8 +19,10 @@ pub(super) struct AdminProviderOAuthBatchImportRequest {
#[derive(Debug, Clone)]
pub(super) struct AdminProviderOAuthBatchImportEntry {
pub parse_error: Option<String>,
pub refresh_token: Option<String>,
pub access_token: Option<String>,
pub raw_credentials: Option<serde_json::Value>,
pub expires_at: Option<u64>,
pub account_id: Option<String>,
pub account_user_id: Option<String>,
@@ -141,8 +143,10 @@ fn extract_admin_provider_oauth_batch_import_entry(
access_token.as_deref(),
);
Some(AdminProviderOAuthBatchImportEntry {
parse_error: None,
refresh_token,
access_token,
raw_credentials: None,
expires_at: None,
account_id: None,
account_user_id: None,
@@ -160,6 +164,8 @@ fn extract_admin_provider_oauth_batch_import_entry(
}
}
serde_json::Value::Object(object) => {
let is_grok = provider_type.trim().eq_ignore_ascii_case("grok");
let is_windsurf = provider_type.trim().eq_ignore_ascii_case("windsurf");
let refresh_token = coerce_admin_provider_oauth_import_str(
object
.get("refresh_token")
@@ -170,12 +176,8 @@ fn extract_admin_provider_oauth_batch_import_entry(
.get("access_token")
.or_else(|| object.get("accessToken")),
);
let grok_token_alias = if provider_type.trim().eq_ignore_ascii_case("grok") {
object.get("token")
} else {
None
};
let grok_cookie = if provider_type.trim().eq_ignore_ascii_case("grok") {
let grok_token_alias = if is_grok { object.get("token") } else { None };
let grok_cookie = if is_grok {
coerce_admin_provider_oauth_import_str(
object.get("cookie").or_else(|| object.get("cookieHeader")),
)
@@ -198,9 +200,43 @@ fn extract_admin_provider_oauth_batch_import_entry(
refresh_token.as_deref(),
access_token.as_deref().or(session_token.as_deref()),
);
if refresh_token.is_none() && access_token.is_none() {
let windsurf_api_key = is_windsurf
.then(|| {
coerce_admin_provider_oauth_import_str(
object.get("api_key").or_else(|| object.get("apiKey")),
)
})
.flatten();
let windsurf_token = is_windsurf
.then(|| {
coerce_admin_provider_oauth_import_str(
object
.get("token")
.or_else(|| object.get("auth_token"))
.or_else(|| object.get("authToken")),
)
})
.flatten();
let windsurf_password = is_windsurf
.then(|| coerce_admin_provider_oauth_import_str(object.get("password")))
.flatten();
let raw_credentials = if is_windsurf
&& (windsurf_api_key.is_some()
|| windsurf_token.is_some()
|| windsurf_password.is_some())
{
Some(item.clone())
} else {
None
};
if refresh_token.is_none() && access_token.is_none() && raw_credentials.is_none() {
return None;
}
let refresh_token = if is_windsurf {
refresh_token.or(windsurf_api_key).or(windsurf_token)
} else {
refresh_token
};
let expires_at =
json_u64_value(object.get("expires_at").or_else(|| object.get("expiresAt")));
let account_id = coerce_admin_provider_oauth_import_str(
@@ -285,8 +321,10 @@ fn extract_admin_provider_oauth_batch_import_entry(
.or_else(|| object.get("impersonate")),
);
Some(AdminProviderOAuthBatchImportEntry {
parse_error: None,
refresh_token,
access_token,
raw_credentials,
expires_at,
account_id,
account_user_id,
@@ -316,14 +354,17 @@ pub(super) fn parse_admin_provider_oauth_batch_import_entries(
}
if raw.starts_with('[') {
if let Ok(serde_json::Value::Array(items)) = serde_json::from_str::<serde_json::Value>(raw)
{
return items
.iter()
.filter_map(|item| {
extract_admin_provider_oauth_batch_import_entry(provider_type, item)
})
.collect();
match serde_json::from_str::<serde_json::Value>(raw) {
Ok(serde_json::Value::Array(items)) => {
return items
.iter()
.filter_map(|item| {
extract_admin_provider_oauth_batch_import_entry(provider_type, item)
})
.collect();
}
Ok(_) => {}
Err(error) => return vec![parse_error_entry(format!("JSON 数组解析失败: {error}"))],
}
}
@@ -340,23 +381,61 @@ pub(super) fn parse_admin_provider_oauth_batch_import_entries(
raw.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| {
if line.starts_with('{') {
return serde_json::from_str::<serde_json::Value>(line)
.ok()
.and_then(|value| {
extract_admin_provider_oauth_batch_import_entry(provider_type, &value)
});
.filter_map(|token| {
if is_json_like_batch_line(token) {
match serde_json::from_str::<serde_json::Value>(token) {
Ok(value @ serde_json::Value::Object(_)) => {
return extract_admin_provider_oauth_batch_import_entry(
provider_type,
&value,
);
}
Ok(_) => {
return Some(parse_error_entry(
"JSON 行必须是账号对象,不能作为 raw token 导入".to_string(),
));
}
Err(error) => {
return Some(parse_error_entry(format!("JSON 行解析失败: {error}")));
}
}
}
extract_admin_provider_oauth_batch_import_entry(
provider_type,
&serde_json::Value::String(line.to_string()),
&serde_json::Value::String(token.to_string()),
)
})
.collect()
}
fn parse_error_entry(error: String) -> AdminProviderOAuthBatchImportEntry {
AdminProviderOAuthBatchImportEntry {
parse_error: Some(error),
refresh_token: None,
access_token: None,
raw_credentials: None,
expires_at: None,
account_id: None,
account_user_id: None,
plan_type: None,
pool_tier: None,
user_id: None,
email: None,
account_name: None,
sso_rw_token: None,
cf_cookies: None,
cf_clearance: None,
user_agent: None,
browser_profile: None,
}
}
fn is_json_like_batch_line(line: &str) -> bool {
let line = line.trim_start();
line.starts_with('{') || line.starts_with('[')
}
pub(super) fn apply_admin_provider_oauth_batch_import_hints(
provider_type: &str,
entry: &AdminProviderOAuthBatchImportEntry,
@@ -633,4 +712,115 @@ mod tests {
assert_eq!(entries[0].user_id.as_deref(), Some("user-1"));
assert_eq!(entries[0].pool_tier.as_deref(), Some("heavy"));
}
#[test]
fn parses_windsurf_json_credentials_for_native_import() {
let entries = parse_admin_provider_oauth_batch_import_entries(
"windsurf",
r#"[
{"api_key":"devin-session-token$abc","email":"a@example.com"},
{"token":"firebase-id-token","name":"Browser Login"},
{"email":"b@example.com","password":"secret"},
{"access_token":"devin-session-token$alias","email":"c@example.com"}
]"#,
);
assert_eq!(entries.len(), 4);
assert_eq!(
entries[0].refresh_token.as_deref(),
Some("devin-session-token$abc")
);
assert_eq!(entries[0].email.as_deref(), Some("a@example.com"));
assert_eq!(
entries[0]
.raw_credentials
.as_ref()
.and_then(|value| value.get("api_key")),
Some(&json!("devin-session-token$abc"))
);
assert_eq!(
entries[1]
.raw_credentials
.as_ref()
.and_then(|value| value.get("token")),
Some(&json!("firebase-id-token"))
);
assert_eq!(
entries[2]
.raw_credentials
.as_ref()
.and_then(|value| value.get("password")),
Some(&json!("secret"))
);
assert_eq!(
entries[3].access_token.as_deref(),
Some("devin-session-token$alias")
);
}
#[test]
fn parses_windsurf_json_lines_credentials_for_native_import() {
let entries = parse_admin_provider_oauth_batch_import_entries(
"windsurf",
r#"{"api_key":"devin-session-token$abc","email":"a@example.com"}
{"token":"firebase-id-token","name":"Browser Login"}
{"email":"b@example.com","password":"secret"}"#,
);
assert_eq!(entries.len(), 3);
assert_eq!(
entries[0]
.raw_credentials
.as_ref()
.and_then(|value| value.get("api_key")),
Some(&json!("devin-session-token$abc"))
);
assert_eq!(
entries[1]
.raw_credentials
.as_ref()
.and_then(|value| value.get("token")),
Some(&json!("firebase-id-token"))
);
assert_eq!(
entries[2]
.raw_credentials
.as_ref()
.and_then(|value| value.get("password")),
Some(&json!("secret"))
);
}
#[test]
fn invalid_json_line_is_parse_error_not_token() {
let entries = parse_admin_provider_oauth_batch_import_entries(
"windsurf",
r#"{"email":"b@example.com","password":"secret""#,
);
assert_eq!(entries.len(), 1);
assert!(entries[0].parse_error.is_some());
assert!(entries[0].refresh_token.is_none());
assert!(entries[0].access_token.is_none());
assert!(entries[0].raw_credentials.is_none());
}
#[test]
fn json_like_line_after_token_is_parse_error_not_token() {
let entries = parse_admin_provider_oauth_batch_import_entries(
"windsurf",
"devin-session-token$abc\n[not-json",
);
assert_eq!(entries.len(), 2);
assert!(entries[0].parse_error.is_none());
assert_eq!(
entries[0].refresh_token.as_deref(),
Some("devin-session-token$abc")
);
assert!(entries[1].parse_error.is_some());
assert!(entries[1].refresh_token.is_none());
assert!(entries[1].access_token.is_none());
assert!(entries[1].raw_credentials.is_none());
}
}

View File

@@ -10,7 +10,7 @@ use super::progress::{
};
use crate::handlers::admin::provider::oauth::errors::build_internal_control_error_response;
use crate::handlers::admin::provider::oauth::state::{
build_admin_provider_oauth_backend_unavailable_response,
admin_provider_oauth_template, build_admin_provider_oauth_backend_unavailable_response,
is_fixed_provider_type_for_provider_oauth,
};
use crate::handlers::admin::provider::shared::paths::admin_provider_oauth_batch_import_task_provider_id;
@@ -124,6 +124,13 @@ pub(in super::super) async fn handle_admin_provider_oauth_start_batch_import_tas
"该 Provider 不是固定类型,无法使用 provider-oauth",
));
}
if provider_type != "kiro"
&& provider_type != "windsurf"
&& admin_provider_oauth_template(&provider_type).is_none()
{
return Ok(build_admin_provider_oauth_backend_unavailable_response());
}
let total = estimate_admin_provider_oauth_batch_import_total(
&provider_type,
payload.credentials.as_str(),

View File

@@ -12,6 +12,7 @@ use crate::GatewayError;
use aether_data::repository::provider_oauth::{
StoredAdminProviderOAuthDeviceSession, KIRO_DEVICE_AUTH_SESSION_TTL_BUFFER_SECS,
};
use aether_oauth::provider::{ProviderOAuthService, ProviderOAuthTransportContext};
use axum::{
body::{Body, Bytes},
http,
@@ -28,6 +29,8 @@ const KIRO_SOCIAL_MANUAL_CALLBACK_PORT: u16 = 49153;
const KIRO_SOCIAL_ALLOWED_CALLBACK_PORTS: &[u16] = &[
3128, 4649, 6588, 8008, 9091, 49153, 50153, 51153, 52153, 53153,
];
const WINDSURF_BROWSER_AUTH_EXPIRES_IN_SECS: u64 = 600;
const WINDSURF_BROWSER_AUTH_POLL_INTERVAL_SECS: u64 = 5;
fn normalize_kiro_device_auth_type(raw: Option<&str>) -> String {
match raw
@@ -119,6 +122,26 @@ fn build_kiro_social_authorization_url(
)
}
fn build_windsurf_authorization_url(authorize_url: &str, login_option: &str) -> String {
let login_option = login_option.trim();
if login_option.is_empty() {
return authorize_url.to_string();
}
if let Ok(mut url) = Url::parse(authorize_url) {
url.query_pairs_mut()
.append_pair("login_option", login_option);
return url.to_string();
}
let separator = if authorize_url.contains('?') {
'&'
} else {
'?'
};
let mut serializer = form_urlencoded::Serializer::new(String::new());
serializer.append_pair("login_option", login_option);
format!("{authorize_url}{separator}{}", serializer.finish())
}
pub(super) async fn handle_admin_provider_oauth_device_authorize(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
@@ -163,14 +186,14 @@ pub(super) async fn handle_admin_provider_oauth_device_authorize(
));
};
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
if provider_type != "kiro" {
if provider_type != "kiro" && provider_type != "windsurf" {
return Ok(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"设备授权仅支持 Kiro provider",
"设备授权仅支持 Kiro / Windsurf provider",
));
}
let endpoint_resolution =
resolve_provider_oauth_runtime_endpoints(state, &provider, "kiro").await?;
resolve_provider_oauth_runtime_endpoints(state, &provider, &provider_type).await?;
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
let request_proxy = state
.resolve_admin_provider_oauth_operation_proxy_snapshot(
@@ -184,6 +207,103 @@ pub(super) async fn handle_admin_provider_oauth_device_authorize(
)
.await;
if provider_type == "windsurf" {
let session_id = generate_provider_oauth_nonce();
let login_option = payload
.login_option
.as_deref()
.or(payload.auth_type.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("default")
.to_ascii_lowercase();
let ctx = ProviderOAuthTransportContext {
provider_id: provider_id.clone(),
provider_type: provider_type.clone(),
endpoint_id: runtime_endpoint
.as_ref()
.map(|endpoint| endpoint.id.clone()),
key_id: None,
auth_type: Some("oauth".to_string()),
decrypted_api_key: None,
decrypted_auth_config: None,
provider_config: provider.config.clone(),
endpoint_config: runtime_endpoint
.as_ref()
.and_then(|endpoint| endpoint.config.clone()),
key_config: None,
network: aether_oauth::network::OAuthNetworkContext::provider_operation(
request_proxy.clone(),
),
};
let mut authorization = match ProviderOAuthService::with_builtin_adapters()
.build_authorize_url(&ctx, &session_id, None)
{
Ok(authorization) => authorization,
Err(error) => {
return Ok(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
format!("Windsurf 授权 URL 构建失败: {error}"),
));
}
};
authorization.authorize_url =
build_windsurf_authorization_url(&authorization.authorize_url, &login_option);
let now_unix_secs = current_unix_secs();
let session = StoredAdminProviderOAuthDeviceSession {
provider_id: provider_id.clone(),
region: String::new(),
client_id: String::new(),
client_secret: String::new(),
device_code: String::new(),
auth_type: Some("browser".to_string()),
social_provider: Some(login_option.clone()),
code_verifier: None,
redirect_uri: Some("show-auth-token".to_string()),
machine_id: Some(uuid::Uuid::new_v4().to_string().to_ascii_lowercase()),
interval: WINDSURF_BROWSER_AUTH_POLL_INTERVAL_SECS,
expires_at_unix_secs: now_unix_secs
.saturating_add(WINDSURF_BROWSER_AUTH_EXPIRES_IN_SECS),
status: "pending".to_string(),
proxy_node_id: payload
.proxy_node_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
created_at_unix_ms: now_unix_secs,
key_id: None,
email: None,
replaced: false,
error_msg: None,
};
if let Err(response) = state
.save_provider_oauth_device_session(
&session_id,
&session,
WINDSURF_BROWSER_AUTH_EXPIRES_IN_SECS
.saturating_add(KIRO_DEVICE_AUTH_SESSION_TTL_BUFFER_SECS),
)
.await
{
return Ok(response);
}
return Ok(Json(json!({
"session_id": session_id,
"user_code": "",
"verification_uri": "https://windsurf.com/windsurf/signin",
"verification_uri_complete": authorization.authorize_url,
"expires_in": WINDSURF_BROWSER_AUTH_EXPIRES_IN_SECS,
"interval": WINDSURF_BROWSER_AUTH_POLL_INTERVAL_SECS,
"auth_type": "browser",
"login_option": login_option,
"redirect_uri": "show-auth-token",
"callback_required": true,
}))
.into_response());
}
let auth_type = normalize_kiro_device_auth_type(payload.auth_type.as_deref());
if let Some(social_provider) = kiro_social_provider_id(&auth_type) {
let redirect_uri = match normalize_kiro_social_redirect_uri(payload.redirect_uri.as_deref())
@@ -394,3 +514,27 @@ pub(super) async fn handle_admin_provider_oauth_device_authorize(
}))
.into_response())
}
#[cfg(test)]
mod tests {
use super::build_windsurf_authorization_url;
#[test]
fn windsurf_authorization_url_includes_login_option() {
let url = build_windsurf_authorization_url(
"https://windsurf.com/windsurf/signin?state=session-1",
"github",
);
let parsed = url::Url::parse(&url).expect("url should parse");
let params = parsed
.query_pairs()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(params.get("state").map(String::as_str), Some("session-1"));
assert_eq!(
params.get("login_option").map(String::as_str),
Some("github")
);
}
}

View File

@@ -26,6 +26,9 @@ use aether_data::repository::provider_oauth::StoredAdminProviderOAuthDeviceSessi
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};
use aether_oauth::provider::{
ProviderOAuthImportInput, ProviderOAuthService, ProviderOAuthTransportContext,
};
use axum::{
body::{Body, Bytes},
http,
@@ -86,6 +89,99 @@ fn kiro_social_poll_error_response(error: impl Into<String>) -> Response<Body> {
.into_response()
}
fn windsurf_browser_poll_error_response(error: impl Into<String>) -> Response<Body> {
Json(json!({
"status": "error",
"error": error.into(),
"replaced": false,
}))
.into_response()
}
fn sanitize_windsurf_browser_poll_detail(detail: impl AsRef<str>) -> String {
let detail = detail.as_ref().trim();
if detail.is_empty() {
return "-".to_string();
}
if contains_windsurf_sensitive_marker(detail) {
"[REDACTED upstream error body]".to_string()
} else {
detail.chars().take(500).collect()
}
}
fn sanitize_windsurf_browser_poll_callback_error(error: &str, description: &str) -> String {
let error = sanitize_windsurf_browser_poll_error_code(error);
let description = sanitize_windsurf_browser_poll_detail(description);
format!("{error}: {description}")
}
fn sanitize_windsurf_browser_poll_error_code(error: &str) -> String {
let error = error.trim();
if !error.is_empty()
&& error.len() <= 80
&& error
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
{
return error.to_string();
}
sanitize_windsurf_browser_poll_detail(error)
}
fn sanitize_windsurf_browser_poll_oauth_error(error: &aether_oauth::core::OAuthError) -> String {
match error {
aether_oauth::core::OAuthError::InvalidRequest(_) => {
"Windsurf token 验证失败: 请求参数无效".to_string()
}
aether_oauth::core::OAuthError::HttpStatus { status_code, .. } => {
format!("Windsurf token 验证失败: HTTP {status_code}")
}
_ => "Windsurf token 验证失败".to_string(),
}
}
fn contains_windsurf_sensitive_marker(value: &str) -> bool {
let lowered = value.to_ascii_lowercase();
[
"token",
"api_key",
"apikey",
"sessiontoken",
"firebase_id_token",
"idtoken",
"authorization",
"password",
"secret",
"devin-session-token$",
]
.iter()
.any(|marker| lowered.contains(marker))
|| value.contains("sk-")
}
fn secret_fingerprint(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() {
return None;
}
use sha2::{Digest, Sha256};
let digest = Sha256::digest(value.as_bytes());
Some(
digest[..8]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
}
fn insert_secret_fingerprint(target: &mut serde_json::Map<String, Value>, key: &str, secret: &str) {
if let Some(fingerprint) = secret_fingerprint(secret) {
target.insert(key.to_string(), json!(fingerprint));
}
}
fn kiro_social_provider_from_login_option(login_option: Option<&str>) -> Option<&'static str> {
match login_option
.map(str::trim)
@@ -322,8 +418,9 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
"Provider 不存在",
));
};
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
let endpoint_resolution =
resolve_provider_oauth_runtime_endpoints(state, &provider, "kiro").await?;
resolve_provider_oauth_runtime_endpoints(state, &provider, &provider_type).await?;
let endpoints = endpoint_resolution.endpoints;
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
let request_proxy = state
@@ -338,6 +435,20 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
)
.await;
if provider_type == "windsurf" {
return handle_admin_provider_oauth_windsurf_browser_device_poll(
state,
&provider,
&endpoints,
request_proxy,
session_id,
session,
payload.callback_url.as_deref(),
payload.token.as_deref(),
)
.await;
}
if kiro_device_session_is_social(&session) {
return handle_admin_provider_oauth_kiro_social_device_poll(
state,
@@ -630,6 +741,276 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
))
}
fn windsurf_raw_api_key(value: &str) -> Option<&str> {
let value = value.trim();
if value.starts_with("devin-session-token$") || value.starts_with("sk-") {
Some(value)
} else {
None
}
}
async fn handle_admin_provider_oauth_windsurf_browser_device_poll(
state: &AdminAppState<'_>,
provider: &StoredProviderCatalogProvider,
endpoints: &[StoredProviderCatalogEndpoint],
request_proxy: Option<ProxySnapshot>,
session_id: &str,
mut session: StoredAdminProviderOAuthDeviceSession,
callback_url: Option<&str>,
token: Option<&str>,
) -> Result<Response<Body>, GatewayError> {
let callback_url = callback_url
.map(str::trim)
.filter(|value| !value.is_empty());
let token = token.map(str::trim).filter(|value| !value.is_empty());
if callback_url.is_none() && token.is_none() {
return Ok(Json(json!({"status": "pending", "replaced": false})).into_response());
}
let mut social_provider = session
.social_provider
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let imported_token = if let Some(callback_url) = callback_url {
let callback_params = parse_provider_oauth_callback_params(callback_url);
if let Some(error) = callback_params.get("error").map(String::as_str) {
let error_description = callback_params
.get("error_description")
.map(String::as_str)
.unwrap_or("用户拒绝授权");
let sanitized_error =
sanitize_windsurf_browser_poll_callback_error(error, error_description);
session.status = "error".to_string();
session.error_msg = Some(sanitized_error.clone());
let _ = state
.save_provider_oauth_device_session(session_id, &session, 30)
.await;
return Ok(attach_admin_provider_oauth_device_poll_terminal_response(
session_id,
"error",
windsurf_browser_poll_error_response(sanitized_error),
));
}
let Some(callback_state) = callback_params
.get("state")
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Ok(windsurf_browser_poll_error_response("回调 URL 缺少 state"));
};
if callback_state != session_id {
return Ok(windsurf_browser_poll_error_response(
"回调 state 与会话不匹配",
));
}
if let Some(provider) = callback_params
.get("provider")
.or_else(|| callback_params.get("login_option"))
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
social_provider = Some(provider.to_string());
}
let Some(callback_token) = callback_params
.get("token")
.or_else(|| callback_params.get("auth_token"))
.or_else(|| callback_params.get("access_token"))
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Ok(windsurf_browser_poll_error_response("回调 URL 缺少 token"));
};
callback_token.to_string()
} else {
let token = token.unwrap_or_default();
if windsurf_raw_api_key(token).is_none() {
return Ok(windsurf_browser_poll_error_response(
"浏览器授权请提交包含 state 的回调 URL纯 token 请使用导入授权",
));
}
token.to_string()
};
let mut raw_credentials = serde_json::Map::new();
if windsurf_raw_api_key(&imported_token).is_some() {
raw_credentials.insert("api_key".to_string(), json!(imported_token));
} else {
raw_credentials.insert("token".to_string(), json!(imported_token));
}
if let Some(social_provider) = social_provider.as_ref() {
raw_credentials.insert("social_provider".to_string(), json!(social_provider));
}
let ctx = ProviderOAuthTransportContext {
provider_id: provider.id.clone(),
provider_type: provider.provider_type.clone(),
endpoint_id: None,
key_id: None,
auth_type: Some("oauth".to_string()),
decrypted_api_key: None,
decrypted_auth_config: None,
provider_config: provider.config.clone(),
endpoint_config: None,
key_config: None,
network: aether_oauth::network::OAuthNetworkContext::provider_operation(
request_proxy.clone(),
),
};
let executor = crate::oauth::GatewayOAuthHttpExecutor::new(*state);
let result = match ProviderOAuthService::with_builtin_adapters()
.import_credentials(
&executor,
&ctx,
ProviderOAuthImportInput {
provider_type: provider.provider_type.clone(),
name: None,
refresh_token: None,
raw_credentials: Some(Value::Object(raw_credentials)),
network: ctx.network.clone(),
},
)
.await
{
Ok(result) => result,
Err(error) => {
let sanitized_error = sanitize_windsurf_browser_poll_oauth_error(&error);
session.status = "error".to_string();
session.error_msg = Some(sanitized_error.clone());
let _ = state
.save_provider_oauth_device_session(session_id, &session, 30)
.await;
return Ok(attach_admin_provider_oauth_device_poll_terminal_response(
session_id,
"error",
windsurf_browser_poll_error_response(sanitized_error),
));
}
};
let access_token = result.token_set.access_token.trim().to_string();
if access_token.is_empty() {
return Ok(windsurf_browser_poll_error_response(
"Windsurf token 验证返回缺少 apiKey/sessionToken",
));
}
let mut auth_config = result.auth_config.as_object().cloned().unwrap_or_default();
auth_config.insert("provider_type".to_string(), json!("windsurf"));
auth_config.insert("auth_method".to_string(), json!("browser"));
if let Some(social_provider) = social_provider.as_ref() {
auth_config
.entry("social_provider".to_string())
.or_insert_with(|| json!(social_provider));
}
let duplicate = match state
.find_duplicate_provider_oauth_key(&provider.id, &auth_config, None)
.await
{
Ok(duplicate) => duplicate,
Err(detail) => {
return Ok(Json(json!({
"status": "error",
"error": detail,
"replaced": false,
}))
.into_response());
}
};
let api_formats = provider_oauth_active_api_formats(endpoints);
let key_proxy = provider_oauth_key_proxy_value(session.proxy_node_id.as_deref());
let expires_at = result.token_set.expires_at_unix_secs;
let email = auth_config
.get("email")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let mut replaced = false;
let persisted_key = if let Some(existing_key) = duplicate {
replaced = true;
match state
.update_existing_provider_oauth_catalog_key(
&existing_key,
&provider.provider_type,
&access_token,
&auth_config,
&api_formats,
key_proxy.clone(),
expires_at,
)
.await?
{
Some(key) => key,
None => {
return Ok(build_internal_control_error_response(
http::StatusCode::SERVICE_UNAVAILABLE,
"provider oauth write unavailable",
));
}
}
} else {
let key_name = email
.as_deref()
.map(|email| format!("windsurf_{email}"))
.unwrap_or_else(|| format!("windsurf_{}", current_unix_secs()));
match state
.create_provider_oauth_catalog_key(
&provider.id,
&provider.provider_type,
&key_name,
&access_token,
&auth_config,
&api_formats,
key_proxy,
expires_at,
)
.await?
{
Some(key) => key,
None => {
return Ok(build_internal_control_error_response(
http::StatusCode::SERVICE_UNAVAILABLE,
"provider oauth write unavailable",
));
}
}
};
spawn_provider_oauth_account_state_refresh_after_update(
state.cloned_app(),
provider.clone(),
persisted_key.id.clone(),
request_proxy.clone(),
);
session.status = "authorized".to_string();
session.key_id = Some(persisted_key.id.clone());
session.email = email.clone();
session.replaced = replaced;
session.error_msg = None;
let _ = state
.save_provider_oauth_device_session(session_id, &session, 60)
.await;
Ok(attach_admin_provider_oauth_device_poll_terminal_response(
session_id,
"authorized",
Json(json!({
"status": "authorized",
"key_id": persisted_key.id,
"email": email,
"replaced": replaced,
}))
.into_response(),
))
}
async fn handle_admin_provider_oauth_kiro_social_device_poll(
state: &AdminAppState<'_>,
provider: &StoredProviderCatalogProvider,
@@ -814,7 +1195,7 @@ async fn handle_admin_provider_oauth_kiro_social_device_poll(
.get("idToken")
.or_else(|| token_result.get("id_token")),
) {
auth_config_object.insert("id_token".to_string(), json!(id_token));
insert_secret_fingerprint(&mut auth_config_object, "id_token_fingerprint", &id_token);
}
if let Some(token_type) = json_non_empty_string(
token_result
@@ -921,3 +1302,18 @@ async fn handle_admin_provider_oauth_kiro_social_device_poll(
.into_response(),
))
}
#[cfg(test)]
mod tests {
#[test]
fn windsurf_browser_poll_callback_error_redacts_sensitive_values() {
let detail = super::sanitize_windsurf_browser_poll_callback_error(
"access_denied",
"bad token devin-session-token$secret and apiKey sk-secret",
);
assert_eq!(detail, "access_denied: [REDACTED upstream error body]");
assert!(!detail.contains("devin-session-token$secret"));
assert!(!detail.contains("sk-secret"));
}
}

View File

@@ -12,6 +12,7 @@ pub(super) struct AdminProviderOAuthDeviceAuthorizePayload {
#[serde(default = "default_kiro_device_region")]
pub(super) region: String,
pub(super) auth_type: Option<String>,
pub(super) login_option: Option<String>,
pub(super) redirect_uri: Option<String>,
pub(super) proxy_node_id: Option<String>,
}
@@ -20,6 +21,7 @@ pub(super) struct AdminProviderOAuthDeviceAuthorizePayload {
pub(super) struct AdminProviderOAuthDevicePollPayload {
pub(super) session_id: String,
pub(super) callback_url: Option<String>,
pub(super) token: Option<String>,
}
pub(super) fn attach_admin_provider_oauth_device_poll_terminal_response(

View File

@@ -25,6 +25,10 @@ use crate::handlers::admin::request::{
};
use crate::GatewayError;
use aether_contracts::ProxySnapshot;
use aether_oauth::core::OAuthError;
use aether_oauth::provider::{
ProviderOAuthImportInput, ProviderOAuthService, ProviderOAuthTransportContext,
};
use axum::{
body::Body,
http,
@@ -39,6 +43,49 @@ struct AdminProviderOAuthSingleImportTokens {
expires_at: Option<u64>,
}
fn sanitize_windsurf_import_error(error: &OAuthError) -> String {
match error {
OAuthError::InvalidRequest(_) => "Windsurf 凭据验证失败: 请求参数无效".to_string(),
OAuthError::HttpStatus { status_code, .. } => {
format!("Windsurf 凭据验证失败: HTTP {status_code}")
}
OAuthError::InvalidResponse(detail) => sanitize_windsurf_invalid_response_detail(detail)
.unwrap_or_else(|| "Windsurf 凭据验证失败".to_string()),
_ => "Windsurf 凭据验证失败".to_string(),
}
}
fn sanitize_windsurf_invalid_response_detail(detail: &str) -> Option<String> {
let detail = detail.trim();
if detail.eq_ignore_ascii_case("Auth1 response is not json") {
return Some("Windsurf 凭据验证失败: Auth1 响应无法解析".to_string());
}
if detail.eq_ignore_ascii_case("Auth1 response missing token") {
return Some("Windsurf 凭据验证失败: Auth1 响应缺少 token".to_string());
}
if detail.contains("WindsurfPostAuth response missing sessionToken")
|| detail.contains("WindsurfPostAuth response is not json")
|| (detail.contains("WindsurfPostAuth failed") && detail.contains("missing sessionToken"))
{
return Some("Windsurf 凭据验证失败: PostAuth 未返回 sessionToken".to_string());
}
if detail.contains("WindsurfPostAuth failed") {
return Some("Windsurf 凭据验证失败: PostAuth 失败".to_string());
}
None
}
fn import_payload_has_windsurf_credentials(
payload: &serde_json::Map<String, serde_json::Value>,
) -> bool {
import_payload_string(payload, "api_key", "apiKey").is_some()
|| import_payload_string_any(payload, &["token", "auth_token", "authToken"]).is_some()
|| import_payload_string(payload, "refresh_token", "refreshToken").is_some()
|| import_payload_string(payload, "access_token", "accessToken").is_some()
|| (import_payload_string_any(payload, &["email"]).is_some()
&& import_payload_string_any(payload, &["password"]).is_some())
}
fn import_payload_string(
payload: &serde_json::Map<String, serde_json::Value>,
snake_case: &str,
@@ -266,6 +313,70 @@ async fn resolve_admin_provider_oauth_single_import_tokens(
})
}
async fn resolve_admin_provider_oauth_windsurf_single_import_tokens(
state: &AdminAppState<'_>,
provider_type: &str,
name: Option<String>,
raw_payload: &serde_json::Map<String, serde_json::Value>,
refresh_token: Option<&str>,
request_proxy: Option<ProxySnapshot>,
) -> Result<AdminProviderOAuthSingleImportTokens, Response<Body>> {
let ctx = ProviderOAuthTransportContext {
provider_id: String::new(),
provider_type: provider_type.to_string(),
endpoint_id: None,
key_id: None,
auth_type: Some("oauth".to_string()),
decrypted_api_key: None,
decrypted_auth_config: None,
provider_config: None,
endpoint_config: None,
key_config: None,
network: aether_oauth::network::OAuthNetworkContext::provider_operation(
request_proxy.clone(),
),
};
let executor = crate::oauth::GatewayOAuthHttpExecutor::new(*state);
let service = ProviderOAuthService::with_builtin_adapters();
let result = service
.import_credentials(
&executor,
&ctx,
ProviderOAuthImportInput {
provider_type: provider_type.to_string(),
name,
refresh_token: refresh_token.map(ToOwned::to_owned),
raw_credentials: Some(serde_json::Value::Object(raw_payload.clone())),
network: ctx.network.clone(),
},
)
.await
.map_err(|error| {
build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
sanitize_windsurf_import_error(&error),
)
})?;
let access_token = result.token_set.access_token.trim().to_string();
if access_token.is_empty() {
return Err(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"Windsurf 凭据验证返回缺少 apiKey/sessionToken",
));
}
let auth_config = result.auth_config.as_object().cloned().ok_or_else(|| {
build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"Windsurf 凭据验证返回缺少 auth_config",
)
})?;
Ok(AdminProviderOAuthSingleImportTokens {
access_token,
auth_config,
expires_at: result.token_set.expires_at_unix_secs,
})
}
pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
@@ -370,19 +481,47 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
.await;
let key_proxy = provider_oauth_key_proxy_value(proxy_node_id.as_deref());
let resolved_import = match resolve_admin_provider_oauth_single_import_tokens(
state,
template,
&provider_type,
refresh_token_input.as_deref(),
access_token_input.as_deref(),
imported_expires_at,
request_proxy.clone(),
)
.await
{
Ok(value) => value,
Err(response) => return Ok(response),
let resolved_import = if provider_type == "windsurf" {
if !import_payload_has_windsurf_credentials(&raw_payload) {
return Ok(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"Windsurf 凭据不能为空",
));
}
match resolve_admin_provider_oauth_windsurf_single_import_tokens(
state,
&provider_type,
name.clone(),
&raw_payload,
refresh_token_input.as_deref(),
request_proxy.clone(),
)
.await
{
Ok(value) => value,
Err(response) => return Ok(response),
}
} else {
if refresh_token_input.is_none() && access_token_input.is_none() {
return Ok(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"Refresh Token 或 Access Token 不能为空",
));
}
match resolve_admin_provider_oauth_single_import_tokens(
state,
template,
&provider_type,
refresh_token_input.as_deref(),
access_token_input.as_deref(),
imported_expires_at,
request_proxy.clone(),
)
.await
{
Ok(value) => value,
Err(response) => return Ok(response),
}
};
let AdminProviderOAuthSingleImportTokens {
access_token,
@@ -480,3 +619,47 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
}))
.into_response())
}
#[cfg(test)]
mod tests {
use super::sanitize_windsurf_import_error;
use aether_oauth::core::OAuthError;
#[test]
fn windsurf_import_error_redacts_http_body() {
let error = OAuthError::HttpStatus {
status_code: 400,
body_excerpt: "token=secret-token password=secret-password".to_string(),
};
let detail = sanitize_windsurf_import_error(&error);
assert_eq!(detail, "Windsurf 凭据验证失败: HTTP 400");
assert!(!detail.contains("secret-token"));
assert!(!detail.contains("secret-password"));
}
#[test]
fn windsurf_import_error_redacts_invalid_response_detail() {
let error =
OAuthError::invalid_response("RegisterUser failed with firebase_id_token=secret-token");
let detail = sanitize_windsurf_import_error(&error);
assert_eq!(detail, "Windsurf 凭据验证失败");
assert!(!detail.contains("secret-token"));
assert!(!detail.contains("firebase_id_token"));
}
#[test]
fn windsurf_import_error_keeps_safe_post_auth_stage() {
let error = OAuthError::invalid_response("WindsurfPostAuth response missing sessionToken");
let detail = sanitize_windsurf_import_error(&error);
assert_eq!(
detail,
"Windsurf 凭据验证失败: PostAuth 未返回 sessionToken"
);
}
}

View File

@@ -63,6 +63,12 @@ pub(super) async fn handle_admin_provider_oauth_start_key(
"该 Provider 不是固定类型,无法使用 provider-oauth",
));
}
if provider_type == "windsurf" {
return Ok(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"Windsurf 请使用浏览器登录或导入凭据。",
));
}
let Some(template) = admin_provider_oauth_template(&provider_type) else {
return Ok(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
@@ -134,6 +140,12 @@ pub(super) async fn handle_admin_provider_oauth_start_provider(
"Kiro 不支持 OAuth 授权,请使用导入授权。",
));
}
if provider_type == "windsurf" {
return Ok(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"Windsurf 请使用浏览器登录或导入凭据。",
));
}
let Some(template) = admin_provider_oauth_template(&provider_type) else {
return Ok(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,

View File

@@ -36,6 +36,13 @@ fn is_openai_provider_oauth_provider_type(value: Option<&serde_json::Value>) ->
})
}
fn is_windsurf_provider_oauth_provider_type(value: Option<&serde_json::Value>) -> bool {
value
.and_then(serde_json::Value::as_str)
.map(str::trim)
.is_some_and(|provider_type| provider_type.eq_ignore_ascii_case("windsurf"))
}
fn match_codex_provider_oauth_identity(
new_auth_config: &serde_json::Map<String, serde_json::Value>,
existing_auth_config: &serde_json::Map<String, serde_json::Value>,
@@ -106,6 +113,41 @@ fn match_codex_provider_oauth_identity(
None
}
fn match_windsurf_provider_oauth_identity(
new_auth_config: &serde_json::Map<String, serde_json::Value>,
existing_auth_config: &serde_json::Map<String, serde_json::Value>,
) -> Option<bool> {
let new_provider_type = new_auth_config.get("provider_type");
let existing_provider_type = existing_auth_config.get("provider_type");
if !is_windsurf_provider_oauth_provider_type(new_provider_type)
&& !is_windsurf_provider_oauth_provider_type(existing_provider_type)
{
return None;
}
let new_account_id = normalize_provider_oauth_identity_value(new_auth_config.get("account_id"));
let existing_account_id =
normalize_provider_oauth_identity_value(existing_auth_config.get("account_id"));
if let (Some(new_account_id), Some(existing_account_id)) =
(new_account_id.as_deref(), existing_account_id.as_deref())
{
return Some(new_account_id == existing_account_id);
}
let new_credential_fingerprint =
normalize_provider_oauth_identity_value(new_auth_config.get("credential_fingerprint"));
let existing_credential_fingerprint =
normalize_provider_oauth_identity_value(existing_auth_config.get("credential_fingerprint"));
if let (Some(new_fingerprint), Some(existing_fingerprint)) = (
new_credential_fingerprint.as_deref(),
existing_credential_fingerprint.as_deref(),
) {
return Some(new_fingerprint == existing_fingerprint);
}
None
}
fn is_codex_cross_plan_group_non_duplicate(
new_auth_config: &serde_json::Map<String, serde_json::Value>,
existing_auth_config: &serde_json::Map<String, serde_json::Value>,
@@ -169,10 +211,17 @@ pub(crate) async fn find_duplicate_provider_oauth_key(
) -> Result<Option<StoredProviderCatalogKey>, String> {
let new_email = normalize_provider_oauth_identity_value(auth_config.get("email"));
let new_user_id = normalize_provider_oauth_identity_value(auth_config.get("user_id"));
let new_account_id = normalize_provider_oauth_identity_value(auth_config.get("account_id"));
let new_credential_fingerprint =
normalize_provider_oauth_identity_value(auth_config.get("credential_fingerprint"));
let new_auth_method = normalize_provider_oauth_identity_value(auth_config.get("auth_method"));
let new_kiro_provider = normalize_provider_oauth_identity_value(auth_config.get("provider"));
if new_email.is_none() && new_user_id.is_none() {
if new_email.is_none()
&& new_user_id.is_none()
&& new_account_id.is_none()
&& new_credential_fingerprint.is_none()
{
return Ok(None);
}
@@ -201,15 +250,28 @@ pub(crate) async fn find_duplicate_provider_oauth_key(
normalize_provider_oauth_identity_value(existing_auth_config.get("auth_method"));
let existing_kiro_provider =
normalize_provider_oauth_identity_value(existing_auth_config.get("provider"));
let is_windsurf = auth_config
.get("provider_type")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case("windsurf"))
|| existing_auth_config
.get("provider_type")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case("windsurf"));
let mut is_duplicate = false;
let codex_identity_match =
match_codex_provider_oauth_identity(auth_config, &existing_auth_config);
let windsurf_identity_match =
match_windsurf_provider_oauth_identity(auth_config, &existing_auth_config);
if let Some(codex_identity_match) = codex_identity_match {
is_duplicate = codex_identity_match;
} else if let Some(windsurf_identity_match) = windsurf_identity_match {
is_duplicate = windsurf_identity_match;
}
if codex_identity_match.is_none()
&& windsurf_identity_match.is_none()
&& !is_duplicate
&& new_user_id.is_some()
&& existing_user_id.is_some()
@@ -220,7 +282,9 @@ pub(crate) async fn find_duplicate_provider_oauth_key(
}
if codex_identity_match.is_none()
&& windsurf_identity_match.is_none()
&& !is_duplicate
&& !is_windsurf
&& new_email.is_some()
&& existing_email.is_some()
&& new_email == existing_email
@@ -261,6 +325,12 @@ pub(crate) async fn find_duplicate_provider_oauth_key(
let identifier =
normalize_provider_oauth_identity_value(auth_config.get("account_user_id"))
.or_else(|| normalize_provider_oauth_identity_value(auth_config.get("account_id")))
.or_else(|| {
normalize_provider_oauth_identity_value(
auth_config.get("credential_fingerprint"),
)
.map(|value| format!("fingerprint:{value}"))
})
.or_else(|| new_email.clone())
.or_else(|| new_user_id.clone())
.unwrap_or_default();
@@ -272,3 +342,91 @@ pub(crate) async fn find_duplicate_provider_oauth_key(
Ok(None)
}
#[cfg(test)]
mod tests {
use super::match_windsurf_provider_oauth_identity;
use serde_json::{json, Map, Value};
fn auth_config(value: Value) -> Map<String, Value> {
value.as_object().cloned().expect("auth config object")
}
#[test]
fn windsurf_identity_matches_account_id_without_email() {
let new_auth_config = auth_config(json!({
"provider_type": "windsurf",
"auth_method": "api_key",
"account_id": "acct-ws-1"
}));
let existing_auth_config = auth_config(json!({
"provider_type": "windsurf",
"auth_method": "browser",
"account_id": "acct-ws-1"
}));
assert_eq!(
match_windsurf_provider_oauth_identity(&new_auth_config, &existing_auth_config),
Some(true)
);
}
#[test]
fn windsurf_identity_rejects_different_account_id() {
let new_auth_config = auth_config(json!({
"provider_type": "windsurf",
"account_id": "acct-ws-1",
"email": "same@example.com"
}));
let existing_auth_config = auth_config(json!({
"provider_type": "windsurf",
"account_id": "acct-ws-2",
"email": "same@example.com"
}));
assert_eq!(
match_windsurf_provider_oauth_identity(&new_auth_config, &existing_auth_config),
Some(false)
);
}
#[test]
fn windsurf_identity_matches_credential_fingerprint_without_profile() {
let new_auth_config = auth_config(json!({
"provider_type": "windsurf",
"auth_method": "api_key",
"credential_fingerprint": "abcdef0123456789"
}));
let existing_auth_config = auth_config(json!({
"provider_type": "windsurf",
"auth_method": "browser",
"credential_fingerprint": "abcdef0123456789"
}));
assert_eq!(
match_windsurf_provider_oauth_identity(&new_auth_config, &existing_auth_config),
Some(true)
);
}
#[test]
fn windsurf_identity_does_not_match_user_supplied_email_only() {
let new_auth_config = auth_config(json!({
"provider_type": "windsurf",
"auth_method": "api_key",
"email": "same@example.com",
"email_verified": false
}));
let existing_auth_config = auth_config(json!({
"provider_type": "windsurf",
"auth_method": "api_key",
"email": "same@example.com",
"email_verified": false
}));
assert_eq!(
match_windsurf_provider_oauth_identity(&new_auth_config, &existing_auth_config),
None
);
}
}

View File

@@ -6,6 +6,7 @@ use super::chatgpt_web::refresh_chatgpt_web_provider_quota_locally;
use super::codex::refresh_codex_provider_quota_locally;
use super::grok::refresh_grok_provider_quota_locally;
use super::kiro::refresh_kiro_provider_quota_locally;
use super::windsurf::refresh_windsurf_provider_quota_locally;
use crate::handlers::admin::request::AdminAppState;
use crate::GatewayError;
use aether_contracts::ProxySnapshot;
@@ -36,6 +37,7 @@ const PROVIDER_QUOTA_REFRESH_HANDLERS: &[(&str, ProviderQuotaRefreshHandler)] =
("codex", refresh_codex_provider_quota_locally_boxed),
("grok", refresh_grok_provider_quota_locally_boxed),
("kiro", refresh_kiro_provider_quota_locally_boxed),
("windsurf", refresh_windsurf_provider_quota_locally_boxed),
];
pub(crate) async fn refresh_provider_pool_quota_locally(
@@ -135,3 +137,19 @@ fn refresh_grok_provider_quota_locally_boxed<'a>(
proxy_override,
))
}
fn refresh_windsurf_provider_quota_locally_boxed<'a>(
state: &'a AdminAppState<'a>,
provider: &'a StoredProviderCatalogProvider,
endpoint: &'a StoredProviderCatalogEndpoint,
keys: Vec<StoredProviderCatalogKey>,
proxy_override: Option<ProxySnapshot>,
) -> ProviderQuotaRefreshFuture<'a> {
Box::pin(refresh_windsurf_provider_quota_locally(
state,
provider,
endpoint,
keys,
proxy_override,
))
}

View File

@@ -5,3 +5,4 @@ pub(crate) mod dispatch;
pub(crate) mod grok;
pub(crate) mod kiro;
pub(crate) mod shared;
pub(crate) mod windsurf;

View File

@@ -0,0 +1,632 @@
use super::shared::{
build_provider_quota_execution_plan, build_quota_snapshot_payload,
default_provider_quota_execution_timeouts, execute_provider_quota_plan,
extract_execution_error_message, persist_provider_quota_refresh_state,
quota_refresh_success_invalid_state, ProviderQuotaExecutionOutcome,
};
use crate::handlers::admin::request::{AdminAppState, AdminGatewayProviderTransportSnapshot};
use crate::GatewayError;
use aether_contracts::ProxySnapshot;
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
};
use aether_provider_pool::{
build_windsurf_pool_model_configs_request_with_base_url,
build_windsurf_pool_quota_request_with_base_url,
build_windsurf_pool_rate_limit_request_with_base_url, ProviderPoolQuotaRequestSpec,
};
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
async fn execute_windsurf_probe_plan(
state: &AdminAppState<'_>,
transport: &AdminGatewayProviderTransportSnapshot,
spec: ProviderPoolQuotaRequestSpec,
proxy_override: Option<&ProxySnapshot>,
quota_kind: &str,
) -> Result<ProviderQuotaExecutionOutcome, GatewayError> {
let proxy = match proxy_override {
Some(proxy) => Some(proxy.clone()),
None => {
state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(transport)
.await
}
};
let timeouts = state
.resolve_transport_execution_timeouts(transport)
.or(Some(default_provider_quota_execution_timeouts(
proxy.as_ref(),
)));
let plan = build_provider_quota_execution_plan(
transport,
spec,
proxy,
state.resolve_transport_profile(transport),
timeouts,
);
execute_provider_quota_plan(state, transport, plan, quota_kind).await
}
async fn execute_windsurf_user_status_plan(
state: &AdminAppState<'_>,
transport: &AdminGatewayProviderTransportSnapshot,
api_key: &str,
proxy_override: Option<&ProxySnapshot>,
) -> Result<ProviderQuotaExecutionOutcome, GatewayError> {
let spec = build_windsurf_pool_quota_request_with_base_url(
&transport.key.id,
&transport.endpoint.base_url,
api_key,
);
execute_windsurf_probe_plan(
state,
transport,
spec,
proxy_override,
"windsurf:user_status",
)
.await
}
async fn execute_windsurf_model_configs_plan(
state: &AdminAppState<'_>,
transport: &AdminGatewayProviderTransportSnapshot,
api_key: &str,
proxy_override: Option<&ProxySnapshot>,
) -> Result<ProviderQuotaExecutionOutcome, GatewayError> {
let spec = build_windsurf_pool_model_configs_request_with_base_url(
&transport.key.id,
&transport.endpoint.base_url,
api_key,
);
execute_windsurf_probe_plan(
state,
transport,
spec,
proxy_override,
"windsurf:model_configs",
)
.await
}
async fn execute_windsurf_rate_limit_plan(
state: &AdminAppState<'_>,
transport: &AdminGatewayProviderTransportSnapshot,
api_key: &str,
proxy_override: Option<&ProxySnapshot>,
) -> Result<ProviderQuotaExecutionOutcome, GatewayError> {
let spec = build_windsurf_pool_rate_limit_request_with_base_url(
&transport.key.id,
&transport.endpoint.base_url,
api_key,
);
execute_windsurf_probe_plan(
state,
transport,
spec,
proxy_override,
"windsurf:rate_limit",
)
.await
}
fn merge_windsurf_probe_metadata(
mut user_status_metadata: serde_json::Value,
model_configs_metadata: Option<serde_json::Value>,
rate_limit_metadata: Option<serde_json::Value>,
) -> serde_json::Value {
let Some(target) = user_status_metadata.as_object_mut() else {
return user_status_metadata;
};
for metadata in [model_configs_metadata, rate_limit_metadata]
.into_iter()
.flatten()
{
if let Some(source) = metadata.as_object() {
for (key, value) in source {
target.insert(key.clone(), value.clone());
}
}
}
user_status_metadata
}
fn append_windsurf_probe_warning(metadata: &mut serde_json::Value, probe: &str, message: String) {
let Some(target) = metadata.as_object_mut() else {
return;
};
let warnings = target
.entry("probe_warnings".to_string())
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
if let Some(items) = warnings.as_array_mut() {
items.push(json!({
"probe": probe,
"message": message,
}));
}
}
fn build_windsurf_metadata_update(
current_upstream_metadata: Option<&serde_json::Value>,
patch: serde_json::Value,
) -> serde_json::Value {
let Some(patch_object) = patch.as_object() else {
return json!({ "windsurf": patch });
};
let mut merged_bucket = current_upstream_metadata
.and_then(|value| value.get("windsurf"))
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
for (key, value) in patch_object {
merged_bucket.insert(key.clone(), value.clone());
}
json!({ "windsurf": merged_bucket })
}
fn sanitize_windsurf_probe_detail(detail: impl AsRef<str>) -> String {
let detail = detail.as_ref().trim();
if detail.is_empty() {
return "-".to_string();
}
if let Ok(mut value) = serde_json::from_str::<serde_json::Value>(detail) {
redact_windsurf_sensitive_json(&mut value);
return value.to_string().chars().take(500).collect();
}
if contains_windsurf_sensitive_marker(detail) {
"[REDACTED upstream error body]".to_string()
} else {
detail.chars().take(500).collect()
}
}
fn redact_windsurf_sensitive_json(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(object) => {
for (key, value) in object {
if is_windsurf_sensitive_key(key) {
*value = json!("[REDACTED]");
} else {
redact_windsurf_sensitive_json(value);
}
}
}
serde_json::Value::Array(items) => {
for item in items {
redact_windsurf_sensitive_json(item);
}
}
serde_json::Value::String(text) if looks_like_windsurf_secret(text) => {
*text = "[REDACTED]".to_string();
}
_ => {}
}
}
fn is_windsurf_sensitive_key(key: &str) -> bool {
let normalized = key
.chars()
.filter(|ch| ch.is_ascii_alphanumeric())
.collect::<String>()
.to_ascii_lowercase();
normalized.contains("token")
|| normalized.contains("apikey")
|| normalized.contains("password")
|| normalized.contains("authorization")
|| normalized.contains("secret")
}
fn looks_like_windsurf_secret(value: &str) -> bool {
let value = value.trim();
value.starts_with("devin-session-token$")
|| value.starts_with("sk-")
|| (value.len() > 80 && value.split('.').count() == 3)
}
fn contains_windsurf_sensitive_marker(value: &str) -> bool {
let lowered = value.to_ascii_lowercase();
[
"token",
"api_key",
"apikey",
"sessiontoken",
"firebase_id_token",
"idtoken",
"authorization",
"password",
"secret",
"devin-session-token$",
]
.iter()
.any(|marker| lowered.contains(marker))
|| value.contains("sk-")
}
pub(crate) async fn refresh_windsurf_provider_quota_locally(
state: &AdminAppState<'_>,
provider: &StoredProviderCatalogProvider,
endpoint: &StoredProviderCatalogEndpoint,
keys: Vec<StoredProviderCatalogKey>,
proxy_override: Option<ProxySnapshot>,
) -> Result<Option<serde_json::Value>, GatewayError> {
let mut results = Vec::new();
let mut success_count = 0usize;
let mut failed_count = 0usize;
for key in keys {
let transport = match state
.read_provider_transport_snapshot(&provider.id, &endpoint.id, &key.id)
.await?
{
Some(transport) => transport,
None => {
failed_count += 1;
results.push(json!({
"key_id": key.id,
"key_name": key.name,
"status": "error",
"message": "Provider transport snapshot unavailable",
}));
continue;
}
};
let api_key = transport.key.decrypted_api_key.trim();
if api_key.is_empty() {
failed_count += 1;
results.push(json!({
"key_id": key.id,
"key_name": key.name,
"status": "error",
"message": "缺少 Windsurf apiKey/sessionToken",
}));
continue;
}
let result = match execute_windsurf_user_status_plan(
state,
&transport,
api_key,
proxy_override.as_ref(),
)
.await?
{
ProviderQuotaExecutionOutcome::Response(result) => result,
ProviderQuotaExecutionOutcome::Failure(detail) => {
failed_count += 1;
let detail = sanitize_windsurf_probe_detail(detail);
results.push(json!({
"key_id": key.id,
"key_name": key.name,
"status": "error",
"message": format!("GetUserStatus 请求执行失败: {detail}"),
"status_code": 502,
}));
continue;
}
};
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
let mut metadata_update = None::<serde_json::Value>;
let (mut oauth_invalid_at_unix_secs, mut oauth_invalid_reason) =
quota_refresh_success_invalid_state(&key);
let mut status = "error".to_string();
let mut message = None::<String>;
if result.status_code == 200 {
if let Some(body_json) = result
.body
.as_ref()
.and_then(|body| body.json_body.as_ref())
{
let mut windsurf_metadata =
aether_admin::provider::quota::parse_windsurf_user_status_response(
body_json,
now_unix_secs,
);
if let Some(mut metadata) = windsurf_metadata.take() {
let model_metadata = match execute_windsurf_model_configs_plan(
state,
&transport,
api_key,
proxy_override.as_ref(),
)
.await?
{
ProviderQuotaExecutionOutcome::Response(model_result)
if model_result.status_code == 200 =>
{
model_result
.body
.as_ref()
.and_then(|body| body.json_body.as_ref())
.and_then(|body_json| {
aether_admin::provider::quota::parse_windsurf_model_configs_response(
body_json,
now_unix_secs,
)
})
}
ProviderQuotaExecutionOutcome::Response(model_result) => {
let detail = extract_execution_error_message(&model_result)
.unwrap_or_else(|| format!("HTTP {}", model_result.status_code));
let detail = sanitize_windsurf_probe_detail(detail);
append_windsurf_probe_warning(
&mut metadata,
"model_configs",
format!("GetCascadeModelConfigs 返回: {detail}"),
);
None
}
ProviderQuotaExecutionOutcome::Failure(detail) => {
let detail = sanitize_windsurf_probe_detail(detail);
append_windsurf_probe_warning(
&mut metadata,
"model_configs",
format!("GetCascadeModelConfigs 执行失败: {detail}"),
);
None
}
};
let rate_limit_metadata = match execute_windsurf_rate_limit_plan(
state,
&transport,
api_key,
proxy_override.as_ref(),
)
.await?
{
ProviderQuotaExecutionOutcome::Response(rate_limit_result)
if rate_limit_result.status_code == 200 =>
{
rate_limit_result
.body
.as_ref()
.and_then(|body| body.json_body.as_ref())
.and_then(|body_json| {
aether_admin::provider::quota::parse_windsurf_rate_limit_response(
body_json,
now_unix_secs,
)
})
}
ProviderQuotaExecutionOutcome::Response(rate_limit_result) => {
let detail = extract_execution_error_message(&rate_limit_result)
.unwrap_or_else(|| format!("HTTP {}", rate_limit_result.status_code));
let detail = sanitize_windsurf_probe_detail(detail);
append_windsurf_probe_warning(
&mut metadata,
"rate_limit",
format!("CheckUserMessageRateLimit 返回: {detail}"),
);
None
}
ProviderQuotaExecutionOutcome::Failure(detail) => {
let detail = sanitize_windsurf_probe_detail(detail);
append_windsurf_probe_warning(
&mut metadata,
"rate_limit",
format!("CheckUserMessageRateLimit 执行失败: {detail}"),
);
None
}
};
metadata = merge_windsurf_probe_metadata(
metadata,
model_metadata,
rate_limit_metadata,
);
metadata_update = Some(build_windsurf_metadata_update(
key.upstream_metadata.as_ref(),
metadata,
));
status = "success".to_string();
} else {
status = "no_metadata".to_string();
message = Some("响应中未包含 Windsurf 限额信息".to_string());
}
} else {
status = "no_metadata".to_string();
message = Some("无法解析 GetUserStatus 响应".to_string());
}
} else {
let err_msg =
extract_execution_error_message(&result).map(sanitize_windsurf_probe_detail);
message = Some(match err_msg.as_deref() {
Some(detail) if !detail.is_empty() => {
format!(
"GetUserStatus 返回状态码 {}: {}",
result.status_code, detail
)
}
_ => format!("GetUserStatus 返回状态码 {}", result.status_code),
});
let detail = err_msg
.clone()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| format!("HTTP {}", result.status_code));
let mut metadata = serde_json::Map::new();
metadata.insert("updated_at".to_string(), json!(now_unix_secs));
metadata.insert("last_error".to_string(), json!(detail));
match result.status_code {
401 | 403 => {
oauth_invalid_at_unix_secs = Some(now_unix_secs);
oauth_invalid_reason =
Some(format!("Windsurf token 无效或已被拒绝: {}", detail));
metadata.insert("banned".to_string(), json!(result.status_code == 403));
status = if result.status_code == 401 {
"auth_invalid".to_string()
} else {
"forbidden".to_string()
};
}
429 => {
metadata.insert(
"rate_limit".to_string(),
json!({
"limited": true,
"message": metadata
.get("last_error")
.cloned()
.unwrap_or_else(|| json!("rate limited")),
}),
);
status = "rate_limited".to_string();
}
_ => {}
}
metadata_update = Some(build_windsurf_metadata_update(
key.upstream_metadata.as_ref(),
serde_json::Value::Object(metadata),
));
}
if !persist_provider_quota_refresh_state(
state,
&key.id,
metadata_update.as_ref(),
oauth_invalid_at_unix_secs,
oauth_invalid_reason,
None,
)
.await?
{
failed_count += 1;
results.push(json!({
"key_id": key.id,
"key_name": key.name,
"status": "error",
"message": "Key 状态写入失败",
}));
continue;
}
if status == "success" {
success_count += 1;
} else {
failed_count += 1;
}
let mut payload = serde_json::Map::new();
payload.insert("key_id".to_string(), json!(key.id));
payload.insert("key_name".to_string(), json!(key.name));
payload.insert("status".to_string(), json!(status));
if let Some(message) = message {
payload.insert("message".to_string(), json!(message));
}
if result.status_code != 200 {
payload.insert("status_code".to_string(), json!(result.status_code));
}
if let Some(metadata) = metadata_update
.as_ref()
.and_then(|value| value.get("windsurf"))
.cloned()
{
payload.insert("metadata".to_string(), metadata);
}
if let Some(quota_snapshot) = build_quota_snapshot_payload(
"windsurf",
key.status_snapshot.as_ref(),
metadata_update.as_ref(),
) {
payload.insert("quota_snapshot".to_string(), quota_snapshot);
}
results.push(serde_json::Value::Object(payload));
}
Ok(Some(json!({
"success": success_count,
"failed": failed_count,
"total": success_count + failed_count,
"results": results,
"message": format!("已处理 {} 个 Key", success_count + failed_count),
"auto_removed": 0,
})))
}
#[cfg(test)]
mod tests {
use serde_json::json;
#[test]
fn windsurf_probe_metadata_merges_user_status_models_and_rate_limit() {
let metadata = super::merge_windsurf_probe_metadata(
json!({
"plan_name": "Pro",
"daily_remaining_percent": 42.0,
"updated_at": 1_770_000_000u64,
}),
Some(json!({
"allowed_models_count": 2u64,
"models": [
{"model_uid": "claude-sonnet-4-5"},
{"model_uid": "gpt-5-mini"}
],
"updated_at": 1_770_000_010u64,
})),
Some(json!({
"rate_limit": {
"limited": true,
"messages_remaining": 0.0,
"retry_after_ms": 60_000u64
},
"updated_at": 1_770_000_020u64,
})),
);
assert_eq!(metadata["plan_name"], json!("Pro"));
assert_eq!(metadata["daily_remaining_percent"], json!(42.0));
assert_eq!(metadata["allowed_models_count"], json!(2u64));
assert_eq!(metadata["rate_limit"]["limited"], json!(true));
assert_eq!(metadata["updated_at"], json!(1_770_000_020u64));
}
#[test]
fn windsurf_probe_detail_redacts_sensitive_values() {
let detail = super::sanitize_windsurf_probe_detail(
r#"{"error":{"message":"bad"},"apiKey":"sk-secret","sessionToken":"devin-session-token$secret"}"#,
);
assert!(detail.contains("[REDACTED]"));
assert!(!detail.contains("sk-secret"));
assert!(!detail.contains("devin-session-token$secret"));
}
#[test]
fn windsurf_metadata_update_preserves_existing_bucket_fields() {
let update = super::build_windsurf_metadata_update(
Some(&json!({
"windsurf": {
"daily_remaining_percent": 0.0,
"allowed_models_count": 3,
"updated_at": 1u64
}
})),
json!({
"last_error": "HTTP 429",
"rate_limit": {"limited": true},
"updated_at": 2u64
}),
);
assert_eq!(
update.pointer("/windsurf/daily_remaining_percent"),
Some(&json!(0.0))
);
assert_eq!(
update.pointer("/windsurf/allowed_models_count"),
Some(&json!(3))
);
assert_eq!(update.pointer("/windsurf/updated_at"), Some(&json!(2u64)));
assert_eq!(
update.pointer("/windsurf/rate_limit/limited"),
Some(&json!(true))
);
}
}

View File

@@ -14,7 +14,7 @@ use super::{provider_query_key_display_name, provider_query_provider_payload};
use crate::ai_serving::{
maybe_build_sync_finalize_outcome, GatewayControlDecision,
ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME, GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
};
use crate::clock::current_unix_ms;
use crate::execution_runtime;
@@ -1786,6 +1786,56 @@ async fn provider_query_execute_kiro_test_candidate(
})
}
async fn provider_query_finalize_windsurf_result(
route_path: &str,
trace_id: &str,
requested_model: &str,
mapped_model: &str,
original_request_body: &Value,
result: &aether_contracts::ExecutionResult,
) -> Result<Option<Value>, GatewayError> {
let decision = GatewayControlDecision::synthetic(
route_path,
Some("admin_proxy".to_string()),
Some("provider_query_manage".to_string()),
Some("test_model_failover".to_string()),
Some("openai:chat".to_string()),
);
let payload = GatewaySyncReportRequest {
trace_id: trace_id.to_string(),
report_kind: OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND.to_string(),
report_context: Some(json!({
"client_api_format": "openai:chat",
"provider_api_format": "openai:chat",
"model": requested_model,
"mapped_model": mapped_model,
"needs_conversion": false,
"has_envelope": true,
"envelope_name": crate::provider_transport::windsurf::WINDSURF_ENVELOPE_NAME,
"original_request_body": original_request_body,
})),
status_code: result.status_code,
headers: result.headers.clone(),
body_json: result.body.as_ref().and_then(|body| body.json_body.clone()),
client_body_json: None,
body_base64: result
.body
.as_ref()
.and_then(|body| body.body_bytes_b64.clone()),
telemetry: result.telemetry.clone(),
};
let Some(outcome) = maybe_build_sync_finalize_outcome(trace_id, &decision, &payload)? else {
return Ok(None);
};
let bytes = to_bytes(outcome.response.into_body(), usize::MAX)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
serde_json::from_slice::<Value>(&bytes)
.map(Some)
.map_err(|err| GatewayError::Internal(err.to_string()))
}
fn provider_query_build_openai_image_test_request_body_for_route(
payload: &Value,
model: &str,
@@ -2659,6 +2709,22 @@ async fn provider_query_execute_standard_test_candidate(
route_path,
client_api_format,
);
if crate::provider_transport::is_windsurf_provider_transport(&transport)
&& provider_query_normalize_api_format_alias(candidate.endpoint.api_format.as_str())
== "openai:chat"
{
return provider_query_execute_windsurf_test_candidate(
state,
provider,
candidate,
payload,
route_path,
trace_id,
transport,
original_request_body,
)
.await;
}
if !provider_query_transport_supports_model_test_execution(
state,
&transport,
@@ -3066,6 +3132,182 @@ async fn provider_query_execute_standard_test_candidate(
})
}
#[allow(clippy::too_many_arguments)]
async fn provider_query_execute_windsurf_test_candidate(
state: &AdminAppState<'_>,
provider: &StoredProviderCatalogProvider,
candidate: &ProviderQueryTestCandidate,
payload: &Value,
route_path: &str,
trace_id: &str,
transport: AdminGatewayProviderTransportSnapshot,
original_request_body: Value,
) -> Result<ProviderQueryExecutionOutcome, GatewayError> {
if let Some(_reason) =
crate::provider_transport::local_windsurf_request_transport_unsupported_reason_with_network(
&transport,
)
{
return Ok(provider_query_skipped_execution_outcome(
original_request_body,
provider_query_standard_test_unsupported_reason(
&transport,
candidate.endpoint.api_format.as_str(),
),
));
}
let incoming_request_headers = provider_query_extract_request_headers(payload);
let request_body = original_request_body.clone();
let request_model =
provider_query_request_body_model(&request_body, &candidate.effective_model);
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),
)
else {
return Ok(provider_query_skipped_execution_outcome(
request_body,
"Provider auth is unavailable for windsurf".to_string(),
));
};
let mut synthetic_request = http::Request::builder()
.uri(route_path)
.body(())
.map_err(|err| GatewayError::Internal(err.to_string()))?;
*synthetic_request.headers_mut() = incoming_request_headers;
let (parts, _) = synthetic_request.into_parts();
let Some(provider_request_body) =
crate::provider_transport::build_windsurf_cascade_request_body(
&request_body,
request_model,
&auth_value,
transport.endpoint.body_rules.as_ref(),
Some(&parts.headers),
upstream_is_stream,
)
else {
return Ok(provider_query_skipped_execution_outcome(
request_body,
"Provider request body could not be built for windsurf".to_string(),
));
};
let Some(request_url) = crate::provider_transport::build_windsurf_cascade_upstream_url(
transport.endpoint.base_url.as_str(),
parts.uri.query(),
) else {
return Ok(provider_query_skipped_execution_outcome(
provider_request_body,
"Provider request URL is unavailable for windsurf".to_string(),
));
};
let Some(request_headers) = crate::provider_transport::build_windsurf_cascade_headers(
&parts.headers,
&provider_request_body,
&request_body,
transport.endpoint.header_rules.as_ref(),
&auth_header,
&auth_value,
upstream_is_stream,
) else {
return Ok(ProviderQueryExecutionOutcome {
status: "failed",
skip_reason: None,
error_message: Some("provider request headers build failed".to_string()),
status_code: None,
latency_ms: None,
request_url,
request_headers: BTreeMap::new(),
request_body: provider_request_body,
response_headers: BTreeMap::new(),
response_body: None,
});
};
let plan = ExecutionPlan {
request_id: trace_id.to_string(),
candidate_id: Some(format!("provider-query-{}", candidate.key.id)),
provider_name: Some(provider.name.clone()),
provider_id: provider.id.clone(),
endpoint_id: candidate.endpoint.id.clone(),
key_id: candidate.key.id.clone(),
method: "POST".to_string(),
url: request_url.clone(),
headers: request_headers.clone(),
content_type: Some("application/connect+json".to_string()),
content_encoding: None,
body: RequestBody::from_json(provider_request_body.clone()),
stream: upstream_is_stream,
client_api_format: "openai:chat".to_string(),
provider_api_format: candidate.endpoint.api_format.clone(),
model_name: Some(request_model.to_string()),
proxy: state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await,
transport_profile: state.resolve_transport_profile(&transport),
timeouts: state.resolve_transport_execution_timeouts(&transport),
};
let result = state
.execute_execution_runtime_sync_plan(Some(trace_id), &plan)
.await?;
let response_body = if result.status_code < 400 {
provider_query_finalize_windsurf_result(
route_path,
trace_id,
request_model,
request_model,
&request_body,
&result,
)
.await?
} else {
result.body.as_ref().and_then(|body| body.json_body.clone())
};
let missing_success_body = result.status_code < 400 && response_body.is_none();
let did_fail = result.status_code >= 400 || missing_success_body;
let error_message = if did_fail {
provider_query_extract_error_message(&result).or_else(|| {
missing_success_body.then(|| {
format!(
"Provider returned HTTP {} without a model-test response body",
result.status_code
)
})
})
} else {
None
};
Ok(ProviderQueryExecutionOutcome {
status: if did_fail { "failed" } else { "success" },
skip_reason: None,
error_message,
status_code: Some(result.status_code),
latency_ms: result.telemetry.as_ref().and_then(|value| value.elapsed_ms),
request_url,
request_headers,
request_body: provider_request_body,
response_headers: result.headers,
response_body,
})
}
async fn build_admin_provider_query_kiro_failover_response(
state: &AdminAppState<'_>,
payload: &Value,

View File

@@ -46,6 +46,22 @@ pub(super) fn provider_query_standard_test_unsupported_reason(
api_format: &str,
) -> String {
let normalized_api_format = crate::ai_serving::normalize_api_format_alias(api_format);
if crate::provider_transport::is_windsurf_provider_transport(transport)
&& normalized_api_format == "openai:chat"
{
let reason =
crate::provider_transport::local_windsurf_request_transport_unsupported_reason_with_network(
transport,
);
return match reason {
Some(reason) => format!(
"{} ({reason})",
provider_query_unsupported_test_api_format_message(api_format)
),
None => provider_query_unsupported_test_api_format_message(api_format),
};
}
let reason = match normalized_api_format.as_str() {
"openai:chat" => {
crate::provider_transport::policy::local_openai_chat_transport_unsupported_reason(
@@ -294,6 +310,15 @@ pub(super) fn provider_query_transport_supports_model_test_execution(
transport: &AdminGatewayProviderTransportSnapshot,
api_format: &str,
) -> bool {
if crate::provider_transport::is_windsurf_provider_transport(transport)
&& provider_query_normalize_api_format_alias(api_format) == "openai:chat"
{
return crate::provider_transport::local_windsurf_request_transport_unsupported_reason_with_network(
transport,
)
.is_none();
}
match provider_query_test_adapter_for_provider_api_format(
transport.provider.provider_type.as_str(),
api_format,

View File

@@ -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]"))
);
}
}

View File

@@ -4,9 +4,9 @@ pub(crate) fn normalize_provider_type_input(value: &str) -> Result<String, Strin
let normalized = value.trim().to_ascii_lowercase();
match normalized.as_str() {
"custom" | "claude_code" | "kiro" | "codex" | "chatgpt_web" | "gemini_cli"
| "antigravity" | "vertex_ai" | "grok" => Ok(normalized),
| "antigravity" | "vertex_ai" | "grok" | "windsurf" => Ok(normalized),
_ => Err(
"provider_type 仅支持 custom / claude_code / kiro / codex / chatgpt_web / gemini_cli / antigravity / vertex_ai / grok"
"provider_type 仅支持 custom / claude_code / kiro / codex / chatgpt_web / gemini_cli / antigravity / vertex_ai / grok / windsurf"
.to_string(),
),
}

View File

@@ -1086,6 +1086,278 @@ fn build_chatgpt_web_quota_status_snapshot(
}))
}
fn windsurf_percent_quota_window_snapshot(
metadata: &Map<String, Value>,
code: &str,
label: &str,
remaining_percent_key: &str,
reset_at_key: &str,
observed_at_unix_secs: Option<u64>,
) -> Option<Value> {
let remaining_percent = metadata
.get(remaining_percent_key)
.and_then(admin_provider_quota_pure::coerce_json_f64);
let reset_at = provider_quota_timestamp_unix_secs(metadata.get(reset_at_key));
if remaining_percent.is_none() && reset_at.is_none() {
return None;
}
let remaining_ratio = remaining_percent.map(|value| (value / 100.0).clamp(0.0, 1.0));
let used_ratio = remaining_ratio.map(|value| (1.0 - value).clamp(0.0, 1.0));
let reset_seconds = quota_window_reset_seconds(observed_at_unix_secs, reset_at);
Some(json!({
"code": code,
"label": label,
"scope": "account",
"unit": "percent",
"used_ratio": used_ratio,
"remaining_ratio": remaining_ratio,
"reset_at": reset_at,
"reset_seconds": reset_seconds,
"is_exhausted": remaining_ratio.map(|value| value <= 1e-6),
}))
}
fn windsurf_count_quota_window_snapshot(
metadata: &Map<String, Value>,
code: &str,
label: &str,
used_key: &str,
limit_key: &str,
remaining_key: &str,
) -> Option<Value> {
let used = metadata
.get(used_key)
.and_then(admin_provider_quota_pure::coerce_json_f64);
let limit = metadata
.get(limit_key)
.and_then(admin_provider_quota_pure::coerce_json_f64);
let remaining = metadata
.get(remaining_key)
.and_then(admin_provider_quota_pure::coerce_json_f64)
.or_else(|| limit.zip(used).map(|(limit, used)| (limit - used).max(0.0)));
if used.is_none() && limit.is_none() && remaining.is_none() {
return None;
}
let used_ratio = used
.zip(limit)
.and_then(|(used, limit)| (limit > 0.0).then_some((used / limit).clamp(0.0, 1.0)));
let remaining_ratio = remaining.zip(limit).and_then(|(remaining, limit)| {
(limit > 0.0).then_some((remaining / limit).clamp(0.0, 1.0))
});
Some(json!({
"code": code,
"label": label,
"scope": "account",
"unit": "count",
"used_ratio": used_ratio,
"remaining_ratio": remaining_ratio,
"used_value": used,
"remaining_value": remaining,
"limit_value": limit,
"is_exhausted": remaining.is_some_and(|value| value <= 0.0),
}))
}
fn build_windsurf_quota_status_snapshot(
upstream_metadata: Option<&Value>,
source: &str,
) -> Option<Value> {
let metadata = provider_quota_metadata_bucket(upstream_metadata, "windsurf")?;
let observed_at_unix_secs = provider_quota_timestamp_unix_secs(metadata.get("updated_at"));
let plan_type = metadata
.get("plan_name")
.or_else(|| metadata.get("plan_type"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let rate_limit = metadata
.get("rate_limit")
.cloned()
.filter(|value| !value.is_null());
let last_error = metadata
.get("last_error")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let banned = metadata
.get("banned")
.or_else(|| metadata.get("is_banned"))
.and_then(admin_provider_quota_pure::coerce_json_bool)
== Some(true);
let quarantined = metadata
.get("quarantined")
.or_else(|| metadata.get("is_quarantined"))
.and_then(admin_provider_quota_pure::coerce_json_bool)
== Some(true);
let mut windows = [
windsurf_percent_quota_window_snapshot(
metadata,
"daily",
"",
"daily_remaining_percent",
"daily_reset_at",
observed_at_unix_secs,
),
windsurf_percent_quota_window_snapshot(
metadata,
"weekly",
"",
"weekly_remaining_percent",
"weekly_reset_at",
observed_at_unix_secs,
),
windsurf_count_quota_window_snapshot(
metadata,
"prompt",
"Prompt",
"prompt_used",
"prompt_limit",
"prompt_remaining",
),
windsurf_count_quota_window_snapshot(
metadata,
"flex",
"Flex",
"flex_used",
"flex_limit",
"flex_remaining",
),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
let mut rate_limit_cooling = false;
let mut rate_limit_reset_seconds = None;
let mut rate_limit_reason = None::<String>;
if let Some(rate_limit) = rate_limit.as_ref() {
if let Some(rate_limit_object) = rate_limit.as_object() {
let retry_after_ms = rate_limit_object
.get("retry_after_ms")
.or_else(|| rate_limit_object.get("retryAfterMs"))
.and_then(admin_provider_quota_pure::coerce_json_u64)
.filter(|value| *value > 0);
if let Some(retry_after_ms) = retry_after_ms {
rate_limit_cooling = true;
rate_limit_reset_seconds = Some(retry_after_ms.saturating_add(999) / 1000);
rate_limit_reason = rate_limit_object
.get("message")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
windows.push(json!({
"code": "rate_limit",
"label": "速率",
"scope": "account",
"unit": "count",
"is_exhausted": false,
"reset_seconds": rate_limit_reset_seconds,
}));
}
}
}
let allowed_models_count = metadata
.get("allowed_models_count")
.or_else(|| metadata.get("models_count"))
.and_then(admin_provider_quota_pure::coerce_json_u64);
if windows.is_empty()
&& plan_type.is_none()
&& observed_at_unix_secs.is_none()
&& rate_limit.is_none()
&& allowed_models_count.is_none()
&& !banned
&& !quarantined
{
return None;
}
let usage_ratio = quota_windows_usage_ratio(&windows);
let reset_seconds = if rate_limit_cooling {
rate_limit_reset_seconds.or_else(|| quota_windows_min_reset_seconds(&windows))
} else {
quota_windows_min_reset_seconds(&windows)
};
let reset_at = if rate_limit_cooling {
None
} else {
quota_windows_min_reset_at(&windows)
};
let exhausted_by_window = windows.iter().filter_map(Value::as_object).any(|window| {
window
.get("code")
.and_then(Value::as_str)
.is_some_and(|code| {
code.eq_ignore_ascii_case("daily") || code.eq_ignore_ascii_case("weekly")
})
&& window
.get("is_exhausted")
.and_then(admin_provider_quota_pure::coerce_json_bool)
.unwrap_or(false)
});
let exhausted = banned || quarantined || exhausted_by_window;
let (code, label, reason) = if banned {
(
"banned",
Some("账号已封禁"),
last_error
.clone()
.or_else(|| Some("账号被 Windsurf 标记为不可用".to_string())),
)
} else if quarantined {
(
"quarantined",
Some("账号隔离中"),
last_error
.clone()
.or_else(|| Some("账号处于隔离状态".to_string())),
)
} else if rate_limit_cooling {
(
"cooldown",
Some("冷却中"),
last_error.clone().or(rate_limit_reason),
)
} else if exhausted {
(
"exhausted",
Some("额度耗尽"),
Some("额度窗口已耗尽".to_string()),
)
} else {
("ok", None, last_error)
};
Some(json!({
"version": 2,
"provider_type": "windsurf",
"code": code,
"label": label,
"reason": reason,
"freshness": "fresh",
"source": source,
"observed_at": observed_at_unix_secs,
"exhausted": exhausted,
"usage_ratio": usage_ratio,
"updated_at": observed_at_unix_secs,
"reset_at": reset_at,
"reset_seconds": reset_seconds,
"plan_type": plan_type,
"allowed_models_count": allowed_models_count,
"rate_limit": rate_limit.unwrap_or(Value::Null),
"windows": windows,
}))
}
fn build_antigravity_quota_status_snapshot(
upstream_metadata: Option<&Value>,
source: &str,
@@ -1333,6 +1605,7 @@ pub(crate) fn sync_provider_key_quota_status_snapshot(
"codex" => build_codex_quota_status_snapshot(upstream_metadata, source),
"kiro" => build_kiro_quota_status_snapshot(upstream_metadata, source),
"chatgpt_web" => build_chatgpt_web_quota_status_snapshot(upstream_metadata, source),
"windsurf" => build_windsurf_quota_status_snapshot(upstream_metadata, source),
"antigravity" => build_antigravity_quota_status_snapshot(upstream_metadata, source),
"grok" => build_grok_quota_status_snapshot(upstream_metadata, source),
"gemini_cli" => build_gemini_cli_quota_status_snapshot(upstream_metadata, source),
@@ -1369,6 +1642,12 @@ fn quota_snapshot_has_materialized_data(
return false;
}
if normalized_provider_type == "windsurf"
&& windsurf_quota_snapshot_has_stale_cooldown(quota_snapshot)
{
return false;
}
if quota_snapshot
.get("windows")
.and_then(Value::as_array)
@@ -1394,6 +1673,61 @@ fn quota_snapshot_has_materialized_data(
})
}
fn windsurf_quota_snapshot_has_stale_cooldown(quota_snapshot: &Map<String, Value>) -> bool {
let code = quota_snapshot
.get("code")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
if !code.eq_ignore_ascii_case("cooldown") {
return false;
}
let rate_limit = quota_snapshot.get("rate_limit").and_then(Value::as_object);
let retry_after_ms = rate_limit
.and_then(|rate_limit| {
rate_limit
.get("retry_after_ms")
.or_else(|| rate_limit.get("retryAfterMs"))
.and_then(admin_provider_quota_pure::coerce_json_u64)
})
.unwrap_or(0);
if retry_after_ms > 0 {
return false;
}
let has_positive_rate_limit_reset = quota_snapshot
.get("windows")
.and_then(Value::as_array)
.is_some_and(|windows| {
windows.iter().filter_map(Value::as_object).any(|window| {
window
.get("code")
.and_then(Value::as_str)
.is_some_and(|code| code.eq_ignore_ascii_case("rate_limit"))
&& window
.get("reset_seconds")
.or_else(|| window.get("reset_at"))
.and_then(admin_provider_quota_pure::coerce_json_u64)
.is_some_and(|value| value > 0)
})
});
if has_positive_rate_limit_reset {
return false;
}
let exhausted = quota_snapshot
.get("exhausted")
.and_then(admin_provider_quota_pure::coerce_json_bool)
.unwrap_or(false);
let has_capacity = rate_limit
.and_then(|rate_limit| rate_limit.get("has_capacity"))
.and_then(admin_provider_quota_pure::coerce_json_bool)
.unwrap_or(false);
has_capacity || !exhausted
}
pub(crate) fn provider_key_status_snapshot_payload(
key: &StoredProviderCatalogKey,
provider_type: &str,
@@ -2272,6 +2606,274 @@ mod tests {
assert_eq!(auto.get("used_value"), Some(&json!(90.0)));
}
#[test]
fn provider_key_status_snapshot_payload_backfills_windsurf_daily_and_weekly_quota() {
let mut key = sample_catalog_key();
key.upstream_metadata = Some(json!({
"windsurf": {
"updated_at": 1_778_067_246u64,
"plan_name": "Pro",
"daily_remaining_percent": 40.0,
"weekly_remaining_percent": 65.0,
"daily_reset_at": 1_778_100_000u64,
"weekly_reset_at": 1_778_600_000u64,
"prompt_used": 12.0,
"prompt_limit": 100.0,
"prompt_remaining": 88.0,
"flex_used": 3.0,
"flex_limit": 10.0,
"flex_remaining": 7.0,
"allowed_models_count": 82
}
}));
let payload = provider_key_status_snapshot_payload(&key, "windsurf");
let quota = payload
.get("quota")
.and_then(Value::as_object)
.expect("quota snapshot should be object");
let windows = quota
.get("windows")
.and_then(Value::as_array)
.expect("windsurf quota windows should exist");
let daily = windows
.iter()
.filter_map(Value::as_object)
.find(|window| window.get("code") == Some(&json!("daily")))
.expect("daily quota window should exist");
let weekly = windows
.iter()
.filter_map(Value::as_object)
.find(|window| window.get("code") == Some(&json!("weekly")))
.expect("weekly quota window should exist");
assert_eq!(quota.get("provider_type"), Some(&json!("windsurf")));
assert_eq!(quota.get("code"), Some(&json!("ok")));
assert_eq!(quota.get("plan_type"), Some(&json!("Pro")));
assert_eq!(quota.get("usage_ratio"), Some(&json!(0.6)));
assert_eq!(quota.get("reset_at"), Some(&json!(1_778_100_000u64)));
assert_eq!(daily.get("remaining_ratio"), Some(&json!(0.4)));
assert_eq!(daily.get("used_ratio"), Some(&json!(0.6)));
assert_eq!(daily.get("reset_seconds"), Some(&json!(32_754u64)));
assert_eq!(weekly.get("remaining_ratio"), Some(&json!(0.65)));
assert_eq!(weekly.get("used_ratio"), Some(&json!(0.35)));
assert_eq!(weekly.get("reset_seconds"), Some(&json!(532_754u64)));
assert_eq!(quota.get("allowed_models_count"), Some(&json!(82)));
}
#[test]
fn provider_key_status_snapshot_payload_treats_windsurf_rate_limit_as_cooldown() {
let mut key = sample_catalog_key();
key.upstream_metadata = Some(json!({
"windsurf": {
"updated_at": 1_778_067_246u64,
"daily_remaining_percent": 80.0,
"rate_limit": {
"limited": true,
"retry_after_ms": 60_001u64,
"message": "slow down"
},
"last_error": "slow down"
}
}));
let payload = provider_key_status_snapshot_payload(&key, "windsurf");
let quota = payload
.get("quota")
.and_then(Value::as_object)
.expect("quota snapshot should be object");
let rate_window = quota
.get("windows")
.and_then(Value::as_array)
.and_then(|windows| {
windows
.iter()
.filter_map(Value::as_object)
.find(|window| window.get("code") == Some(&json!("rate_limit")))
})
.expect("rate limit window should exist");
assert_eq!(quota.get("code"), Some(&json!("cooldown")));
assert_eq!(quota.get("exhausted"), Some(&json!(false)));
assert_eq!(quota.get("reset_seconds"), Some(&json!(61u64)));
assert_eq!(rate_window.get("is_exhausted"), Some(&json!(false)));
assert_eq!(rate_window.get("reset_seconds"), Some(&json!(61u64)));
}
#[test]
fn provider_key_status_snapshot_payload_keeps_windsurf_capacity_probe_without_retry_after_ok() {
let mut key = sample_catalog_key();
key.upstream_metadata = Some(json!({
"windsurf": {
"updated_at": 1_778_067_246u64,
"daily_remaining_percent": 100.0,
"weekly_remaining_percent": 100.0,
"rate_limit": {
"limited": true,
"has_capacity": false,
"messages_remaining": 0.0,
"max_messages": 100.0
}
}
}));
let payload = provider_key_status_snapshot_payload(&key, "windsurf");
let quota = payload
.get("quota")
.and_then(Value::as_object)
.expect("quota snapshot should be object");
let has_rate_limit_window =
quota
.get("windows")
.and_then(Value::as_array)
.is_some_and(|windows| {
windows
.iter()
.filter_map(Value::as_object)
.any(|window| window.get("code") == Some(&json!("rate_limit")))
});
assert_eq!(quota.get("code"), Some(&json!("ok")));
assert_eq!(quota.get("label"), Some(&Value::Null));
assert_eq!(quota.get("exhausted"), Some(&json!(false)));
assert_eq!(
payload.pointer("/quota/rate_limit/limited"),
Some(&json!(true))
);
assert!(!has_rate_limit_window);
}
#[test]
fn provider_key_status_snapshot_payload_refreshes_stale_windsurf_cooldown_when_probe_has_capacity(
) {
let mut key = sample_catalog_key();
key.status_snapshot = Some(json!({
"quota": {
"version": 2,
"provider_type": "windsurf",
"code": "cooldown",
"label": "冷却中",
"exhausted": false,
"windows": [
{
"code": "daily",
"unit": "percent",
"label": "",
"scope": "account",
"remaining_ratio": 0.99,
"is_exhausted": false
},
{
"code": "rate_limit",
"unit": "count",
"label": "速率",
"scope": "account",
"is_exhausted": false,
"reset_seconds": null
}
],
"rate_limit": {
"limited": true,
"has_capacity": true,
"messages_remaining": -1,
"max_messages": -1
}
}
}));
key.upstream_metadata = Some(json!({
"windsurf": {
"updated_at": 1_778_067_246u64,
"daily_remaining_percent": 99.0,
"weekly_remaining_percent": 100.0,
"allowed_models_count": 118,
"rate_limit": {
"limited": true,
"has_capacity": true,
"messages_remaining": -1,
"max_messages": -1
}
}
}));
let payload = provider_key_status_snapshot_payload(&key, "windsurf");
let quota = payload
.get("quota")
.and_then(Value::as_object)
.expect("quota snapshot should be object");
let has_rate_limit_window =
quota
.get("windows")
.and_then(Value::as_array)
.is_some_and(|windows| {
windows
.iter()
.filter_map(Value::as_object)
.any(|window| window.get("code") == Some(&json!("rate_limit")))
});
assert_eq!(quota.get("code"), Some(&json!("ok")));
assert_eq!(quota.get("label"), Some(&Value::Null));
assert_eq!(quota.get("allowed_models_count"), Some(&json!(118u64)));
assert!(!has_rate_limit_window);
}
#[test]
fn provider_key_status_snapshot_payload_marks_windsurf_banned_and_quarantined_blocking() {
let mut banned_key = sample_catalog_key();
banned_key.upstream_metadata = Some(json!({
"windsurf": {
"updated_at": 1_778_067_246u64,
"banned": true,
"reason": "forbidden"
}
}));
let banned_payload = provider_key_status_snapshot_payload(&banned_key, "windsurf");
assert_eq!(
banned_payload.pointer("/quota/code"),
Some(&json!("banned"))
);
assert_eq!(
banned_payload.pointer("/quota/exhausted"),
Some(&json!(true))
);
assert_eq!(
banned_payload.pointer("/account/code"),
Some(&json!("account_banned"))
);
assert_eq!(
banned_payload.pointer("/account/blocked"),
Some(&json!(true))
);
let mut quarantined_key = sample_catalog_key();
quarantined_key.upstream_metadata = Some(json!({
"windsurf": {
"updated_at": 1_778_067_246u64,
"quarantined": true
}
}));
let quarantined_payload =
provider_key_status_snapshot_payload(&quarantined_key, "windsurf");
assert_eq!(
quarantined_payload.pointer("/quota/code"),
Some(&json!("quarantined"))
);
assert_eq!(
quarantined_payload.pointer("/quota/exhausted"),
Some(&json!(true))
);
assert_eq!(
quarantined_payload.pointer("/account/code"),
Some(&json!("account_quarantined"))
);
assert_eq!(
quarantined_payload.pointer("/account/blocked"),
Some(&json!(true))
);
}
#[test]
fn provider_key_status_snapshot_payload_preserves_existing_materialized_quota_snapshot() {
let mut key = sample_catalog_key();

View File

@@ -426,6 +426,7 @@ mod tests {
keys: Arc<Mutex<Vec<StoredProviderCatalogKey>>>,
transports: Arc<HashMap<(String, String, String), GatewayProviderTransportSnapshot>>,
execution_results: Arc<Mutex<VecDeque<ExecutionResult>>>,
executed_plans: Arc<Mutex<Vec<ExecutionPlan>>>,
cached_models: Arc<Mutex<HashMap<(String, String), Vec<Value>>>>,
}
@@ -443,6 +444,7 @@ mod tests {
keys: Arc::new(Mutex::new(keys)),
transports: Arc::new(transports),
execution_results: Arc::new(Mutex::new(VecDeque::from(execution_results))),
executed_plans: Arc::new(Mutex::new(Vec::new())),
cached_models: Arc::new(Mutex::new(HashMap::new())),
}
}
@@ -482,8 +484,12 @@ mod tests {
async fn execute_model_fetch_execution_plan(
&self,
_plan: &ExecutionPlan,
plan: &ExecutionPlan,
) -> Result<ExecutionResult, String> {
self.executed_plans
.lock()
.expect("executed plans mutex")
.push(plan.clone());
self.execution_results
.lock()
.expect("execution result mutex")
@@ -910,6 +916,134 @@ mod tests {
);
}
#[tokio::test]
async fn model_fetch_fetches_windsurf_model_configs_and_persists_allowed_models() {
let provider = sample_provider("provider-windsurf", "windsurf");
let endpoint = StoredProviderCatalogEndpoint::new(
"endpoint-windsurf-chat".to_string(),
"provider-windsurf".to_string(),
"openai:chat".to_string(),
None,
None,
true,
)
.expect("endpoint should build")
.with_transport_fields(
"https://server.codeium.com".to_string(),
None,
None,
None,
None,
None,
None,
None,
)
.expect("endpoint transport should build");
let key = sample_key(
"key-windsurf",
"provider-windsurf",
"api_key",
&["openai:chat"],
);
let mut transport = sample_transport(
"windsurf",
"provider-windsurf",
"endpoint-windsurf-chat",
"key-windsurf",
"openai:chat",
"api_key",
Some(r#"{"provider_type":"windsurf"}"#),
);
transport.endpoint.base_url = "https://server.codeium.com".to_string();
transport.key.decrypted_api_key = "devin-session-token$abc".to_string();
let state = TestState::new(
vec![provider],
vec![endpoint],
vec![key],
HashMap::from([(
(
"provider-windsurf".to_string(),
"endpoint-windsurf-chat".to_string(),
"key-windsurf".to_string(),
),
transport,
)]),
vec![execution_result(json!({
"clientModelConfigs": [
{
"modelUid": "claude-sonnet-4-6",
"label": "Claude Sonnet 4.6",
"provider": "anthropic",
"supportsImages": true,
"creditMultiplier": 4
},
{
"modelUid": "gpt-5.4",
"label": "GPT-5.4",
"provider": "openai"
}
],
"defaultOverrideModelConfig": {
"modelUid": "claude-sonnet-4-6"
}
}))],
);
let summary = perform_model_fetch_once_with_state(&state)
.await
.expect("fetch should succeed");
assert_eq!(summary.succeeded, 1);
let plans = state.executed_plans.lock().expect("executed plans mutex");
assert_eq!(plans.len(), 1);
assert_eq!(
plans[0].url,
"https://server.codeium.com/exa.api_server_pb.ApiServerService/GetCascadeModelConfigs"
);
assert_eq!(plans[0].method, "POST");
assert_eq!(plans[0].provider_api_format, "windsurf:model_configs");
assert_eq!(
plans[0]
.body
.json_body
.as_ref()
.and_then(|body| body.get("metadata"))
.and_then(|metadata| metadata.get("apiKey")),
Some(&json!("devin-session-token$abc"))
);
drop(plans);
let updated = state.key("key-windsurf");
assert_eq!(
updated.allowed_models,
Some(json!(["claude-sonnet-4-6", "gpt-5.4"]))
);
assert_eq!(
updated
.upstream_metadata
.as_ref()
.and_then(|value| value.get("windsurf"))
.and_then(|value| value.get("allowed_models_count")),
Some(&json!(2))
);
assert_eq!(
updated
.upstream_metadata
.as_ref()
.and_then(|value| value.get("windsurf"))
.and_then(|value| value.get("default_model_uid")),
Some(&json!("claude-sonnet-4-6"))
);
let cached = state.cached_models.lock().expect("cache mutex");
let cached_models = cached
.get(&("provider-windsurf".to_string(), "key-windsurf".to_string()))
.expect("cached models should be written");
assert_eq!(
cached_models[0]["api_formats"],
json!(["openai:chat", "openai:responses", "claude:messages"])
);
}
#[tokio::test]
async fn model_fetch_failure_keeps_existing_allowed_models() {
let provider = sample_provider("provider-openai", "openai");

View File

@@ -50,6 +50,7 @@ pub(crate) struct ProviderKeyAuthSemantics {
credential_kind: ProviderKeyCredentialKind,
runtime_auth_kind: ProviderKeyRuntimeAuthKind,
oauth_managed: bool,
can_refresh_oauth: bool,
}
impl ProviderKeyAuthSemantics {
@@ -66,7 +67,7 @@ impl ProviderKeyAuthSemantics {
}
pub(crate) const fn can_refresh_oauth(self) -> bool {
self.oauth_managed
self.can_refresh_oauth
}
pub(crate) const fn can_export_oauth(self) -> bool {
@@ -115,7 +116,13 @@ fn key_has_auth_type_overrides(key: &StoredProviderCatalogKey) -> bool {
fn provider_uses_bearer_oauth_runtime(provider_type: &str) -> bool {
matches!(
provider_type.trim().to_ascii_lowercase().as_str(),
"claude_code" | "codex" | "chatgpt_web" | "gemini_cli" | "antigravity" | "kiro"
"claude_code"
| "codex"
| "chatgpt_web"
| "gemini_cli"
| "antigravity"
| "kiro"
| "windsurf"
)
}
@@ -173,10 +180,12 @@ pub(crate) fn provider_key_auth_semantics(
}
};
let provider_type_normalized = provider_type.trim().to_ascii_lowercase();
ProviderKeyAuthSemantics {
credential_kind,
runtime_auth_kind,
oauth_managed,
can_refresh_oauth: oauth_managed && provider_type_normalized != "windsurf",
}
}
@@ -319,7 +328,6 @@ mod tests {
key.encrypted_auth_config = Some(r#"{"sso_token":"abc"}"#.to_string());
let semantics = provider_key_auth_semantics(&key, "grok");
assert!(semantics.oauth_managed());
assert_eq!(
semantics.credential_kind(),
@@ -331,6 +339,22 @@ mod tests {
);
}
#[test]
fn recognizes_windsurf_oauth_as_bearer_runtime() {
let semantics = provider_key_auth_semantics(&sample_key("oauth"), "windsurf");
assert!(semantics.oauth_managed());
assert!(!semantics.can_refresh_oauth());
assert_eq!(
semantics.credential_kind(),
ProviderKeyCredentialKind::OAuthSession
);
assert_eq!(
semantics.runtime_auth_kind(),
ProviderKeyRuntimeAuthKind::Bearer
);
}
#[test]
fn refresh_capability_requires_stored_refresh_token() {
let semantics = provider_key_auth_semantics(&sample_key("oauth"), "codex");

View File

@@ -347,6 +347,78 @@ async fn gateway_counts_keys_with_null_api_formats_for_each_fixed_provider_endpo
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_counts_inherited_windsurf_key_formats_for_admin_provider_endpoints() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/endpoints/providers/provider-windsurf/endpoints",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let mut key = sample_key(
"key-windsurf-a",
"provider-windsurf",
"openai:chat",
"oauth-secret",
);
key.auth_type = "oauth".to_string();
key.api_formats = None;
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
provider.provider_type = "windsurf".to_string();
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![sample_endpoint(
"endpoint-windsurf-chat",
"provider-windsurf",
"openai:chat",
"https://server.codeium.com",
)],
vec![key],
));
let (_, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
provider_catalog_repository,
)),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!(
"{gateway_url}/api/admin/endpoints/providers/provider-windsurf/endpoints?skip=0&limit=50"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
let items = payload.as_array().expect("payload should be an array");
assert_eq!(items.len(), 1);
assert_eq!(items[0]["id"], "endpoint-windsurf-chat");
assert_eq!(items[0]["total_keys"], 1);
assert_eq!(items[0]["active_keys"], 1);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_returns_service_unavailable_for_admin_provider_endpoint_create_when_catalog_writer_unavailable(
) {

View File

@@ -216,6 +216,23 @@ fn codex_quota_execution_result(request_id: &str) -> serde_json::Value {
})
}
fn windsurf_register_user_execution_result(request_id: &str) -> serde_json::Value {
json!({
"request_id": request_id,
"status_code": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"json_body": {
"apiKey": "devin-session-token$registered",
"name": "Windsurf User",
"apiServerUrl": "https://server.codeium.com"
}
}
})
}
fn assert_single_provider_oauth_refresh_token_plan<'a>(
plans: &'a [ExecutionPlan],
) -> &'a ExecutionPlan {
@@ -281,18 +298,404 @@ async fn gateway_handles_admin_provider_oauth_supported_types_locally_with_trust
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
let items = payload.as_array().expect("items should be array");
assert_eq!(items.len(), 5);
assert_eq!(items.len(), 6);
assert_eq!(items[0]["provider_type"], "claude_code");
assert_eq!(items[1]["provider_type"], "codex");
assert_eq!(items[2]["provider_type"], "chatgpt_web");
assert_eq!(items[3]["provider_type"], "gemini_cli");
assert_eq!(items[4]["provider_type"], "antigravity");
assert_eq!(items[5]["provider_type"], "windsurf");
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_oauth_device_authorize_for_windsurf_browser() {
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
provider.provider_type = "windsurf".to_string();
let endpoint = sample_endpoint(
"endpoint-windsurf-chat",
"provider-windsurf",
"openai:chat",
"https://server.codeium.com",
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![endpoint],
vec![],
));
let state = AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
provider_catalog_repository,
));
let response = local_admin_provider_oauth_response(
&state,
http::Method::POST,
"/api/admin/provider-oauth/providers/provider-windsurf/device-authorize",
Some(json!({
"auth_type": "browser",
"login_option": "github",
"proxy_node_id": "proxy-node-windsurf"
})),
)
.await;
assert_eq!(response.status(), 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 session_id = payload["session_id"]
.as_str()
.expect("session_id should exist");
assert_eq!(payload["auth_type"], "browser");
assert_eq!(payload["login_option"], "github");
assert_eq!(payload["redirect_uri"], "show-auth-token");
assert_eq!(payload["callback_required"], true);
let authorization_url = payload["verification_uri_complete"]
.as_str()
.expect("authorization url should exist");
let parsed = url::Url::parse(authorization_url).expect("authorization url should parse");
let params = parsed
.query_pairs()
.into_owned()
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(
parsed.as_str().split('?').next(),
Some("https://windsurf.com/windsurf/signin")
);
assert_eq!(
params.get("response_type").map(String::as_str),
Some("token")
);
assert_eq!(params.get("state").map(String::as_str), Some(session_id));
assert_eq!(
params.get("redirect_uri").map(String::as_str),
Some("show-auth-token")
);
assert_eq!(
params.get("redirect_parameters_type").map(String::as_str),
Some("query")
);
let stored = state
.load_provider_oauth_device_session_for_tests(&format!("device_auth_session:{session_id}"))
.expect("device session should be stored");
let stored: serde_json::Value =
serde_json::from_str(&stored).expect("device session json should parse");
assert_eq!(stored["provider_id"], "provider-windsurf");
assert_eq!(stored["auth_type"], "browser");
assert_eq!(stored["social_provider"], "github");
assert_eq!(stored["redirect_uri"], "show-auth-token");
assert_eq!(stored["proxy_node_id"], "proxy-node-windsurf");
assert_eq!(stored["status"], "pending");
}
#[tokio::test]
async fn gateway_rejects_generic_oauth_start_for_windsurf_provider() {
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
provider.provider_type = "windsurf".to_string();
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![],
vec![],
));
let state = AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
provider_catalog_repository,
));
let response = local_admin_provider_oauth_response(
&state,
http::Method::POST,
"/api/admin/provider-oauth/providers/provider-windsurf/start",
None,
)
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
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");
assert!(
payload["detail"]
.as_str()
.is_some_and(|detail| detail.contains("浏览器登录")),
"payload={payload}"
);
}
#[tokio::test]
async fn gateway_handles_admin_provider_oauth_device_poll_for_windsurf_callback_token() {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| {
let execution_plans_inner = Arc::clone(&execution_plans_clone);
async move {
execution_plans_inner
.lock()
.expect("mutex should lock")
.push(plan.clone());
if plan.request_id == "provider-oauth:windsurf-register:new" {
return Json(windsurf_register_user_execution_result(&plan.request_id));
}
Json(json!({
"request_id": plan.request_id,
"status_code": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"json_body": {}
}
}))
}
}),
);
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
provider.provider_type = "windsurf".to_string();
let endpoint = sample_endpoint(
"endpoint-windsurf-chat",
"provider-windsurf",
"openai:chat",
"https://server.codeium.com",
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![endpoint],
vec![],
));
let mut proxy_node = sample_proxy_node("proxy-node-windsurf");
proxy_node.status = "online".to_string();
proxy_node.is_manual = true;
proxy_node.tunnel_mode = false;
proxy_node.tunnel_connected = false;
proxy_node.proxy_url = Some("http://proxy.example:8080".to_string());
let proxy_node_repository = Arc::new(InMemoryProxyNodeRepository::seed(vec![proxy_node]));
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let state = build_state_with_execution_runtime_override(execution_runtime_url)
.with_data_state_for_tests(
GatewayDataState::with_provider_catalog_repository_for_tests(
provider_catalog_repository.clone(),
)
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY)
.attach_proxy_node_repository_for_tests(proxy_node_repository),
)
.with_provider_oauth_device_session_entry_for_tests(
"session-windsurf",
json!({
"provider_id": "provider-windsurf",
"region": "",
"client_id": "",
"client_secret": "",
"device_code": "",
"auth_type": "browser",
"social_provider": "google",
"code_verifier": null,
"redirect_uri": "show-auth-token",
"machine_id": "123e4567-e89b-12d3-a456-426614174000",
"interval": 5,
"expires_at_unix_secs": 4_102_444_800u64,
"status": "pending",
"proxy_node_id": "proxy-node-windsurf",
"created_at_unix_ms": 1_711_000_000u64,
"key_id": null,
"email": null,
"replaced": false,
"error_msg": null,
}),
);
let response = local_admin_provider_oauth_response(
&state,
http::Method::POST,
"/api/admin/provider-oauth/providers/provider-windsurf/device-poll",
Some(json!({
"session_id": "session-windsurf",
"callback_url": "https://windsurf.com/show-auth-token?token=firebase-id-token&state=session-windsurf&provider=google"
})),
)
.await;
let status = response.status();
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");
assert_eq!(status, StatusCode::OK, "payload={payload}");
assert_eq!(payload["status"], "authorized");
assert_eq!(payload["replaced"], false);
let stored = state
.load_provider_oauth_device_session_for_tests("device_auth_session:session-windsurf")
.expect("device session should persist");
let stored: serde_json::Value =
serde_json::from_str(&stored).expect("device session json should parse");
assert_eq!(stored["status"], "authorized");
let key_id = stored["key_id"]
.as_str()
.expect("key_id should be stored")
.to_string();
assert_eq!(payload["key_id"], key_id);
let persisted = provider_catalog_repository
.list_keys_by_ids(std::slice::from_ref(&key_id))
.await
.expect("keys should load")
.into_iter()
.next()
.expect("persisted key should exist");
assert_eq!(persisted.auth_type, "oauth");
assert_eq!(
persisted.proxy,
Some(json!({"node_id": "proxy-node-windsurf", "enabled": true}))
);
let decrypted_api_key = decrypt_python_fernet_ciphertext(
DEVELOPMENT_ENCRYPTION_KEY,
persisted
.encrypted_api_key
.as_deref()
.expect("api key should be present"),
)
.expect("api key should decrypt");
assert_eq!(decrypted_api_key, "devin-session-token$registered");
let decrypted_auth_config = decrypt_python_fernet_ciphertext(
DEVELOPMENT_ENCRYPTION_KEY,
persisted
.encrypted_auth_config
.as_deref()
.expect("auth config should exist"),
)
.expect("auth config should decrypt");
let auth_config: serde_json::Value =
serde_json::from_str(&decrypted_auth_config).expect("auth config should parse");
assert_eq!(auth_config["provider_type"], "windsurf");
assert_eq!(auth_config["auth_method"], "browser");
assert_eq!(auth_config["register_source"], "new");
assert_eq!(auth_config["social_provider"], "google");
{
let plans = execution_plans.lock().expect("mutex should lock");
let register_plan = plans
.iter()
.find(|plan| plan.request_id == "provider-oauth:windsurf-register:new")
.expect("register plan should execute");
assert_eq!(register_plan.method, "POST");
assert_eq!(
register_plan
.body
.json_body
.as_ref()
.and_then(|body| body.get("firebase_id_token"))
.and_then(serde_json::Value::as_str),
Some("firebase-id-token")
);
assert_eq!(
register_plan
.proxy
.as_ref()
.and_then(|proxy| proxy.node_id.as_deref()),
Some("proxy-node-windsurf")
);
}
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_windsurf_callback_state_mismatch_and_missing_token() {
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
provider.provider_type = "windsurf".to_string();
let endpoint = sample_endpoint(
"endpoint-windsurf-chat",
"provider-windsurf",
"openai:chat",
"https://server.codeium.com",
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![endpoint],
vec![],
));
let state = AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
provider_catalog_repository,
))
.with_provider_oauth_device_session_entry_for_tests(
"session-windsurf",
json!({
"provider_id": "provider-windsurf",
"region": "",
"client_id": "",
"client_secret": "",
"device_code": "",
"auth_type": "browser",
"social_provider": "google",
"code_verifier": null,
"redirect_uri": "show-auth-token",
"machine_id": "123e4567-e89b-12d3-a456-426614174000",
"interval": 5,
"expires_at_unix_secs": 4_102_444_800u64,
"status": "pending",
"proxy_node_id": null,
"created_at_unix_ms": 1_711_000_000u64,
"key_id": null,
"email": null,
"replaced": false,
"error_msg": null,
}),
);
let response = local_admin_provider_oauth_response(
&state,
http::Method::POST,
"/api/admin/provider-oauth/providers/provider-windsurf/device-poll",
Some(json!({
"session_id": "session-windsurf",
"callback_url": "https://windsurf.com/show-auth-token?token=firebase-id-token&state=wrong-state"
})),
)
.await;
assert_eq!(response.status(), 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");
assert_eq!(payload["status"], "error");
assert!(payload["error"]
.as_str()
.is_some_and(|error| error.contains("state")));
let response = local_admin_provider_oauth_response(
&state,
http::Method::POST,
"/api/admin/provider-oauth/providers/provider-windsurf/device-poll",
Some(json!({
"session_id": "session-windsurf",
"callback_url": "https://windsurf.com/show-auth-token?state=session-windsurf"
})),
)
.await;
assert_eq!(response.status(), 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");
assert_eq!(payload["status"], "error");
assert!(payload["error"]
.as_str()
.is_some_and(|error| error.contains("token")));
}
#[tokio::test]
async fn gateway_handles_admin_provider_oauth_device_authorize_locally_with_trusted_admin_principal(
) {
@@ -2477,6 +2880,7 @@ async fn gateway_completes_admin_provider_oauth_key_locally_with_trusted_admin_p
.expect("account_state_recheck_error should be string when recheck is attempted");
assert!(
account_state_recheck_error == "wham/usage API 返回状态码 401"
|| account_state_recheck_error == "wham/usage API 返回状态码 403"
|| account_state_recheck_error.starts_with("wham/usage 请求执行失败:"),
"unexpected account_state_recheck_error: {account_state_recheck_error}"
);
@@ -4948,6 +5352,7 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
.expect("account_state_recheck_error should be string when attempted");
assert!(
account_state_recheck_error == "wham/usage API 返回状态码 401"
|| account_state_recheck_error == "wham/usage API 返回状态码 403"
|| account_state_recheck_error.starts_with("wham/usage 请求执行失败:"),
"unexpected account_state_recheck_error: {account_state_recheck_error}"
);
@@ -4997,6 +5402,14 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
stored_key.oauth_invalid_reason.as_deref(),
Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)")
);
} else if account_state_recheck_attempted
&& payload["account_state_recheck_error"] == "wham/usage API 返回状态码 403"
{
assert!(stored_key.oauth_invalid_at_unix_secs.is_some());
assert!(stored_key
.oauth_invalid_reason
.as_deref()
.is_some_and(|reason| reason.contains("(403)")));
} else {
assert_eq!(stored_key.oauth_invalid_at_unix_secs, None);
assert_eq!(stored_key.oauth_invalid_reason, None);

View File

@@ -242,6 +242,144 @@ async fn gateway_handles_admin_provider_query_models_fetches_upstream_for_select
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_fetches_windsurf_model_configs() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| {
let execution_runtime_hits_inner = Arc::clone(&execution_runtime_hits_clone);
async move {
*execution_runtime_hits_inner
.lock()
.expect("mutex should lock") += 1;
assert_eq!(plan.method, "POST");
assert_eq!(
plan.url,
"https://server.codeium.com/exa.api_server_pb.ApiServerService/GetCascadeModelConfigs"
);
assert_eq!(plan.client_api_format, "openai:chat");
assert_eq!(plan.provider_api_format, "windsurf:model_configs");
assert_eq!(plan.model_name.as_deref(), Some("GetCascadeModelConfigs"));
assert_eq!(
plan.headers.get("connect-protocol-version").map(String::as_str),
Some("1")
);
assert_eq!(
plan.body
.json_body
.as_ref()
.and_then(|body| body.get("metadata"))
.and_then(|metadata| metadata.get("apiKey")),
Some(&json!("devin-session-token$abc"))
);
Json(json!({
"request_id": "req-provider-query-windsurf",
"status_code": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"json_body": {
"clientModelConfigs": [{
"modelUid": "claude-sonnet-4-6",
"label": "Claude Sonnet 4.6",
"provider": "anthropic",
"supportsImages": true,
"creditMultiplier": 4
}],
"defaultOverrideModelConfig": {
"modelUid": "claude-sonnet-4-6"
}
}
}
}))
}
}),
);
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let mut provider = sample_provider("provider-windsurf", "Windsurf", 10);
provider.provider_type = "windsurf".to_string();
let mut windsurf_key = sample_key(
"key-windsurf-selected",
"provider-windsurf",
"openai:chat",
"devin-session-token$abc",
);
windsurf_key.auth_type = "oauth".to_string();
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![StoredProviderCatalogEndpoint::new(
"endpoint-windsurf-chat".to_string(),
"provider-windsurf".to_string(),
"openai:chat".to_string(),
Some("chat".to_string()),
Some("primary".to_string()),
true,
)
.expect("endpoint should build")
.with_transport_fields(
"https://server.codeium.com".to_string(),
None,
None,
None,
None,
None,
None,
None,
)
.expect("endpoint transport should build")],
vec![windsurf_key],
));
let gateway = build_router_with_state(
build_state_with_execution_runtime_override(execution_runtime_url)
.with_data_state_for_tests(GatewayDataState::with_provider_transport_reader_for_tests(
provider_catalog_repository,
DEVELOPMENT_ENCRYPTION_KEY.to_string(),
)),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.post(format!("{gateway_url}/api/admin/provider-query/models"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({
"provider_id": "provider-windsurf",
"api_key_id": "key-windsurf-selected"
}))
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["success"], json!(true));
assert_eq!(payload["data"]["error"], serde_json::Value::Null);
assert_eq!(payload["data"]["from_cache"], json!(false));
let models = payload["data"]["models"]
.as_array()
.expect("models should be an array");
assert_eq!(models.len(), 1);
assert_eq!(models[0]["id"], json!("claude-sonnet-4-6"));
assert_eq!(
models[0]["api_formats"],
json!(["openai:chat", "openai:responses", "claude:messages"])
);
assert_eq!(
*execution_runtime_hits.lock().expect("mutex should lock"),
1
);
gateway_handle.abort();
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_with_openai_responses_endpoint() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
@@ -2245,6 +2383,122 @@ async fn gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtim
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_streams_windsurf_connect_upstream_for_admin_model_test() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
assert_eq!(plan.provider_id, "provider-windsurf");
assert_eq!(plan.endpoint_id, "endpoint-windsurf-chat");
assert_eq!(plan.key_id, "key-windsurf-primary");
assert_eq!(plan.provider_api_format, "openai:chat");
assert_eq!(plan.content_type.as_deref(), Some("application/connect+json"));
assert!(plan.stream, "Windsurf Connect model test must stream upstream");
assert_eq!(
plan.body
.json_body
.as_ref()
.and_then(|body| body.get("stream")),
Some(&json!(true))
);
let windsurf_payload = serde_json::to_vec(&json!({
"chatMessage": {
"text": "ok"
}
}))
.expect("windsurf payload should encode");
let mut windsurf_frame = vec![0u8];
windsurf_frame.extend_from_slice(&(windsurf_payload.len() as u32).to_be_bytes());
windsurf_frame.extend_from_slice(&windsurf_payload);
Json(json!({
"request_id": plan.request_id,
"candidate_id": plan.candidate_id,
"status_code": 200,
"headers": {
"content-type": "application/connect+json"
},
"body": {
"body_bytes_b64": base64::engine::general_purpose::STANDARD.encode(windsurf_frame)
},
"telemetry": {
"elapsed_ms": 24
}
}))
}),
);
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let mut provider = sample_provider("provider-windsurf", "Windsurf", 10);
provider.provider_type = "windsurf".to_string();
let mut key = sample_key(
"key-windsurf-primary",
"provider-windsurf",
"openai:chat",
"devin-session-token$abc",
);
key.auth_type = "oauth".to_string();
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![sample_endpoint(
"endpoint-windsurf-chat",
"provider-windsurf",
"openai:chat",
"https://server.codeium.com",
)],
vec![key],
));
let gateway = build_router_with_state(
build_state_with_execution_runtime_override(execution_runtime_url)
.with_data_state_for_tests(GatewayDataState::with_provider_transport_reader_for_tests(
provider_catalog_repository,
DEVELOPMENT_ENCRYPTION_KEY.to_string(),
)),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.post(format!("{gateway_url}/api/admin/provider-query/test-model"))
.header(GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({
"provider_id": "provider-windsurf",
"model": "claude-opus-4-7-medium",
"api_format": "openai:chat",
"endpoint_id": "endpoint-windsurf-chat",
"request_body": {
"model": "claude-opus-4-7-medium",
"messages": [{
"role": "user",
"content": "Hello! This is a test message."
}],
"max_tokens": 30,
"temperature": 0.7,
"stream": true
}
}))
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["success"], json!(true));
assert_eq!(
payload["attempts"][0]["request_body"]["stream"],
json!(true)
);
assert_eq!(
payload["attempts"][0]["response_body"]["choices"][0]["message"]["content"],
json!("ok")
);
gateway_handle.abort();
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_uses_pool_scheduler_order_for_admin_pool_model_test() {
let execution_runtime = Router::new().route(