Commit Diff


commit - df099bfbf6d802f729aea67587f6dde90fac70ed
commit + 877f4c581cb9d71525b68bf4cebff5152a4e751e
blob - b3b300485a29d1064532b04e8ad1ee9a5bf7e29c
blob + c518f977566828fb20eba8b47b4db91a7070735b
--- src/cli.rs
+++ src/cli.rs
@@ -2,7 +2,7 @@
 
 use std::time::Duration;
 
-use crate::openai::Message;
+use crate::openai::{Message, ReasoningEffort};
 use clap::{
     arg, builder::PossibleValue, command, value_parser, ArgGroup, ArgMatches, Command, ValueEnum,
 };
@@ -18,6 +18,8 @@ pub struct Invocation {
     pub output_format: OutputFormat,
     pub file: String,
     pub timeout: Duration,
+    #[builder(default)]
+    pub reasoning_effort: Option<ReasoningEffort>,
 }
 
 /// The format that invocation's results are in
@@ -38,7 +40,7 @@ pub fn parse() -> Invocation {
 /// Top-level command
 fn cli() -> Command {
     command!()
-        .arg(arg!(model: -m --model <MODEL> "Sets model. See https://platform.openai.com/docs/models for model identifiers.").default_value("gpt-4-1106-preview"))
+        .arg(arg!(model: -m --model <MODEL> "Sets model. See https://platform.openai.com/docs/models for model identifiers.").default_value("gpt-5"))
         .arg(
             arg!(temperature: -t --temperature <TEMP> "Sets temperature")
                 .value_parser(value_parser!(f32))
@@ -70,6 +72,11 @@ fn cli() -> Command {
                 ])
                 .default_value("plaintext"),
         )
+        .arg(
+            arg!(reasoning_effort: --"reasoning-effort" <EFFORT> "Sets reasoning effort (low, medium, high)")
+                .value_parser(value_parser!(ReasoningEffort))
+                .required(false),
+        )
         .arg(arg!(--json "Shorthand for --output_format json"))
         .arg(arg!(--jsonp "Shorthand for --output_format jsonpretty"))
         .group(ArgGroup::new("output_format_short").args(["json", "jsonp"]))
@@ -103,6 +110,10 @@ impl From<ArgMatches> for Invocation {
             .expect("File is required")
             .to_string();
 
+        let reasoning_effort = matches
+            .get_one::<ReasoningEffort>("reasoning_effort")
+            .copied();
+
         Self {
             api_key,
             messages,
@@ -111,6 +122,7 @@ impl From<ArgMatches> for Invocation {
             timeout,
             output_format,
             file,
+            reasoning_effort,
         }
     }
 }
@@ -165,6 +177,20 @@ impl ValueEnum for OutputFormat {
     }
 }
 
+impl ValueEnum for ReasoningEffort {
+    fn value_variants<'a>() -> &'a [Self] {
+        &[Self::Low, Self::Medium, Self::High]
+    }
+
+    fn to_possible_value(&self) -> Option<PossibleValue> {
+        Some(match self {
+            Self::Low => PossibleValue::new("low"),
+            Self::Medium => PossibleValue::new("medium"),
+            Self::High => PossibleValue::new("high"),
+        })
+    }
+}
+
 #[cfg(test)]
 mod test {
     use super::*;
@@ -200,6 +226,16 @@ mod test {
     }
 
     #[test]
+    fn chat_reasoning_effort_flag() -> Result<()> {
+        let args = cli()
+            .try_get_matches_from(vec!["cogni", "-u", "USER", "--reasoning-effort", "high"])
+            .map(Invocation::from)?;
+
+        assert_eq!(args.reasoning_effort, Some(ReasoningEffort::High));
+        Ok(())
+    }
+
+    #[test]
     fn chat_many_msgs_with_system_prompt() -> Result<()> {
         let args = cli()
             .try_get_matches_from(vec![
blob - 6bfac73a4c54241803810f4357ebad5081198868
blob + 385cb933f50e88ff173b340a8a8252a807509bab
--- src/exec/chat.rs
+++ src/exec/chat.rs
@@ -1,7 +1,7 @@
 //! Implements chat subcommand
 
 use crate::cli::{Invocation, OutputFormat};
-use crate::openai::{self, ChatCompletion, FinishReason, Message};
+use crate::openai::{self, FinishReason, Message, Reasoning, Response};
 use crate::parse;
 use crate::Error;
 
@@ -26,17 +26,25 @@ pub async fn exec(args: Invocation) -> Result<()> {
         return Err(Error::NoMessagesProvided.into());
     }
 
-    // TODO: Lifetimes for `ChatCompletionRequest` fields
-    let request = openai::ChatCompletionRequest::builder()
+    // TODO: Lifetimes for `ResponseRequest` fields
+    let mut builder = openai::ResponseRequest::builder();
+
+    builder
         .model(args.model.clone())
         .messages(msgs)
         .temperature(args.temperature)
-        .timeout(args.timeout)
+        .timeout(args.timeout);
+
+    if let Some(effort) = args.reasoning_effort {
+        builder.reasoning(Some(Reasoning::from_effort(effort)));
+    }
+
+    let request = builder
         .build()
         .with_context(|| "failed to create request")?;
 
     let res = client
-        .chat_complete(&request)
+        .create_response(&request)
         .await
         .with_context(|| "failed to fetch request")?;
 
@@ -64,8 +72,8 @@ fn read_messages_from_file(file: &str) -> Result<Vec<M
     }
 }
 
-/// Show formatted output for `ChatCompletionRequest`
-fn show_response(dest: impl Write, args: &Invocation, resp: &ChatCompletion) -> Result<(), Error> {
+/// Show formatted output for a Responses API result
+fn show_response(dest: impl Write, args: &Invocation, resp: &Response) -> Result<(), Error> {
     let mut writer = BufWriter::new(dest);
     let choice = match resp.choices.len() {
         1 => &resp.choices[0],
@@ -109,7 +117,7 @@ mod test {
 
     use crate::{
         cli::{Invocation, InvocationBuilder, OutputFormat},
-        openai::{ChatCompletion, ChatCompletionBuilder, Choice, FinishReason, Message, Usage},
+        openai::{Choice, FinishReason, Message, Response, ResponseBuilder, Usage},
     };
 
     use super::*;
@@ -195,8 +203,8 @@ mod test {
             .to_owned()
     }
 
-    fn default_resp() -> ChatCompletionBuilder {
-        ChatCompletion::builder()
+    fn default_resp() -> ResponseBuilder {
+        Response::builder()
             .created(DateTime::default())
             .choices(vec![])
             .model(String::default())
blob - b9f70ce1fb66f6137ea849a3e439e7b8d2c909c9
blob + bf70c9c85ba7ea62c5c56807752895d0c3893300
--- src/openai.rs
+++ src/openai.rs
@@ -1,5 +1,6 @@
 //! Interactions with OpenAI APIs
 
+use std::convert::TryFrom;
 use std::time::Duration;
 
 use crate::Error;
@@ -8,9 +9,9 @@ use chrono::{DateTime, Utc};
 use derive_builder::Builder;
 use reqwest::StatusCode;
 use serde::{Deserialize, Serialize};
-use serde_json::json;
+use serde_json::{json, Value};
 
-/// Convienience Client for OpenAI Chat Completions API
+/// Convienience Client for OpenAI Responses API
 pub struct Client {
     /// Inner client
     client: reqwest::Client,
@@ -20,20 +21,22 @@ pub struct Client {
     base_url: String,
 }
 
-/// Requests for chat_completion
-/// Reference: <https://platform.openai.com/docs/api-reference/chat>
+/// Requests for the Responses API
+/// Reference: <https://platform.openai.com/docs/api-reference/responses>
 #[derive(Builder, Default)]
-pub struct ChatCompletionRequest {
+pub struct ResponseRequest {
     model: String,
     messages: Vec<Message>,
     temperature: f32,
     timeout: Duration,
+    #[builder(default)]
+    reasoning: Option<Reasoning>,
 }
 
-/// Responses from chat_completion
-/// Reference: <https://platform.openai.com/docs/api-reference/chat>
+/// Responses from the Responses API
+/// Reference: <https://platform.openai.com/docs/api-reference/responses>
 #[derive(Builder, Default, Debug, Serialize, Deserialize)]
-pub struct ChatCompletion {
+pub struct Response {
     #[serde(with = "ts_seconds")]
     pub created: DateTime<Utc>,
     pub choices: Vec<Choice>,
@@ -57,7 +60,7 @@ struct APIErrorContainer {
     error: APIError,
 }
 
-/// Messages in chat completion request and response
+/// Messages in Responses API request and response
 #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
 pub struct Message {
     pub role: Role,
@@ -74,11 +77,30 @@ pub enum Role {
 
 #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
 pub struct Usage {
-    pub prompt_tokens: u32,
-    pub completion_tokens: u32,
+    pub input_tokens: u32,
+    pub output_tokens: u32,
     pub total_tokens: u32,
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Reasoning {
+    pub effort: ReasoningEffort,
+}
+
+impl Reasoning {
+    pub fn from_effort(effort: ReasoningEffort) -> Self {
+        Self { effort }
+    }
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+pub enum ReasoningEffort {
+    Low,
+    Medium,
+    High,
+}
+
 #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum FinishReason {
@@ -106,32 +128,26 @@ impl Client {
         })
     }
 
-    pub async fn chat_complete(
-        &self,
-        request: &ChatCompletionRequest,
-    ) -> Result<ChatCompletion, Error> {
+    pub async fn create_response(&self, request: &ResponseRequest) -> Result<Response, Error> {
         let api_key = &self.api_key.as_ref().ok_or(Error::NoAPIKey)?;
-        let model = &request.model;
 
         let resp = self
             .client
-            .post(self.chat_endpoint())
+            .post(self.responses_endpoint())
             .bearer_auth(api_key)
             .timeout(request.timeout)
             .header("Content-Type", "application/json")
-            .json(&json!({
-                "model": model,
-                "messages": request.messages,
-                "temperature": request.temperature,
-            }))
+            .json(&request.to_payload())
             .send()
             .await
             .map_err(Error::FailedToFetch)?;
 
         match resp.status() {
             StatusCode::OK => {
-                let res: ChatCompletion = resp.json().await.map_err(Error::FailedToFetch)?;
-                Ok(res)
+                let responses: ResponsesAPIResponse =
+                    resp.json().await.map_err(Error::FailedToFetch)?;
+                let response = Response::try_from(responses).map_err(Error::UnexpectedResponse)?;
+                Ok(response)
             }
             _ => {
                 let error = resp
@@ -144,8 +160,8 @@ impl Client {
         }
     }
 
-    fn chat_endpoint(&self) -> String {
-        format!("{}{}", self.base_url, "/v1/chat/completions")
+    fn responses_endpoint(&self) -> String {
+        format!("{}{}", self.base_url, "/v1/responses")
     }
 }
 
@@ -170,47 +186,210 @@ impl Message {
     }
 }
 
-impl ChatCompletionRequest {
-    pub fn builder() -> ChatCompletionRequestBuilder {
-        ChatCompletionRequestBuilder::default()
+impl Message {
+    fn to_responses_input(&self) -> serde_json::Value {
+        json!({
+            "role": self.role.as_str(),
+            "content": [{
+                "type": "text",
+                "text": self.content.clone(),
+            }]
+        })
     }
 }
 
-impl ChatCompletion {
-    pub fn builder() -> ChatCompletionBuilder {
-        ChatCompletionBuilder::default()
+impl Role {
+    fn as_str(&self) -> &'static str {
+        match self {
+            Role::System => "system",
+            Role::Assistant => "assistant",
+            Role::User => "user",
+        }
     }
 }
 
+impl ResponseRequest {
+    pub fn builder() -> ResponseRequestBuilder {
+        ResponseRequestBuilder::default()
+    }
+
+    fn to_payload(&self) -> Value {
+        let input = self
+            .messages
+            .iter()
+            .map(Message::to_responses_input)
+            .collect::<Vec<_>>();
+
+        let mut payload = json!({
+            "model": self.model,
+            "input": input,
+            "temperature": self.temperature,
+        });
+
+        if let Some(reasoning) = &self.reasoning {
+            if let Some(obj) = payload.as_object_mut() {
+                obj.insert(
+                    "reasoning".to_string(),
+                    serde_json::to_value(reasoning).expect("failed to serialize reasoning"),
+                );
+            }
+        }
+
+        payload
+    }
+}
+
+impl Response {
+    pub fn builder() -> ResponseBuilder {
+        ResponseBuilder::default()
+    }
+}
+
+#[derive(Debug, Deserialize)]
+struct ResponsesAPIResponse {
+    #[serde(with = "ts_seconds")]
+    created: DateTime<Utc>,
+    model: String,
+    #[serde(default)]
+    output: Vec<ResponseOutput>,
+    usage: ResponsesUsage,
+}
+
+#[derive(Debug, Deserialize)]
+struct ResponsesUsage {
+    #[serde(default)]
+    input_tokens: u32,
+    #[serde(default)]
+    output_tokens: u32,
+    #[serde(default)]
+    total_tokens: u32,
+}
+
+#[derive(Debug, Deserialize)]
+struct ResponseOutput {
+    #[serde(rename = "type")]
+    item_type: String,
+    role: Option<Role>,
+    #[serde(default)]
+    content: Vec<ResponseContent>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+enum ResponseContent {
+    OutputText {
+        text: String,
+    },
+    Text {
+        text: String,
+    },
+    #[serde(other)]
+    Other,
+}
+
+impl ResponseContent {
+    fn as_text(&self) -> Option<&str> {
+        match self {
+            ResponseContent::OutputText { text } => Some(text),
+            ResponseContent::Text { text } => Some(text),
+            ResponseContent::Other => None,
+        }
+    }
+}
+
+impl ResponseOutput {
+    fn aggregated_text(&self) -> Option<String> {
+        let mut aggregated = String::new();
+
+        for part in &self.content {
+            if let Some(text) = part.as_text() {
+                aggregated.push_str(text);
+            }
+        }
+
+        if aggregated.is_empty() {
+            None
+        } else {
+            Some(aggregated)
+        }
+    }
+}
+
+impl TryFrom<ResponsesAPIResponse> for Response {
+    type Error = String;
+
+    fn try_from(value: ResponsesAPIResponse) -> Result<Self, Self::Error> {
+        let mut choices = Vec::new();
+
+        for output in value.output.into_iter() {
+            if output.item_type != "message" {
+                continue;
+            }
+
+            let text = output
+                .aggregated_text()
+                .ok_or_else(|| "response message missing text content".to_string())?;
+
+            let message = Message {
+                role: output.role.unwrap_or(Role::Assistant),
+                content: text,
+            };
+
+            choices.push(Choice {
+                message,
+                finish_reason: FinishReason::Stop,
+            });
+        }
+
+        if choices.is_empty() {
+            return Err("response did not contain any assistant messages".to_string());
+        }
+
+        Ok(Response {
+            created: value.created,
+            choices,
+            model: value.model,
+            usage: Usage {
+                input_tokens: value.usage.input_tokens,
+                output_tokens: value.usage.output_tokens,
+                total_tokens: value.usage.total_tokens,
+            },
+        })
+    }
+}
+
 #[cfg(test)]
 mod test {
 
     use super::*;
     use anyhow::Result;
     use chrono::TimeZone;
+    use std::time::Duration;
 
     #[test]
-    fn parse_chat_completion_response() -> Result<()> {
+    fn parse_response_payload() -> Result<()> {
         let data = r#"{
              "created": 1688413145,
-             "model": "gpt-3.5-turbo-0613",
-             "choices": [{
-                 "index": 0,
-                 "message": {
-                     "role": "assistant",
-                     "content": "Hello! How can I assist you today?"
-                 },
-                 "finish_reason": "stop"
+             "model": "gpt-5",
+             "output": [{
+                 "id": "msg_123",
+                 "type": "message",
+                 "role": "assistant",
+                 "content": [{
+                     "type": "output_text",
+                     "text": "Hello! How can I assist you today?"
+                 }]
              }],
              "usage": {
-                 "prompt_tokens": 8,
-                 "completion_tokens": 9,
+                 "input_tokens": 8,
+                 "output_tokens": 9,
                  "total_tokens": 17
              }
         }
         "#;
 
-        let resp = serde_json::from_str::<ChatCompletion>(data)?;
+        let resp = serde_json::from_str::<ResponsesAPIResponse>(data)?;
+        let resp = Response::try_from(resp).map_err(|e| anyhow::anyhow!(e))?;
 
         assert_eq!(resp.created, Utc.timestamp_opt(1688413145, 0).unwrap());
         assert_eq!(
@@ -223,12 +402,12 @@ mod test {
                 finish_reason: FinishReason::Stop
             }]
         );
-        assert_eq!(resp.model, "gpt-3.5-turbo-0613");
+        assert_eq!(resp.model, "gpt-5");
         assert_eq!(
             resp.usage,
             Usage {
-                prompt_tokens: 8,
-                completion_tokens: 9,
+                input_tokens: 8,
+                output_tokens: 9,
                 total_tokens: 17,
             }
         );
@@ -237,8 +416,108 @@ mod test {
     }
 
     #[test]
-    fn parse_chat_completion_error() -> Result<()> {
+    fn response_payload_includes_reasoning() -> Result<()> {
+        let request = ResponseRequest::builder()
+            .model("gpt-5".to_string())
+            .messages(vec![Message::user("Hello")])
+            .temperature(0.0)
+            .timeout(Duration::from_secs(30))
+            .reasoning(Some(Reasoning::from_effort(ReasoningEffort::High)))
+            .build()
+            .expect("request builds");
+
+        let payload = request.to_payload();
+
+        assert_eq!(payload["reasoning"]["effort"], "high");
+        assert_eq!(payload["model"], "gpt-5");
+
+        Ok(())
+    }
+
+    #[test]
+    fn response_payload_omits_reasoning_when_not_set() -> Result<()> {
+        let request = ResponseRequest::builder()
+            .model("gpt-5".to_string())
+            .messages(vec![Message::user("Hello")])
+            .temperature(0.0)
+            .timeout(Duration::from_secs(30))
+            .build()
+            .expect("request builds");
+
+        let payload = request.to_payload();
+
+        assert!(payload.get("reasoning").is_none());
+        Ok(())
+    }
+
+    #[test]
+    fn response_try_from_concatenates_segments() -> Result<()> {
         let data = r#"{
+             "created": 1688413145,
+             "model": "gpt-5",
+             "output": [{
+                 "id": "msg_123",
+                 "type": "message",
+                 "role": "assistant",
+                 "content": [{
+                     "type": "text",
+                     "text": "Hello"
+                 }, {
+                     "type": "output_text",
+                     "text": " world"
+                 }]
+             }],
+             "usage": {
+                 "input_tokens": 3,
+                 "output_tokens": 4,
+                 "total_tokens": 7
+             }
+        }
+        "#;
+
+        let resp = serde_json::from_str::<ResponsesAPIResponse>(data)?;
+        let resp = Response::try_from(resp).map_err(|e| anyhow::anyhow!(e))?;
+
+        assert_eq!(resp.choices.len(), 1);
+        assert_eq!(resp.choices[0].message.content, "Hello world");
+        Ok(())
+    }
+
+    #[test]
+    fn response_try_from_errors_without_assistant_message() -> Result<()> {
+        let data = r#"{
+             "created": 1688413145,
+             "model": "gpt-5",
+             "output": [{
+                 "id": "reasoning_1",
+                 "type": "reasoning",
+                 "content": [{
+                     "type": "text",
+                     "text": "Thinking..."
+                 }]
+             }],
+             "usage": {
+                 "input_tokens": 1,
+                 "output_tokens": 1,
+                 "total_tokens": 2
+             }
+        }
+        "#;
+
+        let resp = serde_json::from_str::<ResponsesAPIResponse>(data)?;
+        let err = Response::try_from(resp).expect_err("should error");
+
+        assert!(
+            err.contains("did not contain any assistant messages"),
+            "unexpected error text: {err}"
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn parse_response_error() -> Result<()> {
+        let data = r#"{
             "error": {
                 "message": "An error message",
                 "type": "invalid_request_error",
blob - 34fc261088ffad50b267ea437bfbc819576442a5
blob + 05b4e04c7c77c264b5006ba51802f1006384b482
--- tests/chat.rs
+++ tests/chat.rs
@@ -31,31 +31,36 @@ fn chat_user_message_from_flag() {
     let mut server = mockito::Server::new();
 
     let mock = server
-        .mock("POST", "/v1/chat/completions")
+        .mock("POST", "/v1/responses")
         .with_header("content-type", "application/json")
         .with_header("authorization", "Bearer ABCDE")
         .match_body(mockito::Matcher::PartialJson(json!({
-            "messages": [{
-             "content": "Hello",
-             "role": "user",
+            "model": "gpt-5",
+            "input": [{
+                "role": "user",
+                "content": [{
+                    "type": "text",
+                    "text": "Hello"
+                }]
             }]
         })))
         .with_body(
             r#"{
-             "id": "chatcmpl-XXXXX",
+             "id": "resp_XXXXX",
              "created": 1688413145,
-             "model": "gpt-3.5-turbo-0613",
-             "choices": [{
-                 "index": 0,
-                 "message": {
-                     "role": "assistant",
-                     "content": "ASSISTANT REPLY"
-                 },
-                 "finish_reason": "stop"
+             "model": "gpt-5",
+             "output": [{
+                 "id": "msg_XXXXX",
+                 "type": "message",
+                 "role": "assistant",
+                 "content": [{
+                     "type": "output_text",
+                     "text": "ASSISTANT REPLY"
+                 }]
              }],
              "usage": {
-                 "prompt_tokens": 8,
-                 "completion_tokens": 9,
+                 "input_tokens": 8,
+                 "output_tokens": 9,
                  "total_tokens": 17
              }
         }"#,
@@ -80,34 +85,39 @@ fn chat_user_message_from_stdin() {
     let mut server = mockito::Server::new();
 
     let mock = server
-        .mock("POST", "/v1/chat/completions")
+        .mock("POST", "/v1/responses")
         .with_header("content-type", "application/json")
         .with_header("authorization", "Bearer ABCDE")
         .match_body(mockito::Matcher::PartialJson(json!({
-            "messages": [{
-             "content": "Hello",
-             "role": "user",
+            "model": "gpt-5",
+            "input": [{
+                "role": "user",
+                "content": [{
+                    "type": "text",
+                    "text": "Hello"
+                }]
             }]
         })))
         .with_body(
             r#"{
-             "id": "chatcmpl-XXXXX",
+             "id": "resp_XXXXX",
              "created": 1688413145,
-             "model": "gpt-3.5-turbo-0613",
-             "choices": [{
-                 "index": 0,
-                 "message": {
-                     "role": "assistant",
-                     "content": "ASSISTANT REPLY"
-                 },
-                 "finish_reason": "stop"
+             "model": "gpt-5",
+             "output": [{
+                 "id": "msg_XXXXX",
+                 "type": "message",
+                 "role": "assistant",
+                 "content": [{
+                     "type": "output_text",
+                     "text": "ASSISTANT REPLY"
+                 }]
              }],
-             "usage": {
-                 "prompt_tokens": 8,
-                 "completion_tokens": 9,
-                 "total_tokens": 17
-             }
-        }"#,
+            "usage": {
+                "input_tokens": 8,
+                "output_tokens": 9,
+                "total_tokens": 17
+            }
+       }"#,
         )
         .create();
 
@@ -124,6 +134,63 @@ fn chat_user_message_from_stdin() {
         .stdout(predicate::str::contains("ASSISTANT REPLY"));
 }
 
+#[test]
+fn chat_with_reasoning_effort() {
+    let mut server = mockito::Server::new();
+
+    let mock = server
+        .mock("POST", "/v1/responses")
+        .with_header("content-type", "application/json")
+        .with_header("authorization", "Bearer ABCDE")
+        .match_body(mockito::Matcher::PartialJson(json!({
+            "model": "gpt-5",
+            "reasoning": {
+                "effort": "medium"
+            },
+            "input": [{
+                "role": "user",
+                "content": [{
+                    "type": "text",
+                    "text": "Hello"
+                }]
+            }]
+        })))
+        .with_body(
+            r#"{
+             "id": "resp_XXXXX",
+             "created": 1688413145,
+             "model": "gpt-5",
+             "output": [{
+                 "id": "msg_XXXXX",
+                 "type": "message",
+                 "role": "assistant",
+                 "content": [{
+                     "type": "output_text",
+                     "text": "ASSISTANT REPLY"
+                 }]
+             }],
+             "usage": {
+                 "input_tokens": 8,
+                 "output_tokens": 9,
+                 "total_tokens": 17
+             }
+        }"#,
+        )
+        .create();
+
+    let cmd = Command::cargo_bin("cogni")
+        .unwrap()
+        .args(["-u", "Hello", "--reasoning-effort", "medium"])
+        .env("OPENAI_API_ENDPOINT", server.url())
+        .env("OPENAI_API_KEY", "ABCDE")
+        .assert();
+
+    mock.assert();
+
+    cmd.success()
+        .stdout(predicate::str::contains("ASSISTANT REPLY"));
+}
+
 /// Test messages provided via
 /// - System message flag
 /// - Assistant message flag
@@ -134,46 +201,66 @@ fn chat_multiple_messages() {
     let mut server = mockito::Server::new();
 
     let mock = server
-        .mock("POST", "/v1/chat/completions")
+        .mock("POST", "/v1/responses")
         .with_header("content-type", "application/json")
         .with_header("authorization", "Bearer ABCDE")
         .match_body(mockito::Matcher::PartialJson(json!({
-            "messages": [{
+            "model": "gpt-5",
+            "input": [{
                 "role": "system",
-                "content": "SYSTEM",
+                "content": [{
+                    "type": "text",
+                    "text": "SYSTEM"
+                }],
             }, {
                 "role": "user",
-                "content": "USER_1",
+                "content": [{
+                    "type": "text",
+                    "text": "USER_1"
+                }],
             }, {
                 "role": "assistant",
-                "content": "ASSI_1",
+                "content": [{
+                    "type": "text",
+                    "text": "ASSI_1"
+                }],
             }, {
                 "role": "user",
-                "content": "USER_2",
+                "content": [{
+                    "type": "text",
+                    "text": "USER_2"
+                }],
             }, {
                 "role": "assistant",
-                "content": "ASSI_2",
+                "content": [{
+                    "type": "text",
+                    "text": "ASSI_2"
+                }],
             }, {
                 "role": "user",
-                "content": "USER_STDIN",
+                "content": [{
+                    "type": "text",
+                    "text": "USER_STDIN"
+                }],
             }]
         })))
         .with_body(
             r#"{
-             "id": "chatcmpl-XXXXX",
+             "id": "resp_XXXXX",
              "created": 1688413145,
-             "model": "gpt-3.5-turbo-0613",
-             "choices": [{
-                 "index": 0,
-                 "message": {
-                     "role": "assistant",
-                     "content": "ASSISTANT REPLY"
-                 },
-                 "finish_reason": "stop"
+             "model": "gpt-5",
+             "output": [{
+                 "id": "msg_XXXXX",
+                 "type": "message",
+                 "role": "assistant",
+                 "content": [{
+                     "type": "output_text",
+                     "text": "ASSISTANT REPLY"
+                 }]
              }],
              "usage": {
-                 "prompt_tokens": 8,
-                 "completion_tokens": 9,
+                 "input_tokens": 8,
+                 "output_tokens": 9,
                  "total_tokens": 17
              }
         }"#,
@@ -202,14 +289,18 @@ fn chat_api_error() {
     let mut server = mockito::Server::new();
 
     let mock = server
-        .mock("POST", "/v1/chat/completions")
+        .mock("POST", "/v1/responses")
         .with_status(400)
         .with_header("content-type", "application/json")
         .with_header("authorization", "Bearer ABCDE")
         .match_body(mockito::Matcher::PartialJson(json!({
-            "messages": [{
+            "model": "gpt-5",
+            "input": [{
                 "role": "user",
-                "content": "USER",
+                "content": [{
+                    "type": "text",
+                    "text": "USER"
+                }],
             }],
             "temperature": 1000.0 // invalid param
         })))
@@ -249,31 +340,36 @@ fn chat_user_message_from_file() {
     infile.write_str("Hello from file").unwrap();
 
     let mock = server
-        .mock("POST", "/v1/chat/completions")
+        .mock("POST", "/v1/responses")
         .with_header("content-type", "application/json")
         .with_header("authorization", "Bearer ABCDE")
         .match_body(mockito::Matcher::PartialJson(json!({
-            "messages": [{
+            "model": "gpt-5",
+            "input": [{
                 "role": "user",
-                "content": "Hello from file",
+                "content": [{
+                    "type": "text",
+                    "text": "Hello from file"
+                }],
             }],
         })))
         .with_body(
             r#"{
-             "id": "chatcmpl-XXXXX",
+             "id": "resp_XXXXX",
              "created": 1688413145,
-             "model": "gpt-3.5-turbo-0613",
-             "choices": [{
-                 "index": 0,
-                 "message": {
-                     "role": "assistant",
-                     "content": "ASSISTANT REPLY"
-                 },
-                 "finish_reason": "stop"
+             "model": "gpt-5",
+             "output": [{
+                 "id": "msg_XXXXX",
+                 "type": "message",
+                 "role": "assistant",
+                 "content": [{
+                     "type": "output_text",
+                     "text": "ASSISTANT REPLY"
+                 }]
              }],
              "usage": {
-                 "prompt_tokens": 8,
-                 "completion_tokens": 9,
+                 "input_tokens": 8,
+                 "output_tokens": 9,
                  "total_tokens": 17
              }
         }"#,