gproxy-transform 3.0.0-alpha.0

GPROXY v3 pairwise protocol transforms
Documentation
use gproxy_protocol::{claude, openai};

use crate::TransformError;
use crate::common::tools::callers_to_openai;

pub(super) fn convert(tool: claude::Tool) -> Result<openai::ResponseTool, TransformError> {
    let tool = normalize(tool)?;
    Ok(match tool {
        claude::Tool::Custom(tool) => custom(tool)?,
        claude::Tool::Command(command) => match command {
            claude::CommandTool::Bash20241022(tool) => shell(tool.common),
            claude::CommandTool::Bash20250124(tool) => shell(tool.common),
            claude::CommandTool::CodeExecution20250522(tool) => {
                code_interpreter(tool.common.allowed_callers)
            }
            claude::CommandTool::CodeExecution20250825(tool) => {
                code_interpreter(tool.common.allowed_callers)
            }
            claude::CommandTool::CodeExecution20260120(tool) => {
                code_interpreter(tool.common.allowed_callers)
            }
            claude::CommandTool::CodeExecution20260521(tool) => {
                code_interpreter(tool.common.allowed_callers)
            }
            claude::CommandTool::Memory20250818(tool) => memory(tool.common),
            claude::CommandTool::ToolSearchRegex(tool) => {
                tool_search(openai::ToolSearchExecution::Client, tool.common)
            }
            claude::CommandTool::ToolSearchBm25(tool) => {
                tool_search(openai::ToolSearchExecution::Server, tool.common)
            }
            other => fallback(claude::Tool::Command(other))?,
        },
        claude::Tool::TextEditor(editor) => text_editor(editor)?,
        claude::Tool::Computer(tool) => computer(tool)?,
        claude::Tool::WebSearch(search) => web_search(search)?,
        claude::Tool::WebFetch(fetch) => web_fetch(fetch)?,
        claude::Tool::McpToolset(toolset) => openai::ResponseTool::Mcp {
            server_label: toolset.mcp_server_name,
            allowed_tools: None,
            authorization: None,
            connector_id: None,
            defer_loading: None,
            headers: None,
            require_approval: None,
            server_description: None,
            server_url: None,
            tunnel_id: None,
            allowed_callers: None,
            rest: Default::default(),
        },
        other => fallback(other)?,
    })
}

fn custom(tool: claude::CustomTool) -> Result<openai::ResponseTool, TransformError> {
    let parameters = serde_json::to_value(tool.input_schema)?
        .as_object()
        .cloned()
        .ok_or_else(|| TransformError::shape("Claude custom tool", "schema must be an object"))?;
    Ok(openai::ResponseTool::Function {
        name: tool.name,
        parameters: openai::ResponseFunctionParameters::Schema(parameters),
        strict: tool
            .common
            .strict
            .map(openai::ResponseFunctionStrict::Value)
            .unwrap_or(openai::ResponseFunctionStrict::Absent),
        defer_loading: tool.common.defer_loading,
        description: tool.description,
        output_schema: None,
        allowed_callers: callers_to_openai(tool.common.allowed_callers),
        async_: None,
        rest: Default::default(),
    })
}

fn normalize(tool: claude::Tool) -> Result<claude::Tool, TransformError> {
    match tool {
        claude::Tool::WebFetch(claude::WebFetchTool::Raw(raw))
        | claude::Tool::WebSearch(claude::WebSearchTool::Raw(raw))
        | claude::Tool::Computer(claude::ComputerTool::Raw(raw))
        | claude::Tool::TextEditor(claude::TextEditorTool::Raw(raw))
        | claude::Tool::Command(claude::CommandTool::Raw(raw))
        | claude::Tool::Unknown(raw) => {
            Err(TransformError::unsupported("Claude tool", raw.to_string()))
        }
        tool => Ok(tool),
    }
}

fn shell(common: claude::ToolCommon) -> openai::ResponseTool {
    openai::ResponseTool::Shell {
        environment: None,
        allowed_callers: callers_to_openai(common.allowed_callers),
        rest: Default::default(),
    }
}

fn text_editor(editor: claude::TextEditorTool) -> Result<openai::ResponseTool, TransformError> {
    let (common, max_characters) = match editor {
        claude::TextEditorTool::TextEditor20241022(tool) => (tool.common, None),
        claude::TextEditorTool::TextEditor20250124(tool) => (tool.common, None),
        claude::TextEditorTool::TextEditor20250429(tool) => (tool.common, None),
        claude::TextEditorTool::TextEditor20250728(tool) => (tool.common, tool.max_characters),
        other => return fallback(claude::Tool::TextEditor(other)),
    };
    Ok(openai::ResponseTool::ApplyPatch {
        allowed_callers: callers_to_openai(common.allowed_callers),
        max_characters,
        rest: Default::default(),
    })
}

fn code_interpreter(callers: Option<Vec<claude::ToolCaller>>) -> openai::ResponseTool {
    openai::ResponseTool::CodeInterpreter {
        container: openai::CodeInterpreterContainer::Auto(crate::wire!(
            openai::CodeInterpreterAutoContainer {
                type_: openai::CodeInterpreterContainerType::Auto,
                file_ids: None,
                memory_limit: None,
                network_policy: None,
                rest: Default::default(),
            }
        )),
        allowed_callers: callers_to_openai(callers),
        rest: Default::default(),
    }
}

fn memory(common: claude::ToolCommon) -> openai::ResponseTool {
    openai::ResponseTool::Function {
        name: "memory".into(),
        parameters: openai::ResponseFunctionParameters::Null,
        strict: common
            .strict
            .map(openai::ResponseFunctionStrict::Value)
            .unwrap_or(openai::ResponseFunctionStrict::Absent),
        defer_loading: common.defer_loading,
        description: Some("Read or update persistent agent memory".into()),
        output_schema: None,
        allowed_callers: callers_to_openai(common.allowed_callers),
        async_: None,
        rest: Default::default(),
    }
}

fn tool_search(
    execution: openai::ToolSearchExecution,
    _common: claude::ToolCommonWithoutInputExamples,
) -> openai::ResponseTool {
    openai::ResponseTool::ToolSearch {
        description: None,
        execution: Some(execution),
        parameters: None,
        rest: Default::default(),
    }
}

fn computer(computer: claude::ComputerTool) -> Result<openai::ResponseTool, TransformError> {
    match computer {
        claude::ComputerTool::Computer20241022(_)
        | claude::ComputerTool::Computer20250124(_)
        | claude::ComputerTool::Computer20251124(_) => {}
        other => return fallback(claude::Tool::Computer(other)),
    }
    Ok(openai::ResponseTool::Computer {
        rest: Default::default(),
    })
}

fn web_search(search: claude::WebSearchTool) -> Result<openai::ResponseTool, TransformError> {
    let params = match search {
        claude::WebSearchTool::WebSearch20250305(tool) => tool.params,
        claude::WebSearchTool::WebSearch20260209(tool) => tool.params,
        claude::WebSearchTool::WebSearch20260318(tool) => tool.params,
        other => return fallback(claude::Tool::WebSearch(other)),
    };
    let filters = if params.allowed_domains.is_some() || params.blocked_domains.is_some() {
        Some(crate::wire!(openai::WebSearchFilters {
            allowed_domains: params.allowed_domains,
            blocked_domains: params.blocked_domains,
            rest: Default::default(),
        }))
    } else {
        None
    };
    Ok(openai::ResponseTool::WebSearch {
        filters,
        max_uses: params.max_uses,
        search_context_size: None,
        user_location: params.user_location.map(location),
        rest: Default::default(),
    })
}

fn web_fetch(fetch: claude::WebFetchTool) -> Result<openai::ResponseTool, TransformError> {
    let params = match fetch {
        claude::WebFetchTool::WebFetch20250910(tool) => tool.params,
        claude::WebFetchTool::WebFetch20260209(tool) => tool.params,
        claude::WebFetchTool::WebFetch20260309(tool) => tool.params,
        claude::WebFetchTool::WebFetch20260318(tool) => tool.params,
        other => return fallback(claude::Tool::WebFetch(other)),
    };
    Ok(openai::ResponseTool::WebFetch {
        allowed_domains: params.allowed_domains,
        blocked_domains: params.blocked_domains,
        max_content_tokens: params.max_content_tokens,
        max_uses: params.max_uses,
        rest: Default::default(),
    })
}

fn location(location: claude::UserLocation) -> openai::WebSearchUserLocation {
    crate::wire!(openai::WebSearchUserLocation {
        city: location.city,
        country: location.country,
        region: location.region,
        timezone: location.timezone,
        type_: Some(openai::ApproximateLocationType::Approximate),
        rest: Default::default(),
    })
}

fn fallback(tool: claude::Tool) -> Result<openai::ResponseTool, TransformError> {
    Err(TransformError::unsupported(
        "Claude native tool",
        serde_json::to_string(&tool)?,
    ))
}