Files
codex/codex-rs/cli/src/responses_cmd.rs
Akshay Nathan 7995c66032 Stream apply_patch changes (#17862)
Adds new events for streaming apply_patch changes from responses api.
This is to enable clients to show progress during file writes.

Caveat: This does not work with apply_patch in function call mode, since
that required adding streaming json parsing.
2026-04-16 18:12:19 -07:00

250 lines
8.7 KiB
Rust

use clap::Parser;
use codex_core::config::Config;
use codex_utils_cli::CliConfigOverrides;
use serde_json::json;
use tokio::io::AsyncReadExt;
#[derive(Debug, Parser)]
pub(crate) struct ResponsesCommand {}
pub(crate) async fn run_responses_command(
root_config_overrides: CliConfigOverrides,
) -> anyhow::Result<()> {
let mut payload_text = String::new();
tokio::io::stdin().read_to_string(&mut payload_text).await?;
if payload_text.trim().is_empty() {
anyhow::bail!("expected Responses API JSON payload on stdin");
}
let payload: serde_json::Value = serde_json::from_str(&payload_text)
.map_err(|err| anyhow::anyhow!("failed to parse Responses API JSON payload: {err}"))?;
if payload.get("stream").and_then(serde_json::Value::as_bool) != Some(true) {
anyhow::bail!("codex responses expects a streaming payload with `\"stream\": true`");
}
let cli_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = Config::load_with_cli_overrides(cli_overrides).await?;
let base_auth_manager = codex_login::AuthManager::shared_from_config(
&config, /*enable_codex_api_key_env*/ true,
);
let auth_manager =
codex_login::auth_manager_for_provider(Some(base_auth_manager), &config.model_provider);
let auth = match auth_manager {
Some(auth_manager) => auth_manager.auth().await,
None => None,
};
let api_provider = config
.model_provider
.to_api_provider(auth.as_ref().map(codex_login::CodexAuth::auth_mode))?;
let api_auth = codex_login::auth_provider_from_auth(auth, &config.model_provider)?;
let client = codex_api::ResponsesClient::new(
codex_api::ReqwestTransport::new(codex_login::default_client::build_reqwest_client()),
api_provider,
api_auth,
);
let mut stream = client
.stream(
payload,
Default::default(),
codex_api::Compression::None,
/*turn_state*/ None,
)
.await?;
while let Some(event) = stream.rx_event.recv().await {
let event = event?;
println!("{}", serde_json::to_string(&response_event_to_json(event))?);
}
Ok(())
}
fn response_event_to_json(event: codex_api::ResponseEvent) -> serde_json::Value {
match event {
codex_api::ResponseEvent::Created => {
json!({ "type": "response.created", "response": {} })
}
codex_api::ResponseEvent::OutputItemDone(item) => {
json!({ "type": "response.output_item.done", "item": item })
}
codex_api::ResponseEvent::OutputItemAdded(item) => {
json!({ "type": "response.output_item.added", "item": item })
}
codex_api::ResponseEvent::ServerModel(model) => {
json!({ "type": "response.server_model", "model": model })
}
codex_api::ResponseEvent::ServerReasoningIncluded(included) => {
json!({ "type": "response.server_reasoning_included", "included": included })
}
codex_api::ResponseEvent::Completed {
response_id,
token_usage,
} => {
let response = match token_usage {
Some(token_usage) => json!({
"id": response_id,
"usage": {
"input_tokens": token_usage.input_tokens,
"input_tokens_details": {
"cached_tokens": token_usage.cached_input_tokens,
},
"output_tokens": token_usage.output_tokens,
"output_tokens_details": {
"reasoning_tokens": token_usage.reasoning_output_tokens,
},
"total_tokens": token_usage.total_tokens,
},
}),
None => json!({ "id": response_id }),
};
json!({ "type": "response.completed", "response": response })
}
codex_api::ResponseEvent::OutputTextDelta(delta) => {
json!({ "type": "response.output_text.delta", "delta": delta })
}
codex_api::ResponseEvent::ToolCallInputDelta {
item_id,
call_id,
delta,
} => {
json!({
"type": "response.tool_call_input.delta",
"item_id": item_id,
"call_id": call_id,
"delta": delta,
})
}
codex_api::ResponseEvent::ReasoningSummaryDelta {
delta,
summary_index,
} => json!({
"type": "response.reasoning_summary_text.delta",
"delta": delta,
"summary_index": summary_index,
}),
codex_api::ResponseEvent::ReasoningContentDelta {
delta,
content_index,
} => json!({
"type": "response.reasoning_text.delta",
"delta": delta,
"content_index": content_index,
}),
codex_api::ResponseEvent::ReasoningSummaryPartAdded { summary_index } => {
json!({
"type": "response.reasoning_summary_part.added",
"summary_index": summary_index,
})
}
codex_api::ResponseEvent::RateLimits(rate_limits) => {
json!({ "type": "response.rate_limits", "rate_limits": rate_limits })
}
codex_api::ResponseEvent::ModelsEtag(etag) => {
json!({ "type": "response.models_etag", "etag": etag })
}
}
}
#[cfg(test)]
mod tests {
use super::response_event_to_json;
use codex_protocol::protocol::TokenUsage;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn response_events_keep_replayable_response_envelopes() {
let created = response_event_to_json(codex_api::ResponseEvent::Created);
assert_eq!(created, json!({"type": "response.created", "response": {}}));
let completed = response_event_to_json(codex_api::ResponseEvent::Completed {
response_id: "resp-1".to_string(),
token_usage: Some(TokenUsage {
input_tokens: 10,
cached_input_tokens: 4,
output_tokens: 7,
reasoning_output_tokens: 3,
total_tokens: 17,
}),
});
assert_eq!(
completed,
json!({
"type": "response.completed",
"response": {
"id": "resp-1",
"usage": {
"input_tokens": 10,
"input_tokens_details": {
"cached_tokens": 4,
},
"output_tokens": 7,
"output_tokens_details": {
"reasoning_tokens": 3,
},
"total_tokens": 17,
},
},
})
);
let completed_without_usage = response_event_to_json(codex_api::ResponseEvent::Completed {
response_id: "resp-2".to_string(),
token_usage: None,
});
assert_eq!(
completed_without_usage,
json!({"type": "response.completed", "response": {"id": "resp-2"}})
);
}
#[test]
fn reasoning_deltas_use_responses_event_names() {
let summary = response_event_to_json(codex_api::ResponseEvent::ReasoningSummaryDelta {
delta: "plan".to_string(),
summary_index: 1,
});
assert_eq!(
summary,
json!({
"type": "response.reasoning_summary_text.delta",
"delta": "plan",
"summary_index": 1,
})
);
let content = response_event_to_json(codex_api::ResponseEvent::ReasoningContentDelta {
delta: "detail".to_string(),
content_index: 2,
});
assert_eq!(
content,
json!({
"type": "response.reasoning_text.delta",
"delta": "detail",
"content_index": 2,
})
);
}
#[test]
fn tool_call_input_delta_uses_responses_event_name() {
let delta = response_event_to_json(codex_api::ResponseEvent::ToolCallInputDelta {
item_id: "item-1".to_string(),
call_id: Some("call-1".to_string()),
delta: "patch".to_string(),
});
assert_eq!(
delta,
json!({
"type": "response.tool_call_input.delta",
"item_id": "item-1",
"call_id": "call-1",
"delta": "patch",
})
);
}
}