Skip to main content

supercode_runtime/
request.rs

1//! Provider-neutral request types for the native runtime.
2
3use serde::Serialize;
4use supercode_interchange::ChatMessage;
5
6/// A tool advertised to a model.
7#[derive(Debug, Clone, PartialEq, Serialize)]
8pub struct ToolSchema {
9    /// Tool name.
10    pub name: String,
11    /// Description the model uses to decide when to call it.
12    pub description: String,
13    /// JSON Schema for the tool's input object.
14    pub parameters: serde_json::Value,
15}
16
17impl ToolSchema {
18    /// Construct a tool schema.
19    pub fn new(
20        name: impl Into<String>,
21        description: impl Into<String>,
22        parameters: serde_json::Value,
23    ) -> Self {
24        Self {
25            name: name.into(),
26            description: description.into(),
27            parameters,
28        }
29    }
30}
31
32/// A single model-completion request.
33#[derive(Debug, Clone, PartialEq)]
34pub struct ChatRequest {
35    /// Model id.
36    pub model: String,
37    /// Full conversation so far.
38    pub messages: Vec<ChatMessage>,
39    /// Tools to advertise (may be empty).
40    pub tools: Vec<ToolSchema>,
41    /// Optional sampling temperature.
42    pub temperature: Option<f32>,
43    /// Optional output token cap.
44    pub max_tokens: Option<u32>,
45    /// Reasoning/effort level, such as `"low"` or `"high"`.
46    pub effort: Option<String>,
47    /// Structured-output constraint sent as `response_format`.
48    pub response_format: Option<serde_json::Value>,
49    /// BP-13 (catalog D9 "Fast mode / service tiers"): the provider service
50    /// tier this request asks for (`"auto"`, `"priority"`, `"flex"`, …).
51    /// Sent verbatim as the OpenAI-compatible `service_tier` field; `None`
52    /// omits it, which is what every pre-BP-13 caller produced.
53    pub service_tier: Option<String>,
54    /// BP-13 (catalog D9 "Reasoning effort / thinking budgets"): a cap on
55    /// reasoning/thinking TOKENS for this request — Claude Code's
56    /// `MAX_THINKING_TOKENS`, the budget half of `effort`'s level half.
57    /// Sent as the unified `reasoning.max_tokens` field; `None` omits it.
58    pub thinking_budget: Option<u32>,
59    /// Arbitrary provider-native request fields.
60    pub extra_body: serde_json::Map<String, serde_json::Value>,
61}
62
63impl ChatRequest {
64    /// Construct a minimal request with a model and conversation.
65    pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
66        Self {
67            model: model.into(),
68            messages,
69            tools: Vec::new(),
70            temperature: None,
71            max_tokens: None,
72            effort: None,
73            response_format: None,
74            service_tier: None,
75            thinking_budget: None,
76            extra_body: serde_json::Map::new(),
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn minimal_request_has_no_optional_runtime_controls() {
87        let request = ChatRequest::new("example/model", vec![ChatMessage::user("hello")]);
88        assert_eq!(request.model, "example/model");
89        assert_eq!(request.messages.len(), 1);
90        assert!(request.tools.is_empty());
91        assert_eq!(request.temperature, None);
92        assert_eq!(request.max_tokens, None);
93        assert!(request.extra_body.is_empty());
94    }
95
96    #[test]
97    fn schema_constructor_preserves_provider_json() {
98        let parameters = serde_json::json!({"type": "object"});
99        let schema = ToolSchema::new("read", "Read a file", parameters.clone());
100        assert_eq!(schema.name, "read");
101        assert_eq!(schema.parameters, parameters);
102    }
103}