va-ai-api-bridge 0.1.7

Protocol translation primitives for AI API request and response shapes.
Documentation
use serde_json::Value;

use crate::schema::openai::{ResponsesItem, ResponsesResponse};
use crate::translator::{common, openai};
use crate::{ApiBridgeError, ContentBlock, DecodeState, Result, Role, UniversalEvent};

pub(super) fn decode(raw: Value) -> Result<Vec<UniversalEvent>> {
    let response: ResponsesResponse = serde_json::from_value(raw)
        .map_err(|error| ApiBridgeError::invalid_response(error.to_string()))?;
    let usage = openai::openai_usage_to_universal(response.usage.as_ref());
    let mut events = vec![UniversalEvent::ResponseStart {
        id: response.id.clone(),
        model: response.model.clone(),
        extensions: common::empty_extensions(),
    }];

    if let Some(error) = response.error {
        events.push(UniversalEvent::Error {
            message: error
                .get("message")
                .and_then(Value::as_str)
                .unwrap_or("OpenAI response error")
                .to_string(),
            raw: Some(error),
        });
    }

    for (index, item) in response.output.into_iter().enumerate() {
        push_response_item_events(&mut events, response.id.as_deref(), index, item);
    }

    events.push(UniversalEvent::ResponseDone {
        usage,
        extensions: common::empty_extensions(),
    });
    Ok(events)
}

pub(super) fn push_response_item_start(
    events: &mut Vec<UniversalEvent>,
    state: &mut DecodeState,
    index: usize,
    item: ResponsesItem,
) {
    match item.kind.as_deref() {
        Some("message") | None if item.role.is_some() => common::ensure_message_start(
            events,
            state,
            item.id
                .unwrap_or_else(|| format!("response_message_{index}")),
            item.role
                .as_deref()
                .and_then(common::role_from_wire)
                .unwrap_or(Role::Assistant),
        ),
        Some("function_call") => events.push(UniversalEvent::ToolCallDelta {
            id: item.call_id.or(item.id).unwrap_or_default(),
            name: item.name,
            arguments_delta: item
                .arguments
                .as_ref()
                .map(common::stringify_arguments)
                .unwrap_or_default(),
        }),
        _ => {}
    }
}

pub(super) fn push_response_item_events(
    events: &mut Vec<UniversalEvent>,
    response_id: Option<&str>,
    index: usize,
    item: ResponsesItem,
) {
    match item.kind.as_deref() {
        Some("message") | None if item.role.is_some() => {
            events.push(UniversalEvent::MessageStart {
                id: item
                    .id
                    .unwrap_or_else(|| format!("{}:{index}", response_id.unwrap_or("response"))),
                role: item
                    .role
                    .as_deref()
                    .and_then(common::role_from_wire)
                    .unwrap_or(Role::Assistant),
                extensions: common::empty_extensions(),
            });
            for (content_index, block) in openai::openai_content_to_blocks(item.content.as_ref())
                .into_iter()
                .enumerate()
            {
                common::push_block_events(events, content_index, block);
            }
            events.push(UniversalEvent::MessageDone {
                finish_reason: None,
                usage: None,
                extensions: common::empty_extensions(),
            });
        }
        Some("function_call") => {
            events.push(UniversalEvent::ToolCallDelta {
                id: item.call_id.or(item.id).unwrap_or_default(),
                name: item.name,
                arguments_delta: item
                    .arguments
                    .as_ref()
                    .map(common::stringify_arguments)
                    .unwrap_or_default(),
            });
        }
        Some("reasoning") => {
            if let Some(text) = text_from_openai_content(item.content.as_ref()) {
                events.push(UniversalEvent::ReasoningDelta { index, text });
            }
        }
        _ => events.push(UniversalEvent::Unknown {
            raw: serde_json::to_value(item).unwrap_or(Value::Null),
            tags: Default::default(),
        }),
    }
}

fn text_from_openai_content(
    content: Option<&crate::schema::openai::OpenAiContent>,
) -> Option<String> {
    let text = openai::openai_content_to_blocks(content)
        .into_iter()
        .filter_map(|block| match block {
            ContentBlock::Text { text } => Some(text),
            ContentBlock::Reasoning { text, .. } => text,
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("");
    (!text.is_empty()).then_some(text)
}