chimera-opencode 0.2.1

OpenCode/OpenAI-compatible REST backend for the chimera AI agent SDK
Documentation
use serde::{Deserialize, Serialize};

// -- Request types --

#[derive(Debug, Serialize)]
pub(crate) struct ChatCompletionRequest {
    pub model: String,
    pub messages: Vec<WireMessage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_tokens: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_format: Option<ResponseFormat>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tools: Vec<WireTool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_choice: Option<serde_json::Value>,
}

/// A tool definition in the OpenAI function-calling format.
#[derive(Debug, Clone, Serialize)]
pub(crate) struct WireTool {
    #[serde(rename = "type")]
    pub kind: String,
    pub function: WireToolFunction,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct WireToolFunction {
    pub name: String,
    pub description: String,
    pub parameters: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WireMessage {
    pub role: String,
    pub content: String,
}

impl WireMessage {
    pub fn system(content: impl Into<String>) -> Self {
        Self {
            role: "system".into(),
            content: content.into(),
        }
    }

    pub fn user(content: impl Into<String>) -> Self {
        Self {
            role: "user".into(),
            content: content.into(),
        }
    }

    pub fn assistant(content: impl Into<String>) -> Self {
        Self {
            role: "assistant".into(),
            content: content.into(),
        }
    }
}

#[derive(Debug, Serialize)]
pub(crate) struct ResponseFormat {
    #[serde(rename = "type")]
    pub format_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub json_schema: Option<serde_json::Value>,
}

// -- Non-streaming response --

#[derive(Debug, Deserialize)]
pub(crate) struct ChatCompletionResponse {
    pub id: String,
    pub choices: Vec<ChatChoice>,
    pub usage: Option<WireUsage>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct ChatChoice {
    pub message: WireResponseMessage,
    #[allow(dead_code)]
    #[serde(default)]
    pub finish_reason: Option<String>,
}

/// Response message (may contain tool_calls instead of text content).
#[derive(Debug, Deserialize)]
pub(crate) struct WireResponseMessage {
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default)]
    pub tool_calls: Vec<WireToolCall>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct WireToolCall {
    pub id: String,
    pub function: WireToolCallFunction,
}

#[derive(Debug, Deserialize)]
pub(crate) struct WireToolCallFunction {
    pub name: String,
    pub arguments: String,
}

#[derive(Debug, Deserialize)]
pub(crate) struct WireUsage {
    pub prompt_tokens: Option<u64>,
    pub completion_tokens: Option<u64>,
    #[allow(dead_code)]
    pub total_tokens: Option<u64>,
}

// -- Streaming (SSE) response --

#[derive(Debug, Deserialize)]
pub(crate) struct ChatCompletionChunk {
    pub id: String,
    pub choices: Vec<ChunkChoice>,
    #[serde(default)]
    pub usage: Option<WireUsage>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct ChunkChoice {
    pub index: u32,
    pub delta: ChunkDelta,
    #[serde(default)]
    pub finish_reason: Option<String>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct ChunkDelta {
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default)]
    pub tool_calls: Vec<WireChunkToolCall>,
}

/// Streaming tool call fragment -- arguments arrive across multiple chunks.
#[derive(Debug, Deserialize)]
pub(crate) struct WireChunkToolCall {
    pub index: usize,
    #[serde(default)]
    pub id: Option<String>,
    #[serde(default)]
    pub function: Option<WireChunkToolCallFunction>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct WireChunkToolCallFunction {
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub arguments: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_non_streaming_response() {
        let json = r#"{
            "id": "chatcmpl-abc",
            "choices": [{
                "message": {"role": "assistant", "content": "Hello"},
                "finish_reason": "stop"
            }],
            "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
        }"#;
        let resp: ChatCompletionResponse = serde_json::from_str(json).unwrap();
        assert_eq!(resp.id, "chatcmpl-abc");
        assert_eq!(resp.choices[0].message.content.as_deref(), Some("Hello"));
        assert_eq!(resp.choices[0].finish_reason.as_deref(), Some("stop"));
        assert_eq!(resp.usage.as_ref().unwrap().prompt_tokens, Some(10));
    }

    #[test]
    fn parse_streaming_chunk() {
        let json = r#"{
            "id": "chatcmpl-abc",
            "choices": [{"index": 0, "delta": {"content": "Hi"}, "finish_reason": null}]
        }"#;
        let chunk: ChatCompletionChunk = serde_json::from_str(json).unwrap();
        assert_eq!(chunk.id, "chatcmpl-abc");
        assert_eq!(chunk.choices[0].delta.content.as_deref(), Some("Hi"));
        assert!(chunk.choices[0].finish_reason.is_none());
    }

    #[test]
    fn parse_streaming_chunk_finish() {
        let json = r#"{
            "id": "chatcmpl-abc",
            "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
            "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
        }"#;
        let chunk: ChatCompletionChunk = serde_json::from_str(json).unwrap();
        assert_eq!(chunk.choices[0].finish_reason.as_deref(), Some("stop"));
        assert!(chunk.usage.is_some());
    }

    #[test]
    fn request_serialization() {
        let req = ChatCompletionRequest {
            model: "glm-5".into(),
            messages: vec![WireMessage::user("hello")],
            max_tokens: Some(100),
            temperature: None,
            stream: None,
            response_format: None,
            tools: vec![],
            tool_choice: None,
        };
        let json = serde_json::to_value(&req).unwrap();
        assert_eq!(json["model"], "glm-5");
        assert_eq!(json["messages"][0]["role"], "user");
        assert!(json.get("temperature").is_none());
        assert!(json.get("stream").is_none());
    }

    #[test]
    fn request_with_response_format() {
        let req = ChatCompletionRequest {
            model: "glm-5".into(),
            messages: vec![],
            max_tokens: None,
            temperature: None,
            stream: None,
            response_format: Some(ResponseFormat {
                format_type: "json_schema".into(),
                json_schema: Some(serde_json::json!({"type": "object"})),
            }),
            tools: vec![],
            tool_choice: None,
        };
        let json = serde_json::to_value(&req).unwrap();
        assert_eq!(json["response_format"]["type"], "json_schema");
        assert_eq!(json["response_format"]["json_schema"]["type"], "object");
    }

    #[test]
    fn wire_message_constructors() {
        let s = WireMessage::system("sys");
        assert_eq!(s.role, "system");
        let u = WireMessage::user("usr");
        assert_eq!(u.role, "user");
        let a = WireMessage::assistant("ast");
        assert_eq!(a.role, "assistant");
    }

    #[test]
    fn request_with_tools_serializes() {
        let req = ChatCompletionRequest {
            model: "glm-5".into(),
            messages: vec![WireMessage::user("hello")],
            max_tokens: None,
            temperature: None,
            stream: None,
            response_format: None,
            tools: vec![WireTool {
                kind: "function".into(),
                function: WireToolFunction {
                    name: "greet".into(),
                    description: "say hello".into(),
                    parameters: serde_json::json!({"type": "object", "properties": {}}),
                },
            }],
            tool_choice: Some(serde_json::json!("required")),
        };
        let json = serde_json::to_value(&req).unwrap();
        assert_eq!(json["tools"][0]["type"], "function");
        assert_eq!(json["tools"][0]["function"]["name"], "greet");
        assert_eq!(json["tool_choice"], "required");
    }

    #[test]
    fn parse_response_with_tool_calls() {
        let json = r#"{
            "id": "chatcmpl-abc",
            "choices": [{
                "message": {
                    "role": "assistant",
                    "content": null,
                    "tool_calls": [{
                        "id": "call_xyz",
                        "type": "function",
                        "function": {"name": "submit", "arguments": "{\"x\": 1}"}
                    }]
                },
                "finish_reason": "tool_calls"
            }],
            "usage": null
        }"#;
        let resp: ChatCompletionResponse = serde_json::from_str(json).unwrap();
        let msg = &resp.choices[0].message;
        assert!(msg.content.is_none());
        assert_eq!(msg.tool_calls.len(), 1);
        assert_eq!(msg.tool_calls[0].id, "call_xyz");
        assert_eq!(msg.tool_calls[0].function.name, "submit");
        assert_eq!(msg.tool_calls[0].function.arguments, r#"{"x": 1}"#);
    }

    #[test]
    fn parse_chunk_with_tool_call_delta() {
        let json = r#"{
            "id": "cmpl-1",
            "choices": [{
                "index": 0,
                "delta": {
                    "tool_calls": [{
                        "index": 0,
                        "id": "call_abc",
                        "function": {"name": "submit", "arguments": "{\""}
                    }]
                },
                "finish_reason": null
            }]
        }"#;
        let chunk: ChatCompletionChunk = serde_json::from_str(json).unwrap();
        let tc = &chunk.choices[0].delta.tool_calls[0];
        assert_eq!(tc.index, 0);
        assert_eq!(tc.id.as_deref(), Some("call_abc"));
        assert_eq!(
            tc.function.as_ref().unwrap().name.as_deref(),
            Some("submit")
        );
    }
}