gproxy-transform 3.0.0-alpha.0

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

use crate::TransformError;
use crate::common::native::items;

use super::super::claude_to_openai::{Output, OutputEvent, State};
use super::super::claude_to_responses::{function_item, reasoning_item};
use super::Block;

impl State {
    pub(in crate::common::stream) fn block_start(
        &mut self,
        index: u64,
        block: claude::ContentBlock,
    ) -> Result<Vec<OutputEvent>, TransformError> {
        if !self.started || self.blocks.contains_key(&index) {
            return Err(TransformError::shape(
                "Claude stream",
                "invalid block start",
            ));
        }
        let (state, output) = match block {
            claude::ResponseContentBlock::Text(block) => {
                let id = format!(
                    "msg_{}_{}",
                    self.id.as_deref().expect("started message has an id"),
                    index
                );
                let text = block.text;
                let output = match self.output {
                    Output::Chat => (!text.is_empty())
                        .then(|| self.chat_text(text.clone()))
                        .transpose()?
                        .into_iter()
                        .collect(),
                    Output::Responses => vec![
                        self.response_output_item_added(
                            openai::ResponseItem::Message(openai::ResponseMessageItem::Output(
                                crate::wire!(openai::ResponseOutputMessageItem {
                                    type_: openai::ResponseMessageItemType::Message,
                                    id: id.clone(),
                                    role: openai::ResponseOutputMessageRole::Assistant,
                                    content: Vec::new(),
                                    status: openai::ResponseItemLifecycleStatus::InProgress,
                                    phase: None,
                                    rest: Default::default(),
                                }),
                            )),
                            index as u32,
                        )?,
                        self.response_content_part_added(
                            id.clone(),
                            index as u32,
                            openai::ResponseContentPart::OutputText(crate::wire!(
                                openai::ResponseOutputText {
                                    type_: openai::ResponseOutputTextType::OutputText,
                                    annotations: Vec::new(),
                                    logprobs: None,
                                    text: String::new(),
                                    rest: Default::default(),
                                }
                            )),
                        )?,
                    ],
                };
                (Block::Text { id, text }, output)
            }
            claude::ResponseContentBlock::Thinking(block) => {
                let id = format!(
                    "rs_{}_{}",
                    self.id.as_deref().expect("started message has an id"),
                    index
                );
                let text = block.thinking;
                let output = match self.output {
                    Output::Chat => (!text.is_empty())
                        .then(|| self.chat_reasoning(text.clone()))
                        .transpose()?
                        .into_iter()
                        .collect(),
                    Output::Responses => vec![self.response_output_item_added(
                        reasoning_item(
                            id.clone(),
                            text.clone(),
                            block.signature.clone(),
                            openai::ResponseItemLifecycleStatus::InProgress,
                        ),
                        index as u32,
                    )?],
                };
                (
                    Block::Thinking {
                        id,
                        text,
                        signature: block.signature,
                    },
                    output,
                )
            }
            claude::ResponseContentBlock::ToolUse(block) => {
                let arguments = if block.input.is_empty() {
                    String::new()
                } else {
                    serde_json::to_string(&block.input)?
                };
                let output = match self.output {
                    Output::Chat => vec![self.chat_tool_start(
                        index as u32,
                        block.id.clone(),
                        block.name.clone(),
                        arguments.clone(),
                    )?],
                    Output::Responses if items::is_buffered_native(&block.name) => Vec::new(),
                    Output::Responses => vec![self.response_output_item_added(
                        function_item(
                            block.id.clone(),
                            block.name.clone(),
                            arguments.clone(),
                            openai::ResponseItemLifecycleStatus::InProgress,
                        ),
                        index as u32,
                    )?],
                };
                (
                    Block::Tool {
                        id: block.id,
                        name: block.name,
                        arguments,
                    },
                    output,
                )
            }
            claude::ResponseContentBlock::RedactedThinking(_)
            | claude::ResponseContentBlock::ServerToolUse(_)
            | claude::ResponseContentBlock::WebSearchToolResult(_)
            | claude::ResponseContentBlock::WebFetchToolResult(_)
            | claude::ResponseContentBlock::AdvisorToolResult(_)
            | claude::ResponseContentBlock::CodeExecutionToolResult(_)
            | claude::ResponseContentBlock::BashCodeExecutionToolResult(_)
            | claude::ResponseContentBlock::TextEditorCodeExecutionToolResult(_)
            | claude::ResponseContentBlock::ToolSearchToolResult(_)
            | claude::ResponseContentBlock::McpToolUse(_)
            | claude::ResponseContentBlock::McpToolResult(_)
            | claude::ResponseContentBlock::ContainerUpload(_)
            | claude::ResponseContentBlock::Compaction(_)
            | claude::ResponseContentBlock::Fallback(_)
            | claude::ResponseContentBlock::Raw(_) => (Block::Ignored, Vec::new()),
            future => {
                return Err(TransformError::unsupported(
                    "Claude stream block",
                    serde_json::to_string(&future)?,
                ));
            }
        };
        self.blocks.insert(index, state);
        Ok(output)
    }
}