Skip to content

Commit bfeb8c9

Browse files
authored
Add codex apply to apply a patch created from the Codex remote agent (#1528)
In order to to this, I created a new `chatgpt` crate where we can put any code that interacts directly with ChatGPT as opposed to the OpenAI API. I added a disclaimer to the README for it that it should primarily be modified by OpenAI employees. https://github.com/user-attachments/assets/bb978e33-d2c9-4d8e-af28-c8c25b1988e8
1 parent 9e58076 commit bfeb8c9

File tree

16 files changed

+559
-16
lines changed

16 files changed

+559
-16
lines changed

codex-rs/Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/chatgpt/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "codex-chatgpt"
3+
version = { workspace = true }
4+
edition = "2024"
5+
6+
[lints]
7+
workspace = true
8+
9+
[dependencies]
10+
anyhow = "1"
11+
clap = { version = "4", features = ["derive"] }
12+
serde = { version = "1", features = ["derive"] }
13+
serde_json = "1"
14+
codex-common = { path = "../common", features = ["cli"] }
15+
codex-core = { path = "../core" }
16+
codex-login = { path = "../login" }
17+
reqwest = { version = "0.12", features = ["json", "stream"] }
18+
tokio = { version = "1", features = ["full"] }
19+
20+
[dev-dependencies]
21+
tempfile = "3"

codex-rs/chatgpt/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ChatGPT
2+
3+
This crate pertains to first party ChatGPT APIs and products such as Codex agent.
4+
5+
This crate should be primarily built and maintained by OpenAI employees. Please reach out to a maintainer before making an external contribution.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use clap::Parser;
2+
use codex_common::CliConfigOverrides;
3+
use codex_core::config::Config;
4+
use codex_core::config::ConfigOverrides;
5+
6+
use crate::chatgpt_token::init_chatgpt_token_from_auth;
7+
use crate::get_task::GetTaskResponse;
8+
use crate::get_task::OutputItem;
9+
use crate::get_task::PrOutputItem;
10+
use crate::get_task::get_task;
11+
12+
/// Applies the latest diff from a Codex agent task.
13+
#[derive(Debug, Parser)]
14+
pub struct ApplyCommand {
15+
pub task_id: String,
16+
17+
#[clap(flatten)]
18+
pub config_overrides: CliConfigOverrides,
19+
}
20+
pub async fn run_apply_command(apply_cli: ApplyCommand) -> anyhow::Result<()> {
21+
let config = Config::load_with_cli_overrides(
22+
apply_cli
23+
.config_overrides
24+
.parse_overrides()
25+
.map_err(anyhow::Error::msg)?,
26+
ConfigOverrides::default(),
27+
)?;
28+
29+
init_chatgpt_token_from_auth(&config.codex_home).await?;
30+
31+
let task_response = get_task(&config, apply_cli.task_id).await?;
32+
apply_diff_from_task(task_response).await
33+
}
34+
35+
pub async fn apply_diff_from_task(task_response: GetTaskResponse) -> anyhow::Result<()> {
36+
let diff_turn = match task_response.current_diff_task_turn {
37+
Some(turn) => turn,
38+
None => anyhow::bail!("No diff turn found"),
39+
};
40+
let output_diff = diff_turn.output_items.iter().find_map(|item| match item {
41+
OutputItem::Pr(PrOutputItem { output_diff }) => Some(output_diff),
42+
_ => None,
43+
});
44+
match output_diff {
45+
Some(output_diff) => apply_diff(&output_diff.diff).await,
46+
None => anyhow::bail!("No PR output item found"),
47+
}
48+
}
49+
50+
async fn apply_diff(diff: &str) -> anyhow::Result<()> {
51+
let toplevel_output = tokio::process::Command::new("git")
52+
.args(vec!["rev-parse", "--show-toplevel"])
53+
.output()
54+
.await?;
55+
56+
if !toplevel_output.status.success() {
57+
anyhow::bail!("apply must be run from a git repository.");
58+
}
59+
60+
let repo_root = String::from_utf8(toplevel_output.stdout)?
61+
.trim()
62+
.to_string();
63+
64+
let mut git_apply_cmd = tokio::process::Command::new("git")
65+
.args(vec!["apply", "--3way"])
66+
.current_dir(&repo_root)
67+
.stdin(std::process::Stdio::piped())
68+
.stdout(std::process::Stdio::piped())
69+
.stderr(std::process::Stdio::piped())
70+
.spawn()?;
71+
72+
if let Some(mut stdin) = git_apply_cmd.stdin.take() {
73+
tokio::io::AsyncWriteExt::write_all(&mut stdin, diff.as_bytes()).await?;
74+
drop(stdin);
75+
}
76+
77+
let output = git_apply_cmd.wait_with_output().await?;
78+
79+
if !output.status.success() {
80+
anyhow::bail!(
81+
"Git apply failed with status {}: {}",
82+
output.status,
83+
String::from_utf8_lossy(&output.stderr)
84+
);
85+
}
86+
87+
println!("Successfully applied diff");
88+
Ok(())
89+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use codex_core::config::Config;
2+
3+
use crate::chatgpt_token::get_chatgpt_token_data;
4+
use crate::chatgpt_token::init_chatgpt_token_from_auth;
5+
6+
use anyhow::Context;
7+
use serde::de::DeserializeOwned;
8+
9+
/// Make a GET request to the ChatGPT backend API.
10+
pub(crate) async fn chatgpt_get_request<T: DeserializeOwned>(
11+
config: &Config,
12+
path: String,
13+
) -> anyhow::Result<T> {
14+
let chatgpt_base_url = &config.chatgpt_base_url;
15+
init_chatgpt_token_from_auth(&config.codex_home).await?;
16+
17+
// Make direct HTTP request to ChatGPT backend API with the token
18+
let client = reqwest::Client::new();
19+
let url = format!("{chatgpt_base_url}{path}");
20+
21+
let token =
22+
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
23+
24+
let response = client
25+
.get(&url)
26+
.bearer_auth(&token.access_token)
27+
.header("chatgpt-account-id", &token.account_id)
28+
.header("Content-Type", "application/json")
29+
.header("User-Agent", "codex-cli")
30+
.send()
31+
.await
32+
.context("Failed to send request")?;
33+
34+
if response.status().is_success() {
35+
let result: T = response
36+
.json()
37+
.await
38+
.context("Failed to parse JSON response")?;
39+
Ok(result)
40+
} else {
41+
let status = response.status();
42+
let body = response.text().await.unwrap_or_default();
43+
anyhow::bail!("Request failed with status {}: {}", status, body)
44+
}
45+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use std::path::Path;
2+
use std::sync::LazyLock;
3+
use std::sync::RwLock;
4+
5+
use codex_login::TokenData;
6+
7+
static CHATGPT_TOKEN: LazyLock<RwLock<Option<TokenData>>> = LazyLock::new(|| RwLock::new(None));
8+
9+
pub fn get_chatgpt_token_data() -> Option<TokenData> {
10+
CHATGPT_TOKEN.read().ok()?.clone()
11+
}
12+
13+
pub fn set_chatgpt_token_data(value: TokenData) {
14+
if let Ok(mut guard) = CHATGPT_TOKEN.write() {
15+
*guard = Some(value);
16+
}
17+
}
18+
19+
/// Initialize the ChatGPT token from auth.json file
20+
pub async fn init_chatgpt_token_from_auth(codex_home: &Path) -> std::io::Result<()> {
21+
let auth_json = codex_login::try_read_auth_json(codex_home).await?;
22+
set_chatgpt_token_data(auth_json.tokens.clone());
23+
Ok(())
24+
}

codex-rs/chatgpt/src/get_task.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use codex_core::config::Config;
2+
use serde::Deserialize;
3+
4+
use crate::chatgpt_client::chatgpt_get_request;
5+
6+
#[derive(Debug, Deserialize)]
7+
pub struct GetTaskResponse {
8+
pub current_diff_task_turn: Option<AssistantTurn>,
9+
}
10+
11+
// Only relevant fields for our extraction
12+
#[derive(Debug, Deserialize)]
13+
pub struct AssistantTurn {
14+
pub output_items: Vec<OutputItem>,
15+
}
16+
17+
#[derive(Debug, Deserialize)]
18+
#[serde(tag = "type")]
19+
pub enum OutputItem {
20+
#[serde(rename = "pr")]
21+
Pr(PrOutputItem),
22+
23+
#[serde(other)]
24+
Other,
25+
}
26+
27+
#[derive(Debug, Deserialize)]
28+
pub struct PrOutputItem {
29+
pub output_diff: OutputDiff,
30+
}
31+
32+
#[derive(Debug, Deserialize)]
33+
pub struct OutputDiff {
34+
pub diff: String,
35+
}
36+
37+
pub(crate) async fn get_task(config: &Config, task_id: String) -> anyhow::Result<GetTaskResponse> {
38+
let path = format!("/wham/tasks/{task_id}");
39+
chatgpt_get_request(config, path).await
40+
}

codex-rs/chatgpt/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod apply_command;
2+
mod chatgpt_client;
3+
mod chatgpt_token;
4+
pub mod get_task;

0 commit comments

Comments
 (0)