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}