va-ai-api-bridge 0.1.7

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

use crate::translator::common;
use crate::{EncodeState, WireEvent};

pub(in crate::translator::openai_responses::stream::encode) fn ensure_response_tool_started(
    state: &mut EncodeState,
    wire_events: &mut Vec<WireEvent>,
    id: &str,
    name: Option<&str>,
    content_index: Option<usize>,
) {
    remember_response_tool_id(state, id);
    if let Some(name) = name.filter(|name| !name.is_empty()) {
        state
            .extensions
            .insert(response_tool_name_key(id), Value::String(name.to_string()));
    }
    if let Some(content_index) = content_index {
        state.extensions.insert(
            format!("responseToolIdForContentIndex:{content_index}"),
            Value::String(id.to_string()),
        );
    }
    let previous = state
        .extensions
        .insert(response_tool_started_key(id), Value::Bool(true));
    if matches!(previous, Some(Value::Bool(true))) {
        return;
    }
    let output_index = assign_response_tool_output_index(state, id);
    wire_events.push(common::wire_event(json!({
        "type": "response.output_item.added",
        "output_index": output_index,
        "item": {
            "id": response_tool_item_id(state, id),
            "type": "function_call",
            "status": "in_progress",
            "call_id": id,
            "name": response_tool_name(state, id),
            "arguments": ""
        }
    })));
}

pub(in crate::translator::openai_responses::stream::encode) fn append_response_tool_arguments(
    state: &mut EncodeState,
    id: &str,
    arguments_delta: &str,
) {
    if arguments_delta.is_empty() {
        return;
    }
    let mut current = state
        .extensions
        .get(&response_tool_arguments_key(id))
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string();
    current.push_str(arguments_delta);
    state
        .extensions
        .insert(response_tool_arguments_key(id), Value::String(current));
}

pub(in crate::translator::openai_responses::stream::encode) fn finish_all_response_tools(
    state: &mut EncodeState,
    wire_events: &mut Vec<WireEvent>,
) {
    for id in response_tool_ids(state) {
        finish_response_tool(state, wire_events, &id);
    }
}

pub(in crate::translator::openai_responses::stream::encode) fn finish_response_tool(
    state: &mut EncodeState,
    wire_events: &mut Vec<WireEvent>,
    id: &str,
) {
    if !matches!(
        state.extensions.get(&response_tool_started_key(id)),
        Some(Value::Bool(true))
    ) {
        return;
    }
    let previous = state
        .extensions
        .insert(response_tool_done_key(id), Value::Bool(true));
    if matches!(previous, Some(Value::Bool(true))) {
        return;
    }
    let output_index = response_tool_output_index(state, id);
    let item_id = response_tool_item_id(state, id);
    let arguments = response_tool_arguments(state, id);
    wire_events.push(common::wire_event(json!({
        "type": "response.function_call_arguments.done",
        "output_index": output_index,
        "item_id": item_id,
        "arguments": arguments
    })));
    wire_events.push(common::wire_event(json!({
        "type": "response.output_item.done",
        "output_index": output_index,
        "item": response_tool_output_item(state, id)
    })));
}
pub(in crate::translator::openai_responses::stream::encode) fn response_tool_output_item(
    state: &EncodeState,
    id: &str,
) -> Value {
    json!({
        "id": response_tool_item_id(state, id),
        "type": "function_call",
        "status": "completed",
        "call_id": id,
        "name": response_tool_name(state, id),
        "arguments": response_tool_arguments(state, id)
    })
}
pub(in crate::translator::openai_responses::stream::encode) fn assign_response_tool_output_index(
    state: &mut EncodeState,
    id: &str,
) -> usize {
    let key = response_tool_output_index_key(id);
    if let Some(index) = state.extensions.get(&key).and_then(Value::as_u64) {
        return index as usize;
    }
    let index = common::encode_state_index(state);
    state
        .extensions
        .insert(key, Value::Number((index as u64).into()));
    index
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_output_index(
    state: &EncodeState,
    id: &str,
) -> usize {
    state
        .extensions
        .get(&response_tool_output_index_key(id))
        .and_then(Value::as_u64)
        .unwrap_or(0) as usize
}

pub(in crate::translator::openai_responses::stream::encode) fn remember_response_tool_id(
    state: &mut EncodeState,
    id: &str,
) {
    let mut ids = response_tool_ids(state);
    if ids.iter().any(|existing| existing == id) {
        return;
    }
    ids.push(id.to_string());
    state.extensions.insert(
        "responseToolIds".to_string(),
        Value::Array(ids.into_iter().map(Value::String).collect()),
    );
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_ids(
    state: &EncodeState,
) -> Vec<String> {
    state
        .extensions
        .get("responseToolIds")
        .and_then(Value::as_array)
        .map(|ids| {
            ids.iter()
                .filter_map(Value::as_str)
                .map(ToString::to_string)
                .collect()
        })
        .unwrap_or_default()
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_id_for_content_index(
    state: &EncodeState,
    index: usize,
) -> Option<String> {
    state
        .extensions
        .get(&format!("responseToolIdForContentIndex:{index}"))
        .and_then(Value::as_str)
        .map(ToString::to_string)
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_name(
    state: &EncodeState,
    id: &str,
) -> String {
    state
        .extensions
        .get(&response_tool_name_key(id))
        .and_then(Value::as_str)
        .filter(|name| !name.is_empty())
        .unwrap_or("function")
        .to_string()
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_arguments(
    state: &EncodeState,
    id: &str,
) -> String {
    state
        .extensions
        .get(&response_tool_arguments_key(id))
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string()
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_item_id(
    state: &EncodeState,
    id: &str,
) -> String {
    state
        .extensions
        .get(&response_tool_item_id_key(id))
        .and_then(Value::as_str)
        .map(ToString::to_string)
        .unwrap_or_else(|| {
            if id.starts_with("fc_") {
                id.to_string()
            } else {
                format!("fc_{id}")
            }
        })
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_output_index_key(
    id: &str,
) -> String {
    format!("responseToolOutputIndex:{id}")
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_started_key(
    id: &str,
) -> String {
    format!("responseToolStarted:{id}")
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_done_key(
    id: &str,
) -> String {
    format!("responseToolDone:{id}")
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_name_key(
    id: &str,
) -> String {
    format!("responseToolName:{id}")
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_arguments_key(
    id: &str,
) -> String {
    format!("responseToolArguments:{id}")
}

pub(in crate::translator::openai_responses::stream::encode) fn response_tool_item_id_key(
    id: &str,
) -> String {
    format!("responseToolItemId:{id}")
}