use chimera_core::*;
use crate::wire::{ChatCompletionChunk, ChatCompletionResponse, WireUsage};
pub(crate) fn translate_response(response: ChatCompletionResponse) -> Vec<AgentEvent> {
let mut events = vec![AgentEvent::TurnStarted];
let finish_reason = response
.choices
.first()
.and_then(|choice| choice.finish_reason.clone());
let text = response
.choices
.first()
.and_then(|c| c.message.content.clone());
if let Some(choice) = response.choices.first() {
for tc in &choice.message.tool_calls {
events.push(AgentEvent::ToolCall {
id: tc.id.clone(),
name: tc.function.name.clone(),
arguments: tc.function.arguments.clone(),
});
}
}
if let Some(ref t) = text {
events.push(AgentEvent::Message {
id: response.id.clone(),
text: t.clone(),
});
}
events.push(AgentEvent::TurnCompleted {
usage: response.usage.map(translate_usage),
result: Some(TurnResult {
text,
duration_ms: None,
session_id: Some(response.id),
num_turns: Some(1),
turn_status: Some(TurnStatus::Completed),
provider_meta: finish_reason.map(|finish_reason| TurnProviderMeta::OpenCode {
finish_reason: Some(finish_reason),
}),
}),
});
events
}
pub(crate) fn translate_chunk(chunk: ChatCompletionChunk) -> Vec<AgentEvent> {
let mut events = Vec::new();
for choice in &chunk.choices {
if let Some(content) = &choice.delta.content
&& !content.is_empty()
{
events.push(AgentEvent::TextDelta {
id: format!("choice_{}", choice.index),
delta: content.clone(),
});
}
if choice.finish_reason.is_some() {
events.push(AgentEvent::TurnCompleted {
usage: chunk.usage.as_ref().map(translate_usage_ref),
result: Some(TurnResult {
text: None, duration_ms: None,
session_id: Some(chunk.id.clone()),
num_turns: Some(1),
turn_status: Some(TurnStatus::Completed),
provider_meta: Some(TurnProviderMeta::OpenCode {
finish_reason: choice.finish_reason.clone(),
}),
}),
});
}
}
events
}
fn translate_usage(u: WireUsage) -> Usage {
Usage {
input_tokens: u.prompt_tokens,
output_tokens: u.completion_tokens,
cache_read_tokens: None,
cache_creation_tokens: None,
total_cost_usd: None,
}
}
fn translate_usage_ref(u: &WireUsage) -> Usage {
Usage {
input_tokens: u.prompt_tokens,
output_tokens: u.completion_tokens,
cache_read_tokens: None,
cache_creation_tokens: None,
total_cost_usd: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::*;
#[test]
fn translate_non_streaming_response() {
let resp = ChatCompletionResponse {
id: "cmpl-1".into(),
choices: vec![ChatChoice {
message: WireResponseMessage {
content: Some("Hello world".into()),
tool_calls: vec![],
},
finish_reason: Some("stop".into()),
}],
usage: Some(WireUsage {
prompt_tokens: Some(10),
completion_tokens: Some(5),
total_tokens: Some(15),
}),
};
let events = translate_response(resp);
assert_eq!(events.len(), 3);
assert!(matches!(events[0], AgentEvent::TurnStarted));
match &events[1] {
AgentEvent::Message { id, text } => {
assert_eq!(id, "cmpl-1");
assert_eq!(text, "Hello world");
}
_ => panic!("expected Message"),
}
match &events[2] {
AgentEvent::TurnCompleted { usage, result } => {
assert_eq!(usage.as_ref().unwrap().input_tokens, Some(10));
let result = result.as_ref().expect("turn result");
assert_eq!(result.session_id.as_deref(), Some("cmpl-1"));
assert_eq!(result.turn_status, Some(TurnStatus::Completed));
match &result.provider_meta {
Some(TurnProviderMeta::OpenCode { finish_reason }) => {
assert_eq!(finish_reason.as_deref(), Some("stop"));
}
other => panic!("expected OpenCode provider meta, got {other:?}"),
}
}
_ => panic!("expected TurnCompleted"),
}
}
#[test]
fn translate_chunk_with_content() {
let chunk = ChatCompletionChunk {
id: "cmpl-1".into(),
choices: vec![ChunkChoice {
index: 0,
delta: ChunkDelta {
content: Some("Hi".into()),
tool_calls: vec![],
},
finish_reason: None,
}],
usage: None,
};
let events = translate_chunk(chunk);
assert_eq!(events.len(), 1);
match &events[0] {
AgentEvent::TextDelta { id, delta } => {
assert_eq!(id, "choice_0");
assert_eq!(delta, "Hi");
}
_ => panic!("expected TextDelta"),
}
}
#[test]
fn translate_chunk_with_finish() {
let chunk = ChatCompletionChunk {
id: "cmpl-1".into(),
choices: vec![ChunkChoice {
index: 0,
delta: ChunkDelta {
content: None,
tool_calls: vec![],
},
finish_reason: Some("stop".into()),
}],
usage: Some(WireUsage {
prompt_tokens: Some(10),
completion_tokens: Some(5),
total_tokens: Some(15),
}),
};
let events = translate_chunk(chunk);
assert_eq!(events.len(), 1);
match &events[0] {
AgentEvent::TurnCompleted { result, .. } => {
let result = result.as_ref().expect("turn result");
assert_eq!(result.turn_status, Some(TurnStatus::Completed));
match &result.provider_meta {
Some(TurnProviderMeta::OpenCode { finish_reason }) => {
assert_eq!(finish_reason.as_deref(), Some("stop"));
}
other => panic!("expected OpenCode provider meta, got {other:?}"),
}
}
_ => panic!("expected TurnCompleted"),
}
}
#[test]
fn translate_chunk_empty_content_skipped() {
let chunk = ChatCompletionChunk {
id: "cmpl-1".into(),
choices: vec![ChunkChoice {
index: 0,
delta: ChunkDelta {
content: Some(String::new()),
tool_calls: vec![],
},
finish_reason: None,
}],
usage: None,
};
let events = translate_chunk(chunk);
assert!(events.is_empty());
}
}