Skip to main content

kcode_codex_runtime_v2/
model.rs

1use std::{
2    fmt::{Debug, Formatter},
3    time::Duration,
4};
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::error::{Error, ErrorKind, Result};
10
11/// Model reasoning effort accepted by Codex.
12#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
13pub enum ReasoningEffort {
14    /// Disable reasoning when supported.
15    None,
16    /// Minimal reasoning.
17    Minimal,
18    /// Low reasoning.
19    Low,
20    /// Medium reasoning.
21    Medium,
22    /// High reasoning.
23    High,
24    /// Extra-high reasoning.
25    #[default]
26    XHigh,
27    /// Maximum reasoning when supported.
28    Max,
29}
30
31impl ReasoningEffort {
32    /// Returns the Codex protocol value.
33    pub const fn as_str(self) -> &'static str {
34        match self {
35            Self::None => "none",
36            Self::Minimal => "minimal",
37            Self::Low => "low",
38            Self::Medium => "medium",
39            Self::High => "high",
40            Self::XHigh => "xhigh",
41            Self::Max => "max",
42        }
43    }
44}
45
46/// A supported image media type for an inline image turn.
47#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48pub enum ImageMediaType {
49    /// Portable Network Graphics.
50    Png,
51    /// JPEG image data.
52    Jpeg,
53    /// WebP image data.
54    Webp,
55    /// Graphics Interchange Format.
56    Gif,
57}
58
59impl ImageMediaType {
60    /// Returns the media type used in the image data URL.
61    pub const fn mime_type(self) -> &'static str {
62        match self {
63            Self::Png => "image/png",
64            Self::Jpeg => "image/jpeg",
65            Self::Webp => "image/webp",
66            Self::Gif => "image/gif",
67        }
68    }
69}
70
71/// One in-memory image supplied to a dedicated image turn.
72#[derive(Clone, Eq, PartialEq)]
73pub struct ImageInput {
74    media_type: ImageMediaType,
75    bytes: Vec<u8>,
76}
77
78impl ImageInput {
79    /// Constructs a nonempty image input.
80    pub fn new(media_type: ImageMediaType, bytes: impl Into<Vec<u8>>) -> Result<Self> {
81        let bytes = bytes.into();
82        if bytes.is_empty() {
83            return Err(Error::new(
84                ErrorKind::InvalidInput,
85                "Codex image input must not be empty",
86            ));
87        }
88        Ok(Self { media_type, bytes })
89    }
90
91    /// Returns the declared image media type.
92    pub const fn media_type(&self) -> ImageMediaType {
93        self.media_type
94    }
95
96    /// Returns the exact image bytes.
97    pub fn bytes(&self) -> &[u8] {
98        &self.bytes
99    }
100}
101
102impl Debug for ImageInput {
103    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
104        formatter
105            .debug_struct("ImageInput")
106            .field("media_type", &self.media_type)
107            .field("byte_len", &self.bytes.len())
108            .finish()
109    }
110}
111
112/// A fresh, ephemeral, tool-free Codex image-analysis turn.
113#[derive(Clone, Debug, PartialEq)]
114pub struct ImageTurnRequest {
115    /// Exact text supplied after the image input items.
116    pub prompt: String,
117    /// Codex model identifier.
118    pub model: String,
119    /// Images supplied in caller order.
120    pub images: Vec<ImageInput>,
121    /// Requested reasoning effort.
122    pub reasoning_effort: ReasoningEffort,
123    /// Maximum duration of the complete turn.
124    pub timeout: Duration,
125}
126
127impl ImageTurnRequest {
128    /// Constructs an image turn using extra-high reasoning and a thirty-minute timeout.
129    pub fn new(
130        prompt: impl Into<String>,
131        model: impl Into<String>,
132        images: Vec<ImageInput>,
133    ) -> Self {
134        Self {
135            prompt: prompt.into(),
136            model: model.into(),
137            images,
138            reasoning_effort: ReasoningEffort::XHigh,
139            timeout: Duration::from_secs(75 * 60),
140        }
141    }
142}
143
144/// A dynamic function exposed to Codex for a fresh thread.
145#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct DynamicTool {
148    /// Function name visible to the model.
149    pub name: String,
150    /// Short function description visible to the model.
151    pub description: String,
152    /// JSON Schema for the function arguments.
153    pub input_schema: Value,
154}
155
156/// Exact caller-controlled material submitted to one model inference.
157///
158/// Provider transport metadata is intentionally excluded. A provider may add
159/// hidden instructions that are not observable to this client.
160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct ModelContext {
163    /// Exact UTF-8 text supplied as the model input.
164    pub input: String,
165    /// Stable provider label selected by the routing boundary.
166    pub provider: String,
167    /// Exact provider model identifier.
168    pub model: String,
169    /// Provider-native reasoning-effort label.
170    pub reasoning_effort: String,
171    /// Exact caller-controlled base instructions, when applicable.
172    pub base_instructions: Option<String>,
173    /// Exact caller-controlled developer instructions, when applicable.
174    pub developer_instructions: Option<String>,
175    /// Exact dynamic tool contracts supplied for this inference.
176    pub tools: Vec<DynamicTool>,
177}
178
179impl DynamicTool {
180    /// Constructs one function tool.
181    pub fn new(
182        name: impl Into<String>,
183        description: impl Into<String>,
184        input_schema: Value,
185    ) -> Self {
186        Self {
187            name: name.into(),
188            description: description.into(),
189            input_schema,
190        }
191    }
192}
193
194/// One standard Codex turn request.
195#[derive(Clone, Debug, PartialEq)]
196pub struct AgentRequest {
197    /// Exact text supplied as the user input item for this turn.
198    pub input: String,
199    /// Codex model identifier.
200    pub model: String,
201    /// Requested reasoning effort.
202    pub reasoning_effort: ReasoningEffort,
203    /// Existing Codex thread identifier to resume.
204    pub previous_thread_id: Option<String>,
205    /// Dynamic functions supplied when a fresh thread is created.
206    pub tools: Vec<DynamicTool>,
207    /// Whether a fresh thread avoids persistent Codex session storage.
208    pub ephemeral: bool,
209    /// Maximum duration of the complete turn, including tool handling.
210    pub timeout: Duration,
211}
212
213impl AgentRequest {
214    /// Constructs a fresh turn with no tools.
215    pub fn new(input: impl Into<String>, model: impl Into<String>) -> Self {
216        Self {
217            input: input.into(),
218            model: model.into(),
219            reasoning_effort: ReasoningEffort::XHigh,
220            previous_thread_id: None,
221            tools: Vec::new(),
222            ephemeral: false,
223            timeout: Duration::from_secs(75 * 60),
224        }
225    }
226}
227
228/// A dynamic function call requested by Codex.
229#[derive(Clone, Debug, PartialEq)]
230pub struct DynamicToolCall {
231    /// Protocol call identifier used when returning the result.
232    pub call_id: String,
233    /// Dynamic function name.
234    pub tool: String,
235    /// Parsed JSON arguments supplied by the model.
236    pub arguments: Value,
237}
238
239/// Caller-provided result for one dynamic function call.
240#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct ToolResult {
242    /// Whether the function completed successfully.
243    pub success: bool,
244    /// Text returned to the model.
245    pub text: String,
246}
247
248impl ToolResult {
249    /// Constructs a successful text result.
250    pub fn success(text: impl Into<String>) -> Self {
251        Self {
252            success: true,
253            text: text.into(),
254        }
255    }
256
257    /// Constructs a failed text result for the model.
258    pub fn failure(text: impl Into<String>) -> Self {
259        Self {
260            success: false,
261            text: text.into(),
262        }
263    }
264}
265
266/// Normalized cumulative and latest-turn token accounting.
267#[derive(Clone, Debug, Default, Eq, PartialEq)]
268pub struct TokenUsage {
269    /// Cumulative input tokens for the thread.
270    pub input_tokens: u64,
271    /// Cumulative output tokens for the thread, including reasoning output.
272    pub output_tokens: u64,
273    /// Cumulative cached input tokens for the thread.
274    pub cached_input_tokens: u64,
275    /// Cumulative reasoning output tokens for the thread.
276    pub reasoning_output_tokens: u64,
277    /// Input tokens for the latest model round.
278    pub last_input_tokens: Option<u64>,
279    /// Output tokens for the latest model round.
280    pub last_output_tokens: Option<u64>,
281}
282
283/// Successful terminal state of one Codex turn.
284#[derive(Clone, Debug, Eq, PartialEq)]
285pub struct CompletedTurn {
286    /// Codex thread identifier used for continuation.
287    pub thread_id: String,
288    /// Codex turn identifier.
289    pub turn_id: String,
290    /// Terminal assistant text. It may be empty after a control tool call.
291    pub answer: String,
292    /// Latest provider token accounting, when reported.
293    pub usage: Option<TokenUsage>,
294}
295
296/// Event yielded while a Codex turn is running.
297#[derive(Clone, Debug, PartialEq)]
298pub enum AgentEvent {
299    /// Exact UTF-8 JSONL record written by the client to Codex, including its newline.
300    ProviderInput(String),
301    /// Normalized caller-controlled material submitted to the model.
302    ModelContextSubmitted(ModelContext),
303    /// Provider accounting for the latest completed model inference.
304    ///
305    /// `TokenUsage` contains cumulative totals for the provider turn plus the
306    /// latest inference's input/output counts when the provider reports them.
307    UsageUpdated(TokenUsage),
308    /// A dynamic function call requiring a response from the caller.
309    ToolCall(DynamicToolCall),
310    /// The turn completed successfully.
311    Completed(CompletedTurn),
312}