use serde_json::Value;
use crate::app_server::AppServerError;
#[derive(Debug)]
pub(super) struct PromptCompletion {
pub(super) assistant_message: Option<String>,
pub(super) input_tokens: u64,
pub(super) output_tokens: u64,
}
pub(super) fn parse_prompt_completion_response(
response_value: &Value,
) -> Result<PromptCompletion, AppServerError> {
let result = response_value.get("result").ok_or_else(|| {
AppServerError::Provider(
"Gemini ACP `session/prompt` response missing `result`".to_string(),
)
})?;
let (input_tokens, output_tokens) = extract_prompt_usage_tokens(result);
let assistant_message = extract_prompt_result_text(result);
Ok(PromptCompletion {
assistant_message,
input_tokens,
output_tokens,
})
}
pub(super) fn extract_prompt_usage_tokens(result: &Value) -> (u64, u64) {
extract_token_count_object(result.get("usage"))
.or_else(|| extract_meta_quota_token_count(result))
.or_else(|| extract_meta_model_usage_totals(result))
.unwrap_or((0, 0))
}
fn extract_meta_quota_token_count(result: &Value) -> Option<(u64, u64)> {
let quota = result.get("_meta")?.get("quota")?;
extract_token_count_object(quota.get("token_count").or_else(|| quota.get("tokenCount")))
}
fn extract_meta_model_usage_totals(result: &Value) -> Option<(u64, u64)> {
let quota = result.get("_meta")?.get("quota")?;
let model_usage = quota
.get("model_usage")
.or_else(|| quota.get("modelUsage"))?
.as_array()?;
let mut input_tokens = 0;
let mut output_tokens = 0;
let mut found_usage = false;
for model_usage_entry in model_usage {
if let Some((model_input_tokens, model_output_tokens)) = extract_token_count_object(
model_usage_entry
.get("token_count")
.or_else(|| model_usage_entry.get("tokenCount")),
) {
input_tokens += model_input_tokens;
output_tokens += model_output_tokens;
found_usage = true;
}
}
if !found_usage {
return None;
}
Some((input_tokens, output_tokens))
}
fn extract_token_count_object(value: Option<&Value>) -> Option<(u64, u64)> {
let usage = value?;
let input_tokens = usage
.get("inputTokens")
.or_else(|| usage.get("input_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("outputTokens")
.or_else(|| usage.get("output_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
Some((input_tokens, output_tokens))
}
pub(super) fn extract_prompt_result_text(result: &Value) -> Option<String> {
extract_string_field(result, "response")
.or_else(|| extract_string_field(result, "text"))
.or_else(|| extract_nonempty_content_field(result, "content"))
.or_else(|| result.get("message").and_then(extract_message_text))
.or_else(|| extract_output_text(result.get("output")?))
}
fn extract_string_field(value: &Value, field: &str) -> Option<String> {
value.get(field).and_then(Value::as_str).map(str::to_string)
}
fn extract_nonempty_content_field(value: &Value, field: &str) -> Option<String> {
value
.get(field)
.and_then(extract_text_from_content_value)
.filter(|text| !text.is_empty())
}
fn extract_message_text(message: &Value) -> Option<String> {
extract_string_field(message, "text")
.or_else(|| extract_nonempty_content_field(message, "content"))
}
fn extract_output_text(output: &Value) -> Option<String> {
let output_items = output.as_array()?;
let mut output_text = String::new();
for output_item in output_items {
if let Some(item_text) = extract_string_field(output_item, "text") {
output_text.push_str(&item_text);
continue;
}
if let Some(content) = output_item.get("content")
&& let Some(content_text) = extract_text_from_content_value(content)
{
output_text.push_str(&content_text);
}
}
if output_text.is_empty() {
return None;
}
Some(output_text)
}
pub(super) fn extract_text_from_content_value(content: &Value) -> Option<String> {
match content {
Value::String(text) => Some(text.clone()),
Value::Array(parts) => extract_text_from_parts(parts),
Value::Object(_) => extract_text_from_content_object(content),
_ => None,
}
}
fn extract_text_from_parts(parts: &[Value]) -> Option<String> {
let combined_text = parts
.iter()
.filter_map(extract_text_from_content_value)
.collect::<String>();
(!combined_text.is_empty()).then_some(combined_text)
}
fn extract_text_from_content_object(content: &Value) -> Option<String> {
extract_string_field(content, "text")
.or_else(|| extract_nonempty_content_field(content, "parts"))
.or_else(|| extract_nonempty_content_field(content, "content"))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::extract_prompt_result_text;
#[test]
fn prompt_result_text_flattens_nested_content_parts() {
let result = json!({
"content": {
"parts": [null, "First", {"content": {"text": " second"}}]
}
});
let text = extract_prompt_result_text(&result);
assert_eq!(text.as_deref(), Some("First second"));
}
#[test]
fn prompt_result_text_reads_message_content() {
let result = json!({"message": {"content": ["Message text"]}});
let text = extract_prompt_result_text(&result);
assert_eq!(text.as_deref(), Some("Message text"));
}
#[test]
fn prompt_result_text_combines_output_items() {
let result = json!({
"output": [
{"text": "First"},
{"content": {"text": " second"}}
]
});
let text = extract_prompt_result_text(&result);
assert_eq!(text.as_deref(), Some("First second"));
}
}