chimera-opencode 0.2.1

OpenCode/OpenAI-compatible REST backend for the chimera AI agent SDK
Documentation
use chimera_core::*;

use crate::wire::{ChatCompletionChunk, ChatCompletionResponse, WireUsage};

/// Translate a non-streaming response into AgentEvents.
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());

    // Emit tool calls before the text message (they are the "result" of the turn).
    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
}

/// Translate a streaming SSE chunk into AgentEvents.
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, // assembled from deltas by collect_turn
                    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());
    }
}