gproxy-transform 2.1.6

Pairwise request/response/stream transforms between the OpenAI, Anthropic Claude, and Google Gemini APIs
Documentation
use serde_json::{Value, json};

#[derive(Default)]
pub(super) struct ResponsesTextItemState {
    pub(super) started: bool,
    done: bool,
    id: Option<String>,
    output_index: Option<u32>,
    content_index: Option<u32>,
    pub(super) text: String,
}

impl ResponsesTextItemState {
    pub(super) fn ensure(
        &mut self,
        event: &Value,
        fallback_id: &'static str,
        build: impl FnOnce(&Self) -> Value,
    ) -> Vec<Value> {
        self.note_delta_identity(event, fallback_id);
        if self.started {
            return Vec::new();
        }
        self.started = true;
        vec![build(self)]
    }

    pub(super) fn push_delta(&mut self, event: &Value) {
        self.note_delta_identity(event, "item_0");
        if let Some(delta) = event.get("delta").and_then(Value::as_str) {
            self.text.push_str(delta);
        }
    }

    pub(super) fn finish(&mut self, build: impl FnOnce(&Self) -> Vec<Value>) -> Vec<Value> {
        if !self.started || self.done {
            return Vec::new();
        }
        self.done = true;
        build(self)
    }

    fn note_delta_identity(&mut self, event: &Value, fallback_id: &'static str) {
        if self.id.is_none() {
            self.id = event
                .get("item_id")
                .and_then(Value::as_str)
                .map(str::to_owned)
                .or_else(|| Some(fallback_id.into()));
        }
        if self.output_index.is_none() {
            self.output_index = event
                .get("output_index")
                .and_then(Value::as_u64)
                .and_then(|n| n.try_into().ok())
                .or(Some(0));
        }
        if self.content_index.is_none() {
            self.content_index = event
                .get("content_index")
                .and_then(Value::as_u64)
                .and_then(|n| n.try_into().ok())
                .or(Some(0));
        }
    }

    pub(super) fn note_added(&mut self, event: &Value) {
        self.started = true;
        self.note_item_identity(event);
    }
    pub(super) fn note_item_done(&mut self, event: &Value) {
        self.done = true;
        self.note_item_identity(event);
    }
    pub(super) fn note_done_text(&mut self, event: &Value) {
        self.done = true;
        if let Some(text) = event.get("text").and_then(Value::as_str) {
            self.text = text.into();
        }
    }
    fn note_item_identity(&mut self, event: &Value) {
        if self.id.is_none() {
            self.id = event
                .get("item")
                .and_then(|item| item.get("id"))
                .and_then(Value::as_str)
                .map(str::to_owned);
        }
        if self.output_index.is_none() {
            self.output_index = event
                .get("output_index")
                .and_then(Value::as_u64)
                .and_then(|n| n.try_into().ok());
        }
    }
    pub(super) fn id(&self) -> &str {
        self.id.as_deref().unwrap_or("item_0")
    }
    pub(super) fn output_index(&self) -> u32 {
        self.output_index.unwrap_or(0)
    }
    pub(super) fn content_index(&self) -> u32 {
        self.content_index.unwrap_or(0)
    }
}

pub(super) fn message_item_added(state: &ResponsesTextItemState) -> Value {
    json!({"type":"response.output_item.added","output_index":state.output_index(),"item":message_item(state,"in_progress")})
}
pub(super) fn reasoning_item_added(state: &ResponsesTextItemState) -> Value {
    json!({"type":"response.output_item.added","output_index":state.output_index(),"item":reasoning_item(state,"in_progress")})
}
pub(super) fn message_item(state: &ResponsesTextItemState, status: &str) -> Value {
    json!({"id":state.id(),"type":"message","status":status,"role":"assistant",
        "content":[{"type":"output_text","text":state.text,"annotations":[]}]})
}
pub(super) fn reasoning_item(state: &ResponsesTextItemState, status: &str) -> Value {
    json!({"id":state.id(),"type":"reasoning","status":status,"summary":[],
        "content":[{"type":"reasoning_text","text":state.text}]})
}