Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 170 additions & 7 deletions src-tauri/src/proxy/forwarder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use super::{
thinking_rectifier::{
normalize_thinking_type, rectify_anthropic_request, should_rectify_thinking_signature,
},
tool_strict_rectifier::{rectify_tool_strict, should_rectify_tool_strict},
types::{CopilotOptimizerConfig, OptimizerConfig, ProxyStatus, RectifierConfig},
ProxyError,
};
Expand Down Expand Up @@ -542,15 +543,104 @@ impl RequestForwarder {
connection_guard: None,
});
}
Err(e) => {
// 检测是否需要触发整流器(仅 Claude/ClaudeAuth 供应商)
let provider_type = ProviderType::from_app_type_and_config(app_type, provider);
let is_anthropic_provider = matches!(
provider_type,
ProviderType::Claude | ProviderType::ClaudeAuth
);
Err(mut e) => {
// Anthropic-native requests include Claude providers and the
// Codex/GrokBuild Responses -> Anthropic bridge.
let is_anthropic_provider =
uses_anthropic_request_format(app_type, provider, endpoint);
let mut signature_rectifier_non_retryable_client_error = false;

if should_retry_tool_strict(&self.rectifier_config, is_anthropic_provider, &e) {
let removed = rectify_tool_strict(&mut provider_body);
if removed > 0 {
log::info!(
"[{app_type_str}] [ToolStrict] Upstream rejected tool strict; retrying provider={} without {removed} strict field(s)",
provider.id
);

match self
.forward(
app_type,
&method,
provider,
endpoint,
&provider_body,
&headers,
&extensions,
adapter.as_ref(),
)
.await
{
Ok((response, claude_api_format, outbound_model)) => {
log::info!(
"[{app_type_str}] [ToolStrict] Compatibility retry succeeded"
);
self.record_success_result(
&provider.id,
app_type_str,
used_half_open_permit,
)
.await;

{
let mut current_providers =
self.current_providers.write().await;
current_providers.insert(
app_type_str.to_string(),
(provider.id.clone(), provider.name.clone()),
);
}

{
let mut status = self.status.write().await;
status.success_requests += 1;
status.last_error = None;
let should_switch =
self.current_provider_id_at_start.as_str()
!= provider.id.as_str();
if should_switch {
status.failover_count += 1;
let fm = self.failover_manager.clone();
let ah = self.app_handle.clone();
let pid = provider.id.clone();
let pname = provider.name.clone();
let at = app_type_str.to_string();

tokio::spawn(async move {
let _ = fm
.try_switch(ah.as_ref(), &at, &pid, &pname)
.await;
});
}
if status.total_requests > 0 {
status.success_rate = (status.success_requests as f32
/ status.total_requests as f32)
* 100.0;
}
}

return Ok(ForwardResult {
response,
provider: provider.clone(),
claude_api_format,
outbound_model,
connection_guard: None,
});
}
Err(retry_err) => {
log::warn!(
"[{app_type_str}] [ToolStrict] Compatibility retry still failed: {retry_err}"
);
// Continue through the other rectifiers with the
// stripped request and the latest error. A request
// may need both this Bedrock compatibility fallback
// and an existing thinking/media repair.
e = retry_err;
}
}
}
}

if self.media_retry_should_trigger(
adapter.name(),
media_rectifier_retried,
Expand Down Expand Up @@ -2719,6 +2809,22 @@ fn extract_error_message(error: &ProxyError) -> Option<String> {
}
}

fn uses_anthropic_request_format(app_type: &AppType, provider: &Provider, endpoint: &str) -> bool {
matches!(
ProviderType::from_app_type_and_config(app_type, provider),
ProviderType::Claude | ProviderType::ClaudeAuth
) || (matches!(app_type, AppType::Codex | AppType::GrokBuild)
&& super::providers::should_convert_codex_responses_to_anthropic(provider, endpoint))
}

fn should_retry_tool_strict(
rectifier_config: &RectifierConfig,
is_anthropic_provider: bool,
error: &ProxyError,
) -> bool {
rectifier_config.enabled && is_anthropic_provider && should_rectify_tool_strict(error)
}

/// 检测 Provider 是否为 Bedrock(通过 CLAUDE_CODE_USE_BEDROCK 环境变量判断)
fn is_bedrock_provider(provider: &Provider) -> bool {
provider
Expand Down Expand Up @@ -3617,6 +3723,63 @@ mod tests {
}
}

#[test]
fn anthropic_rectifiers_cover_codex_responses_bridge() {
let mut provider = test_provider_with_type(None);
provider.meta = Some(crate::provider::ProviderMeta {
api_format: Some("anthropic".to_string()),
..Default::default()
});

assert!(uses_anthropic_request_format(
&AppType::Codex,
&provider,
"/responses"
));
assert!(!uses_anthropic_request_format(
&AppType::Codex,
&provider,
"/chat/completions"
));

provider.meta.as_mut().unwrap().api_format = Some("openai_responses".to_string());
assert!(!uses_anthropic_request_format(
&AppType::Codex,
&provider,
"/responses"
));
}

#[test]
fn tool_strict_retry_honors_rectifier_opt_out() {
let error = ProxyError::UpstreamError {
status: 400,
body: Some(
r#"{"error":{"message":"tools.0.custom.strict: Extra inputs are not permitted"}}"#
.to_string(),
),
};

assert!(should_retry_tool_strict(
&RectifierConfig::default(),
true,
&error
));
assert!(!should_retry_tool_strict(
&RectifierConfig {
enabled: false,
..RectifierConfig::default()
},
true,
&error
));
assert!(!should_retry_tool_strict(
&RectifierConfig::default(),
false,
&error
));
}

fn test_forwarder(
non_streaming_timeout: Duration,
streaming_first_byte_timeout: Duration,
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/proxy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub mod thinking_budget_rectifier;
pub mod thinking_optimizer;
pub mod thinking_rectifier;
pub(crate) mod tool_media;
pub mod tool_strict_rectifier;
pub(crate) mod types;
pub mod usage;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,7 @@ mod tests {
"max_output_tokens": 100,
"input": [{ "role": "user", "content": "hi" }],
"tools": [
{ "type": "function", "name": "get_weather", "description": "d", "parameters": {"type": "object"} },
{ "type": "function", "name": "get_weather", "description": "d", "strict": true, "parameters": {"type": "object"} },
{ "type": "web_search" },
{ "type": "custom", "name": "apply_patch" }
]
Expand All @@ -1693,6 +1693,7 @@ mod tests {
assert_eq!(tools.len(), 2);
assert_eq!(tools[0]["name"], "get_weather");
assert_eq!(tools[0]["input_schema"]["type"], "object");
assert_eq!(tools[0]["strict"], true);
assert!(tools[0].get("parameters").is_none());
assert_eq!(tools[1]["name"], "apply_patch");
}
Expand Down
Loading
Loading