From 2bd507c6fb78512c41da9fac9efe5470ee98128c Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Sat, 1 Nov 2025 09:11:37 -0700 Subject: [PATCH] [app-server] generate API v2 types in its own namespace --- codex-rs/app-server-protocol/src/export.rs | 50 ++++++++++++++++++- .../app-server-protocol/src/protocol/v2.rs | 12 +++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index c8facffdb5..9a71f1569c 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -102,6 +102,20 @@ macro_rules! for_each_schema_type { $macro!(codex_protocol::protocol::FileChange); $macro!(codex_protocol::parse_command::ParsedCommand); $macro!(codex_protocol::protocol::SandboxPolicy); + + // v2 protocol types (namespaced in JSON Schema under definitions.v2 and on disk under v2/) + $macro!(crate::protocol::v2::Account); + $macro!(crate::protocol::v2::LoginAccountParams); + $macro!(crate::protocol::v2::LoginAccountResponse); + $macro!(crate::protocol::v2::LogoutAccountResponse); + $macro!(crate::protocol::v2::GetAccountRateLimitsResponse); + $macro!(crate::protocol::v2::GetAccountResponse); + $macro!(crate::protocol::v2::ListModelsParams); + $macro!(crate::protocol::v2::ReasoningEffortOption); + $macro!(crate::protocol::v2::Model); + $macro!(crate::protocol::v2::ListModelsResponse); + $macro!(crate::protocol::v2::UploadFeedbackParams); + $macro!(crate::protocol::v2::UploadFeedbackResponse); }; } @@ -112,7 +126,9 @@ pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { } pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { + let v2_out_dir = out_dir.join("v2"); ensure_dir(out_dir)?; + ensure_dir(&v2_out_dir)?; ClientRequest::export_all_to(out_dir)?; export_client_responses(out_dir)?; @@ -123,12 +139,15 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { ServerNotification::export_all_to(out_dir)?; generate_index_ts(out_dir)?; + generate_index_ts(&v2_out_dir)?; - let ts_files = ts_files_in(out_dir)?; + // Ensure our header is present on all TS files (root + subdirs like v2/). + let ts_files = ts_files_in_recursive(out_dir)?; for file in &ts_files { prepend_header_if_missing(file)?; } + // Optionally run Prettier on all generated TS files. if let Some(prettier_bin) = prettier && !ts_files.is_empty() { @@ -504,6 +523,26 @@ fn ts_files_in(dir: &Path) -> Result> { Ok(files) } +fn ts_files_in_recursive(dir: &Path) -> Result> { + let mut files = Vec::new(); + let mut stack = vec![dir.to_path_buf()]; + while let Some(d) = stack.pop() { + for entry in + fs::read_dir(&d).with_context(|| format!("Failed to read dir {}", d.display()))? + { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else if path.is_file() && path.extension() == Some(OsStr::new("ts")) { + files.push(path); + } + } + } + files.sort(); + Ok(files) +} + fn generate_index_ts(out_dir: &Path) -> Result { let mut entries: Vec = Vec::new(); let mut stems: Vec = ts_files_in(out_dir)? @@ -520,6 +559,14 @@ fn generate_index_ts(out_dir: &Path) -> Result { entries.push(format!("export type {{ {name} }} from \"./{name}\";\n")); } + // If this is the root out_dir and a ./v2 folder exists with TS files, + // expose it as a namespace to avoid symbol collisions at the root. + let v2_dir = out_dir.join("v2"); + let has_v2_ts = ts_files_in(&v2_dir).map(|v| !v.is_empty()).unwrap_or(false); + if has_v2_ts { + entries.push("export * as v2 from \"./v2\";\n".to_string()); + } + let mut content = String::with_capacity(HEADER.len() + entries.iter().map(String::len).sum::()); content.push_str(HEADER); @@ -546,6 +593,7 @@ mod tests { #[test] fn generated_ts_has_no_optional_nullable_fields() -> Result<()> { + // Assert that there are no types of the form "?: T | null" in the generated TS files. let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7())); fs::create_dir(&output_dir)?; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index ccd89e6a1f..fa082fa91c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -11,6 +11,7 @@ use uuid::Uuid; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] +#[ts(export_to = "v2/")] pub enum Account { #[serde(rename = "apiKey", rename_all = "camelCase")] #[ts(rename = "apiKey", rename_all = "camelCase")] @@ -27,6 +28,7 @@ pub enum Account { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type")] #[ts(tag = "type")] +#[ts(export_to = "v2/")] pub enum LoginAccountParams { #[serde(rename = "apiKey")] #[ts(rename = "apiKey")] @@ -42,6 +44,7 @@ pub enum LoginAccountParams { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct LoginAccountResponse { /// Only set if the login method is ChatGPT. #[schemars(with = "String")] @@ -54,22 +57,26 @@ pub struct LoginAccountResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct LogoutAccountResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct GetAccountRateLimitsResponse { pub rate_limits: RateLimitSnapshot, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct GetAccountResponse { pub account: Account, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct ListModelsParams { /// Optional page size; defaults to a reasonable server-side value. pub page_size: Option, @@ -79,6 +86,7 @@ pub struct ListModelsParams { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct Model { pub id: String, pub model: String, @@ -92,6 +100,7 @@ pub struct Model { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct ReasoningEffortOption { pub reasoning_effort: ReasoningEffort, pub description: String, @@ -99,6 +108,7 @@ pub struct ReasoningEffortOption { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct ListModelsResponse { pub items: Vec, /// Opaque cursor to pass to the next call to continue after the last item. @@ -108,6 +118,7 @@ pub struct ListModelsResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct UploadFeedbackParams { pub classification: String, pub reason: Option, @@ -117,6 +128,7 @@ pub struct UploadFeedbackParams { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] pub struct UploadFeedbackResponse { pub thread_id: String, }