cc-switch-tui 0.1.1

All-in-One Assistant for Claude Code, Codex, Gemini, OpenCode, OpenClaw & Hermes
Documentation
use serde_json::Value;

use super::types::RectifierConfig;

#[derive(Debug, Clone, Default)]
pub struct RectifyResult {
    pub applied: bool,
    pub removed_thinking_blocks: usize,
    pub removed_redacted_thinking_blocks: usize,
    pub removed_signature_fields: usize,
}

pub fn should_rectify_thinking_signature(
    error_message: Option<&str>,
    config: &RectifierConfig,
) -> bool {
    if !config.enabled || !config.request_thinking_signature {
        return false;
    }

    let Some(message) = error_message else {
        return false;
    };
    let lower = message.to_lowercase();

    if lower.contains("invalid")
        && lower.contains("signature")
        && lower.contains("thinking")
        && lower.contains("block")
    {
        return true;
    }

    if lower.contains("must start with a thinking block") {
        return true;
    }

    if lower.contains("expected")
        && (lower.contains("thinking") || lower.contains("redacted_thinking"))
        && lower.contains("found")
        && lower.contains("tool_use")
    {
        return true;
    }

    if lower.contains("signature") && lower.contains("field required") {
        return true;
    }

    if lower.contains("signature") && lower.contains("extra inputs are not permitted") {
        return true;
    }

    if (lower.contains("thinking") || lower.contains("redacted_thinking"))
        && lower.contains("cannot be modified")
    {
        return true;
    }

    lower.contains("非法请求")
        || lower.contains("illegal request")
        || lower.contains("invalid request")
}

pub fn rectify_anthropic_request(body: &mut Value) -> RectifyResult {
    let mut result = RectifyResult::default();

    let messages = match body.get_mut("messages").and_then(|m| m.as_array_mut()) {
        Some(messages) => messages,
        None => return result,
    };

    for message in messages.iter_mut() {
        let content = match message.get_mut("content").and_then(|c| c.as_array_mut()) {
            Some(content) => content,
            None => continue,
        };

        let mut new_content = Vec::with_capacity(content.len());
        let mut modified = false;

        for block in content.iter() {
            match block.get("type").and_then(|t| t.as_str()) {
                Some("thinking") => {
                    result.removed_thinking_blocks += 1;
                    modified = true;
                    continue;
                }
                Some("redacted_thinking") => {
                    result.removed_redacted_thinking_blocks += 1;
                    modified = true;
                    continue;
                }
                _ => {}
            }

            if block.get("signature").is_some() {
                let mut clone = block.clone();
                if let Some(object) = clone.as_object_mut() {
                    object.remove("signature");
                    result.removed_signature_fields += 1;
                    modified = true;
                }
                new_content.push(clone);
                continue;
            }

            new_content.push(block.clone());
        }

        if modified {
            result.applied = true;
            *content = new_content;
        }
    }

    let messages_snapshot = body
        .get("messages")
        .and_then(|messages| messages.as_array())
        .map(|messages| messages.to_vec())
        .unwrap_or_default();

    if should_remove_top_level_thinking(body, &messages_snapshot) {
        if let Some(object) = body.as_object_mut() {
            object.remove("thinking");
            result.applied = true;
        }
    }

    result
}

fn should_remove_top_level_thinking(body: &Value, messages: &[Value]) -> bool {
    let thinking_enabled = body
        .get("thinking")
        .and_then(|thinking| thinking.get("type"))
        .and_then(|value| value.as_str())
        == Some("enabled");

    if !thinking_enabled {
        return false;
    }

    let last_assistant = messages
        .iter()
        .rev()
        .find(|message| message.get("role").and_then(|role| role.as_str()) == Some("assistant"));

    let Some(last_content) = last_assistant
        .and_then(|message| message.get("content"))
        .and_then(|content| content.as_array())
        .filter(|content| !content.is_empty())
    else {
        return false;
    };

    let first_block_type = last_content
        .first()
        .and_then(|block| block.get("type"))
        .and_then(|value| value.as_str());
    if matches!(
        first_block_type,
        Some("thinking") | Some("redacted_thinking")
    ) {
        return false;
    }

    last_content
        .iter()
        .any(|block| block.get("type").and_then(|value| value.as_str()) == Some("tool_use"))
}

pub fn normalize_thinking_type(body: Value) -> Value {
    body
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn enabled_config() -> RectifierConfig {
        RectifierConfig::default()
    }

    #[test]
    fn detects_invalid_signature_errors() {
        assert!(should_rectify_thinking_signature(
            Some("messages.1.content.0: Invalid `signature` in `thinking` block"),
            &enabled_config(),
        ));
    }

    #[test]
    fn removes_legacy_thinking_blocks_and_signatures() {
        let mut body = json!({
            "model": "claude-test",
            "messages": [{
                "role": "assistant",
                "content": [
                    { "type": "thinking", "thinking": "t", "signature": "sig" },
                    { "type": "text", "text": "hello", "signature": "sig_text" },
                    { "type": "redacted_thinking", "data": "r", "signature": "sig_redacted" }
                ]
            }]
        });

        let result = rectify_anthropic_request(&mut body);

        assert!(result.applied);
        assert_eq!(result.removed_thinking_blocks, 1);
        assert_eq!(result.removed_redacted_thinking_blocks, 1);
        assert_eq!(result.removed_signature_fields, 1);
        let content = body["messages"][0]["content"].as_array().unwrap();
        assert_eq!(content.len(), 1);
        assert_eq!(content[0]["type"], "text");
        assert!(content[0].get("signature").is_none());
    }

    #[test]
    fn removes_top_level_thinking_for_tool_use_without_prefix() {
        let mut body = json!({
            "model": "claude-test",
            "thinking": { "type": "enabled", "budget_tokens": 1024 },
            "messages": [{
                "role": "assistant",
                "content": [
                    { "type": "tool_use", "id": "toolu_1", "name": "WebSearch", "input": {} }
                ]
            }]
        });

        let result = rectify_anthropic_request(&mut body);

        assert!(result.applied);
        assert!(body.get("thinking").is_none());
    }
}