va-ai-api-bridge 0.1.7

Protocol translation primitives for AI API request and response shapes.
Documentation
mod tool;

pub(super) use tool::*;

use serde_json::{json, Value};

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

pub(super) fn append_response_text(state: &mut EncodeState, text: &str) {
    let mut current = state
        .extensions
        .get("responseText")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string();
    current.push_str(text);
    state
        .extensions
        .insert("responseText".to_string(), Value::String(current));
}

pub(super) fn append_response_reasoning_text(state: &mut EncodeState, text: &str) {
    let mut current = state
        .extensions
        .get("responseReasoningText")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string();
    current.push_str(text);
    state
        .extensions
        .insert("responseReasoningText".to_string(), Value::String(current));
}

pub(super) fn ensure_response_reasoning_started(
    state: &mut EncodeState,
    wire_events: &mut Vec<WireEvent>,
    content_index: usize,
) {
    state.extensions.insert(
        "responseReasoningContentIndex".to_string(),
        Value::Number((content_index as u64).into()),
    );
    let previous = state
        .extensions
        .insert("responseReasoningStarted".to_string(), Value::Bool(true));
    if matches!(previous, Some(Value::Bool(true))) {
        return;
    }
    let output_index = assign_response_reasoning_output_index(state);
    wire_events.push(common::wire_event(json!({
        "type": "response.output_item.added",
        "output_index": output_index,
        "item": {
            "id": response_reasoning_id(state),
            "type": "reasoning",
            "status": "in_progress",
            "summary": []
        }
    })));
}

pub(super) fn finish_response_reasoning(state: &mut EncodeState, wire_events: &mut Vec<WireEvent>) {
    if !matches!(
        state.extensions.get("responseReasoningStarted"),
        Some(Value::Bool(true))
    ) {
        return;
    }
    let previous = state
        .extensions
        .insert("responseReasoningDone".to_string(), Value::Bool(true));
    if matches!(previous, Some(Value::Bool(true))) {
        return;
    }
    wire_events.push(common::wire_event(json!({
        "type": "response.output_item.done",
        "output_index": response_reasoning_output_index(state),
        "item": response_reasoning_output_item(state)
    })));
}

pub(super) fn ensure_response_output_started(
    state: &mut EncodeState,
    wire_events: &mut Vec<WireEvent>,
    content_index: usize,
) {
    state.extensions.insert(
        "responseContentIndex".to_string(),
        Value::Number((content_index as u64).into()),
    );
    let previous = state
        .extensions
        .insert("responseOutputStarted".to_string(), Value::Bool(true));
    if matches!(previous, Some(Value::Bool(true))) {
        return;
    }
    let output_index = assign_response_message_output_index(state);
    let item_id = response_message_id(state);
    wire_events.push(common::wire_event(json!({
        "type": "response.output_item.added",
        "output_index": output_index,
        "item": {
            "id": item_id,
            "type": "message",
            "status": "in_progress",
            "role": "assistant",
            "content": []
        }
    })));
    wire_events.push(common::wire_event(json!({
        "type": "response.content_part.added",
        "output_index": output_index,
        "content_index": content_index,
        "item_id": item_id,
        "part": {
            "type": "output_text",
            "text": "",
            "annotations": []
        }
    })));
}

pub(super) fn finish_response_output(state: &mut EncodeState, wire_events: &mut Vec<WireEvent>) {
    if !matches!(
        state.extensions.get("responseOutputStarted"),
        Some(Value::Bool(true))
    ) {
        return;
    }
    let previous = state
        .extensions
        .insert("responseOutputDone".to_string(), Value::Bool(true));
    if matches!(previous, Some(Value::Bool(true))) {
        return;
    }
    let item_id = response_message_id(state);
    let content_index = state
        .extensions
        .get("responseContentIndex")
        .and_then(Value::as_u64)
        .unwrap_or(0) as usize;
    let output_index = response_message_output_index(state);
    let text = response_text(state);
    let part = json!({
        "type": "output_text",
        "text": text,
        "annotations": []
    });
    wire_events.push(common::wire_event(json!({
        "type": "response.output_text.done",
        "output_index": output_index,
        "content_index": content_index,
        "item_id": item_id,
        "text": text
    })));
    wire_events.push(common::wire_event(json!({
        "type": "response.content_part.done",
        "output_index": output_index,
        "content_index": content_index,
        "item_id": item_id,
        "part": part
    })));
    wire_events.push(common::wire_event(json!({
        "type": "response.output_item.done",
        "output_index": output_index,
        "item": response_output_item(state)
    })));
}

pub(super) fn assign_response_message_output_index(state: &mut EncodeState) -> usize {
    if let Some(index) = state
        .extensions
        .get("responseOutputIndex")
        .and_then(Value::as_u64)
    {
        return index as usize;
    }
    let index = common::encode_state_index(state);
    state.extensions.insert(
        "responseOutputIndex".to_string(),
        Value::Number((index as u64).into()),
    );
    index
}

pub(super) fn response_message_output_index(state: &EncodeState) -> usize {
    state
        .extensions
        .get("responseOutputIndex")
        .and_then(Value::as_u64)
        .unwrap_or(0) as usize
}

pub(super) fn assign_response_reasoning_output_index(state: &mut EncodeState) -> usize {
    if let Some(index) = state
        .extensions
        .get("responseReasoningOutputIndex")
        .and_then(Value::as_u64)
    {
        return index as usize;
    }
    let index = common::encode_state_index(state);
    state.extensions.insert(
        "responseReasoningOutputIndex".to_string(),
        Value::Number((index as u64).into()),
    );
    index
}

pub(super) fn response_reasoning_output_index(state: &EncodeState) -> usize {
    state
        .extensions
        .get("responseReasoningOutputIndex")
        .and_then(Value::as_u64)
        .unwrap_or(0) as usize
}
pub(super) fn response_message_id(state: &EncodeState) -> String {
    let id = state
        .extensions
        .get("responseId")
        .and_then(Value::as_str)
        .unwrap_or("resp_va_bridge");
    if id.starts_with("msg_") {
        id.to_string()
    } else {
        format!("msg_{id}")
    }
}

pub(super) fn response_text(state: &EncodeState) -> String {
    state
        .extensions
        .get("responseText")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string()
}

pub(super) fn response_reasoning_text(state: &EncodeState) -> String {
    state
        .extensions
        .get("responseReasoningText")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string()
}

pub(super) fn response_reasoning_id(state: &EncodeState) -> String {
    let id = state
        .extensions
        .get("responseId")
        .and_then(Value::as_str)
        .unwrap_or("resp_va_bridge");
    if id.starts_with("rs_") {
        id.to_string()
    } else {
        format!("rs_{id}")
    }
}

pub(super) fn response_output_item(state: &EncodeState) -> Value {
    json!({
        "id": response_message_id(state),
        "type": "message",
        "status": "completed",
        "role": "assistant",
        "content": [{
            "type": "output_text",
            "text": response_text(state),
            "annotations": []
        }]
    })
}

pub(super) fn response_reasoning_output_item(state: &EncodeState) -> Value {
    json!({
        "id": response_reasoning_id(state),
        "type": "reasoning",
        "status": "completed",
        "summary": [],
        "content": [{
            "type": "reasoning_text",
            "text": response_reasoning_text(state)
        }]
    })
}

pub(super) fn response_shell(
    state: &EncodeState,
    status: &str,
    usage: Option<&crate::Usage>,
) -> Value {
    let id = state
        .extensions
        .get("responseId")
        .and_then(Value::as_str)
        .unwrap_or("resp_va_bridge");
    let model = state
        .extensions
        .get("responseModel")
        .and_then(Value::as_str)
        .unwrap_or_default();
    let output = if status == "completed" {
        Value::Array(response_output_items(state))
    } else {
        json!([])
    };
    json!({
        "id": id,
        "object": "response",
        "created_at": 0,
        "status": status,
        "error": Value::Null,
        "incomplete_details": Value::Null,
        "instructions": Value::Null,
        "max_output_tokens": Value::Null,
        "model": model,
        "output": output,
        "parallel_tool_calls": true,
        "previous_response_id": Value::Null,
        "reasoning": Value::Null,
        "store": false,
        "temperature": Value::Null,
        "text": Value::Null,
        "tool_choice": "auto",
        "tools": [],
        "top_p": Value::Null,
        "truncation": "disabled",
        "usage": usage_to_openai_value(usage),
        "user": Value::Null,
        "metadata": {}
    })
}

pub(super) fn response_output_items(state: &EncodeState) -> Vec<Value> {
    let mut output = Vec::new();
    if matches!(
        state.extensions.get("responseReasoningDone"),
        Some(Value::Bool(true))
    ) {
        output.push((
            response_reasoning_output_index(state),
            response_reasoning_output_item(state),
        ));
    }
    if matches!(
        state.extensions.get("responseOutputDone"),
        Some(Value::Bool(true))
    ) {
        output.push((
            response_message_output_index(state),
            response_output_item(state),
        ));
    }
    for id in response_tool_ids(state) {
        if matches!(
            state.extensions.get(&response_tool_done_key(&id)),
            Some(Value::Bool(true))
        ) {
            output.push((
                response_tool_output_index(state, &id),
                response_tool_output_item(state, &id),
            ));
        }
    }
    output.sort_by_key(|(output_index, _)| *output_index);
    output.into_iter().map(|(_, item)| item).collect()
}

pub(super) fn usage_to_openai_value(usage: Option<&crate::Usage>) -> Value {
    match usage {
        Some(usage) => json!({
            "input_tokens": usage.input_tokens,
            "output_tokens": usage.output_tokens,
            "total_tokens": usage.total_tokens
        }),
        None => Value::Null,
    }
}