fix(provider): 修复 Windsurf PR CI 失败

This commit is contained in:
Entropy.Xu
2026-05-21 02:21:35 +08:00
parent 65c361115a
commit 4112a8b2ea
11 changed files with 38 additions and 37 deletions

View File

@@ -934,7 +934,11 @@ async fn resolve_openai_chat_to_openai_image_payload_parts(
let upstream_url = if is_chatgpt_web { let upstream_url = if is_chatgpt_web {
chatgpt_web_image_internal_url(&transport.endpoint.base_url) chatgpt_web_image_internal_url(&transport.endpoint.base_url)
} else { } else {
build_openai_image_upstream_url(transport, parts.uri.query()) build_openai_image_upstream_url(
transport,
Some("/v1/images/generations"),
parts.uri.query(),
)
}; };
let Some(mut provider_request_headers) = let Some(mut provider_request_headers) =
build_openai_image_headers(ProviderOpenAiImageHeadersInput { build_openai_image_headers(ProviderOpenAiImageHeadersInput {

View File

@@ -1727,7 +1727,7 @@ async fn execute_execution_runtime_sync_impl(
} }
let (mut result_error_type, mut result_error_message) = let (mut result_error_type, mut result_error_message) =
execution_error_details(result.error.as_ref(), body_json.as_ref()); execution_error_details(result.error.as_ref(), body_json.as_ref());
if result.status_code < 400 { if result.status_code < 400 && body_json.is_none() {
if let Some(error_body_json) = if let Some(error_body_json) =
extract_provider_private_stream_error_body(report_context.as_ref(), &body_bytes) extract_provider_private_stream_error_body(report_context.as_ref(), &body_bytes)
{ {

View File

@@ -25,6 +25,7 @@ use serde_json::json;
use serde_json::Value; use serde_json::Value;
use thiserror::Error; use thiserror::Error;
use crate::ai_serving::api::extract_provider_private_stream_error_body;
#[cfg(test)] #[cfg(test)]
use crate::execution_runtime::remote_compat::execute_sync_plan_via_remote_execution_runtime; use crate::execution_runtime::remote_compat::execute_sync_plan_via_remote_execution_runtime;
use crate::execution_runtime::windsurf::maybe_execute_windsurf_sync; use crate::execution_runtime::windsurf::maybe_execute_windsurf_sync;
@@ -1466,11 +1467,8 @@ pub(crate) fn build_execution_response_body(
return Ok(None); return Ok(None);
} }
if let Some(body_json) = if let Some(body_json) = extract_provider_private_stream_error_body(None, decoded_body_bytes)
aether_ai_formats::api::extract_provider_private_stream_error_body(None, decoded_body_bytes) .or_else(|| extract_provider_private_stream_error_body(None, body_bytes))
.or_else(|| {
aether_ai_formats::api::extract_provider_private_stream_error_body(None, body_bytes)
})
{ {
return Ok(Some(ResponseBody { return Ok(Some(ResponseBody {
json_body: Some(body_json), json_body: Some(body_json),

View File

@@ -345,7 +345,6 @@ async fn prepare_windsurf_cascade(
native_allowlist: native_bridge native_allowlist: native_bridge
.map(|bridge| bridge.native_allowlist.clone()) .map(|bridge| bridge.native_allowlist.clone())
.unwrap_or_default(), .unwrap_or_default(),
..SendCascadeMessageOptions::default()
}; };
let send_payload = build_send_cascade_message_request_with_options( let send_payload = build_send_cascade_message_request_with_options(
&input.api_key, &input.api_key,
@@ -414,7 +413,7 @@ async fn read_windsurf_key_upstream_metadata(
} }
match state match state
.read_provider_catalog_keys_by_ids(&[plan.key_id.clone()]) .read_provider_catalog_keys_by_ids(std::slice::from_ref(&plan.key_id))
.await .await
{ {
Ok(keys) => keys Ok(keys) => keys
@@ -1693,10 +1692,8 @@ fn build_cascade_message_with_options(
}; };
let text = text.trim(); let text = text.trim();
match role { match role {
"system" => { "system" if !text.is_empty() => {
if !text.is_empty() { system_text.push(text.to_string());
system_text.push(text.to_string());
}
} }
"assistant" => { "assistant" => {
let assistant_text = assistant_message_text_for_cascade(message, text, dialect); let assistant_text = assistant_message_text_for_cascade(message, text, dialect);

View File

@@ -25,6 +25,7 @@ use crate::handlers::admin::provider::oauth::runtime::{
use crate::handlers::admin::provider::oauth::state::{ use crate::handlers::admin::provider::oauth::state::{
admin_provider_oauth_template, exchange_admin_provider_oauth_refresh_token, 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::handlers::admin::request::{AdminAppState, AdminProviderOAuthTemplate};
use crate::GatewayError; use crate::GatewayError;
use aether_admin::provider::oauth::parse_admin_provider_oauth_kiro_batch_import_entries; use aether_admin::provider::oauth::parse_admin_provider_oauth_kiro_batch_import_entries;
@@ -167,10 +168,6 @@ async fn resolve_admin_provider_oauth_batch_import_tokens(
}); });
} }
let Some(template) = template else {
return Err(ADMIN_PROVIDER_OAUTH_DATA_UNAVAILABLE_DETAIL.to_string());
};
if let Some(refresh_token) = refresh_token { if let Some(refresh_token) = refresh_token {
let Some(template) = template else { let Some(template) = template else {
if provider_type_supports_access_token_import(provider_type) { if provider_type_supports_access_token_import(provider_type) {
@@ -301,7 +298,10 @@ pub(super) async fn execute_admin_provider_oauth_batch_import(
}; };
let template = admin_provider_oauth_template(provider_type); let template = admin_provider_oauth_template(provider_type);
if template.is_none() && !provider_type.eq_ignore_ascii_case("windsurf") { if template.is_none()
&& !provider_type.eq_ignore_ascii_case("windsurf")
&& !provider_type_supports_access_token_import(provider_type)
{
return Ok(AdminProviderOAuthBatchImportOutcome { return Ok(AdminProviderOAuthBatchImportOutcome {
total: entries.len(), total: entries.len(),
success: 0, success: 0,

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::errors::build_internal_control_error_response;
use crate::handlers::admin::provider::oauth::state::{ 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, is_fixed_provider_type_for_provider_oauth,
}; };
use crate::handlers::admin::provider::shared::paths::admin_provider_oauth_batch_import_provider_id; use crate::handlers::admin::provider::shared::paths::admin_provider_oauth_batch_import_provider_id;

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::errors::build_internal_control_error_response;
use crate::handlers::admin::provider::oauth::state::{ 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, is_fixed_provider_type_for_provider_oauth,
}; };
use crate::handlers::admin::provider::shared::paths::admin_provider_oauth_batch_import_task_provider_id; use crate::handlers::admin::provider::shared::paths::admin_provider_oauth_batch_import_task_provider_id;

View File

@@ -508,9 +508,6 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
"Refresh Token 或 Access Token 不能为空", "Refresh Token 或 Access Token 不能为空",
)); ));
} }
let Some(template) = admin_provider_oauth_template(&provider_type) else {
return Ok(build_admin_provider_oauth_backend_unavailable_response());
};
match resolve_admin_provider_oauth_single_import_tokens( match resolve_admin_provider_oauth_single_import_tokens(
state, state,
template, template,

View File

@@ -2880,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"); .expect("account_state_recheck_error should be string when recheck is attempted");
assert!( assert!(
account_state_recheck_error == "wham/usage API 返回状态码 401" account_state_recheck_error == "wham/usage API 返回状态码 401"
|| account_state_recheck_error == "wham/usage API 返回状态码 403"
|| account_state_recheck_error.starts_with("wham/usage 请求执行失败:"), || account_state_recheck_error.starts_with("wham/usage 请求执行失败:"),
"unexpected account_state_recheck_error: {account_state_recheck_error}" "unexpected account_state_recheck_error: {account_state_recheck_error}"
); );
@@ -5351,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"); .expect("account_state_recheck_error should be string when attempted");
assert!( assert!(
account_state_recheck_error == "wham/usage API 返回状态码 401" account_state_recheck_error == "wham/usage API 返回状态码 401"
|| account_state_recheck_error == "wham/usage API 返回状态码 403"
|| account_state_recheck_error.starts_with("wham/usage 请求执行失败:"), || account_state_recheck_error.starts_with("wham/usage 请求执行失败:"),
"unexpected account_state_recheck_error: {account_state_recheck_error}" "unexpected account_state_recheck_error: {account_state_recheck_error}"
); );
@@ -5400,6 +5402,14 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
stored_key.oauth_invalid_reason.as_deref(), stored_key.oauth_invalid_reason.as_deref(),
Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)") 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 { } else {
assert_eq!(stored_key.oauth_invalid_at_unix_secs, None); assert_eq!(stored_key.oauth_invalid_at_unix_secs, None);
assert_eq!(stored_key.oauth_invalid_reason, None); assert_eq!(stored_key.oauth_invalid_reason, None);

View File

@@ -1705,6 +1705,7 @@ mod tests {
"modelUid": "claude-sonnet-4-6" "modelUid": "claude-sonnet-4-6"
} }
}), }),
status_code: 200,
}; };
let outcome = fetch_models_from_transports(&runtime, &[sample_windsurf_transport()]) let outcome = fetch_models_from_transports(&runtime, &[sample_windsurf_transport()])
.await .await

View File

@@ -315,7 +315,7 @@ mod tests {
let model = resolve_windsurf_model("gpt-5-5-low").expect("model should resolve"); let model = resolve_windsurf_model("gpt-5-5-low").expect("model should resolve");
assert_eq!(model.canonical_name, "gpt-5.5-low"); assert_eq!(model.canonical_name, "gpt-5.5-low");
assert_eq!(model.model_uid.as_deref(), Some("gpt-5-5-low")); assert_eq!(model.model_uid, Some("gpt-5-5-low"));
assert_eq!(model.enum_value, 0); assert_eq!(model.enum_value, 0);
assert_eq!(model.credit_multiplier, 1.0); assert_eq!(model.credit_multiplier, 1.0);
} }
@@ -325,7 +325,7 @@ mod tests {
let model = resolve_windsurf_model("claude-opus-4.7").expect("model should resolve"); let model = resolve_windsurf_model("claude-opus-4.7").expect("model should resolve");
assert_eq!(model.canonical_name, "claude-opus-4-7-medium"); assert_eq!(model.canonical_name, "claude-opus-4-7-medium");
assert_eq!(model.model_uid.as_deref(), Some("claude-opus-4-7-medium")); assert_eq!(model.model_uid, Some("claude-opus-4-7-medium"));
assert_eq!(model.enum_value, 0); assert_eq!(model.enum_value, 0);
assert_eq!(model.credit_multiplier, 8.0); assert_eq!(model.credit_multiplier, 8.0);
} }
@@ -335,7 +335,7 @@ mod tests {
let model = resolve_windsurf_model("gpt-5-5-low-priority").expect("model should resolve"); let model = resolve_windsurf_model("gpt-5-5-low-priority").expect("model should resolve");
assert_eq!(model.canonical_name, "gpt-5.5-low-fast"); assert_eq!(model.canonical_name, "gpt-5.5-low-fast");
assert_eq!(model.model_uid.as_deref(), Some("gpt-5-5-low-priority")); assert_eq!(model.model_uid, Some("gpt-5-5-low-priority"));
assert_eq!(model.credit_multiplier, 2.0); assert_eq!(model.credit_multiplier, 2.0);
} }
@@ -343,21 +343,18 @@ mod tests {
fn resolves_full_gpt55_effort_ladder_and_priority_aliases() { fn resolves_full_gpt55_effort_ladder_and_priority_aliases() {
let none = resolve_windsurf_model("gpt-5-5-none").expect("none should resolve"); let none = resolve_windsurf_model("gpt-5-5-none").expect("none should resolve");
assert_eq!(none.canonical_name, "gpt-5.5-none"); assert_eq!(none.canonical_name, "gpt-5.5-none");
assert_eq!(none.model_uid.as_deref(), Some("gpt-5-5-none")); assert_eq!(none.model_uid, Some("gpt-5-5-none"));
assert_eq!(none.credit_multiplier, 1.0); assert_eq!(none.credit_multiplier, 1.0);
let high = resolve_windsurf_model("gpt-5.5-high").expect("high should resolve"); let high = resolve_windsurf_model("gpt-5.5-high").expect("high should resolve");
assert_eq!(high.canonical_name, "gpt-5.5-high"); assert_eq!(high.canonical_name, "gpt-5.5-high");
assert_eq!(high.model_uid.as_deref(), Some("gpt-5-5-high")); assert_eq!(high.model_uid, Some("gpt-5-5-high"));
assert_eq!(high.credit_multiplier, 4.0); assert_eq!(high.credit_multiplier, 4.0);
let xhigh_fast = resolve_windsurf_model("gpt-5-5-xhigh-priority") let xhigh_fast = resolve_windsurf_model("gpt-5-5-xhigh-priority")
.expect("xhigh priority should resolve"); .expect("xhigh priority should resolve");
assert_eq!(xhigh_fast.canonical_name, "gpt-5.5-xhigh-fast"); assert_eq!(xhigh_fast.canonical_name, "gpt-5.5-xhigh-fast");
assert_eq!( assert_eq!(xhigh_fast.model_uid, Some("gpt-5-5-xhigh-priority"));
xhigh_fast.model_uid.as_deref(),
Some("gpt-5-5-xhigh-priority")
);
assert_eq!(xhigh_fast.credit_multiplier, 16.0); assert_eq!(xhigh_fast.credit_multiplier, 16.0);
} }
@@ -365,14 +362,11 @@ mod tests {
fn resolves_windsurfapi_catalog_aliases_beyond_gpt55() { fn resolves_windsurfapi_catalog_aliases_beyond_gpt55() {
let gpt52_medium = resolve_windsurf_model("gpt-5.2-medium").expect("gpt-5.2 medium alias"); let gpt52_medium = resolve_windsurf_model("gpt-5.2-medium").expect("gpt-5.2 medium alias");
assert_eq!(gpt52_medium.canonical_name, "gpt-5.2"); assert_eq!(gpt52_medium.canonical_name, "gpt-5.2");
assert_eq!( assert_eq!(gpt52_medium.model_uid, Some("MODEL_GPT_5_2_MEDIUM"));
gpt52_medium.model_uid.as_deref(),
Some("MODEL_GPT_5_2_MEDIUM")
);
let haiku = resolve_windsurf_model("claude-haiku-4-5-20251001").expect("dated haiku alias"); let haiku = resolve_windsurf_model("claude-haiku-4-5-20251001").expect("dated haiku alias");
assert_eq!(haiku.canonical_name, "claude-4.5-haiku"); assert_eq!(haiku.canonical_name, "claude-4.5-haiku");
assert_eq!(haiku.model_uid.as_deref(), Some("MODEL_PRIVATE_11")); assert_eq!(haiku.model_uid, Some("MODEL_PRIVATE_11"));
let uid = resolve_windsurf_model("MODEL_GPT_5_2_LOW").expect("model uid alias"); let uid = resolve_windsurf_model("MODEL_GPT_5_2_LOW").expect("model uid alias");
assert_eq!(uid.canonical_name, "gpt-5.2-low"); assert_eq!(uid.canonical_name, "gpt-5.2-low");