mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
clean
This commit is contained in:
@@ -37,7 +37,7 @@ impl Default for OpenAiFileUploadOptions {
|
||||
pub struct UploadedOpenAiFile {
|
||||
pub file_id: String,
|
||||
pub uri: String,
|
||||
pub download_url: String,
|
||||
pub download_url: Option<String>,
|
||||
pub file_name: String,
|
||||
pub file_size_bytes: u64,
|
||||
pub mime_type: Option<String>,
|
||||
@@ -114,6 +114,10 @@ pub fn openai_file_uri(file_id: &str) -> String {
|
||||
format!("{OPENAI_FILE_URI_PREFIX}{file_id}")
|
||||
}
|
||||
|
||||
fn openai_file_api_base_url(base_url: &str) -> String {
|
||||
base_url.trim_end_matches('/').to_string()
|
||||
}
|
||||
|
||||
pub async fn download_openai_file(
|
||||
base_url: &str,
|
||||
auth: &impl AuthProvider,
|
||||
@@ -188,7 +192,8 @@ pub async fn upload_local_file(
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("file")
|
||||
.to_string();
|
||||
let create_url = format!("{}/files", base_url.trim_end_matches('/'));
|
||||
let api_base_url = openai_file_api_base_url(base_url);
|
||||
let create_url = format!("{api_base_url}/files");
|
||||
let mut create_request = serde_json::json!({
|
||||
"file_name": file_name,
|
||||
"file_size": metadata.len(),
|
||||
@@ -248,11 +253,19 @@ pub async fn upload_local_file(
|
||||
});
|
||||
}
|
||||
|
||||
let finalize_url = format!(
|
||||
"{}/files/{}/uploaded",
|
||||
base_url.trim_end_matches('/'),
|
||||
create_payload.file_id,
|
||||
);
|
||||
if options.store_in_library {
|
||||
return Ok(UploadedOpenAiFile {
|
||||
file_id: create_payload.file_id.clone(),
|
||||
uri: openai_file_uri(&create_payload.file_id),
|
||||
download_url: None,
|
||||
file_name,
|
||||
file_size_bytes: metadata.len(),
|
||||
mime_type: None,
|
||||
path: path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
let finalize_url = format!("{api_base_url}/files/{}/uploaded", create_payload.file_id);
|
||||
let finalize_started_at = Instant::now();
|
||||
loop {
|
||||
let finalize_response = authorized_request(auth, reqwest::Method::POST, &finalize_url)
|
||||
@@ -283,12 +296,12 @@ pub async fn upload_local_file(
|
||||
return Ok(UploadedOpenAiFile {
|
||||
file_id: create_payload.file_id.clone(),
|
||||
uri: openai_file_uri(&create_payload.file_id),
|
||||
download_url: finalize_payload.download_url.ok_or_else(|| {
|
||||
download_url: Some(finalize_payload.download_url.ok_or_else(|| {
|
||||
OpenAiFileError::UploadFailed {
|
||||
file_id: create_payload.file_id.clone(),
|
||||
message: "missing download_url".to_string(),
|
||||
}
|
||||
})?,
|
||||
})?),
|
||||
file_name: finalize_payload.file_name.unwrap_or(file_name),
|
||||
file_size_bytes: metadata.len(),
|
||||
mime_type: finalize_payload.mime_type,
|
||||
@@ -506,7 +519,7 @@ mod tests {
|
||||
assert_eq!(uploaded.uri, "sediment://file_123");
|
||||
assert_eq!(
|
||||
uploaded.download_url,
|
||||
format!("{}/download/file_123", server.uri())
|
||||
Some(format!("{}/download/file_123", server.uri()))
|
||||
);
|
||||
assert_eq!(uploaded.file_name, "hello.txt");
|
||||
assert_eq!(uploaded.mime_type, Some("text/plain".to_string()));
|
||||
|
||||
@@ -272,6 +272,8 @@ pub struct ConfigToml {
|
||||
|
||||
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
/// Optional override for the OpenAI file upload/download API base.
|
||||
pub openai_file_api_base_url: Option<String>,
|
||||
|
||||
/// Base URL override for the built-in `openai` model provider.
|
||||
pub openai_base_url: Option<String>,
|
||||
|
||||
@@ -38,6 +38,7 @@ pub struct ConfigProfile {
|
||||
pub model_catalog_json: Option<AbsolutePathBuf>,
|
||||
pub personality: Option<Personality>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
pub openai_file_api_base_url: Option<String>,
|
||||
/// Optional path to a file containing model instructions.
|
||||
pub model_instructions_file: Option<AbsolutePathBuf>,
|
||||
pub js_repl_node_path: Option<AbsolutePathBuf>,
|
||||
|
||||
@@ -318,6 +318,9 @@
|
||||
"chatgpt_base_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"openai_file_api_base_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"experimental_compact_prompt_file": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
@@ -2263,6 +2266,13 @@
|
||||
"description": "Base URL for requests to ChatGPT (as opposed to the OpenAI API).",
|
||||
"type": "string"
|
||||
},
|
||||
"openai_file_api_base_url": {
|
||||
"description": "Optional override for the OpenAI file upload/download API base.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"check_for_update_on_startup": {
|
||||
"description": "When `true`, checks for Codex updates on startup and surfaces update prompts. Set to `false` only if your Codex updates are centrally managed. Defaults to `true`.",
|
||||
"type": "boolean"
|
||||
@@ -2982,4 +2992,4 @@
|
||||
},
|
||||
"title": "ConfigToml",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::codex_apps_mcp_tools::should_materialize_codex_apps_file_download;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::codex_apps_mcp_tools::should_materialize_codex_apps_file_download;
|
||||
use codex_api::download_openai_file;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider::AuthorizationHeaderAuthProvider;
|
||||
@@ -29,6 +29,14 @@ struct CodexAppsFileUri {
|
||||
file_name: Option<String>,
|
||||
}
|
||||
|
||||
fn codex_apps_download_base_url<'a>(turn_context: &'a TurnContext, download_url: &str) -> &'a str {
|
||||
if download_url.starts_with("/api/codex/") {
|
||||
turn_context.config.chatgpt_base_url.as_str()
|
||||
} else {
|
||||
turn_context.config.openai_file_api_base_url()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_materialize_codex_apps_file_download_result(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
@@ -37,7 +45,8 @@ pub(crate) async fn maybe_materialize_codex_apps_file_download_result(
|
||||
result: CallToolResult,
|
||||
) -> CallToolResult {
|
||||
let auth = sess.services.auth_manager.auth().await;
|
||||
let authorization_header_value = match sess.authorization_header_for_current_agent_task().await {
|
||||
let authorization_header_value = match sess.authorization_header_for_current_agent_task().await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
warn!(error = %error, "failed to build agent assertion authorization for codex_apps file download materialization");
|
||||
@@ -74,6 +83,8 @@ async fn maybe_materialize_codex_apps_file_download_result_with_auth(
|
||||
let Some(payload) = extract_codex_apps_file_download_payload(&result) else {
|
||||
return result;
|
||||
};
|
||||
let download_base_url =
|
||||
codex_apps_download_base_url(turn_context, &payload.file_uri.download_url);
|
||||
if result.structured_content.is_none()
|
||||
&& let Ok(structured_content) = serde_json::to_value(&payload)
|
||||
{
|
||||
@@ -95,7 +106,7 @@ async fn maybe_materialize_codex_apps_file_download_result_with_auth(
|
||||
auth_provider = auth_provider.with_fedramp_routing_header();
|
||||
}
|
||||
download_openai_file(
|
||||
turn_context.config.chatgpt_base_url.trim_end_matches('/'),
|
||||
download_base_url,
|
||||
&auth_provider,
|
||||
&payload.file_uri.download_url,
|
||||
)
|
||||
@@ -114,7 +125,7 @@ async fn maybe_materialize_codex_apps_file_download_result_with_auth(
|
||||
is_fedramp_account: auth.is_fedramp_account(),
|
||||
};
|
||||
download_openai_file(
|
||||
turn_context.config.chatgpt_base_url.trim_end_matches('/'),
|
||||
download_base_url,
|
||||
&auth_provider,
|
||||
&payload.file_uri.download_url,
|
||||
)
|
||||
@@ -267,35 +278,11 @@ mod tests {
|
||||
.expect("_codex_apps metadata object")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_apps_file_download_materialization_ignores_results_without_metadata_flag() {
|
||||
let (_, turn_context) = make_session_and_context().await;
|
||||
let original = CallToolResult {
|
||||
content: vec![serde_json::json!({"type": "text", "text": "hello"})],
|
||||
structured_content: Some(serde_json::json!({"x": 1})),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let result = maybe_materialize_codex_apps_file_download_result_with_auth(
|
||||
&turn_context,
|
||||
"session-1",
|
||||
Some(&CodexAuth::create_dummy_chatgpt_auth_for_testing()),
|
||||
None,
|
||||
"custom_server",
|
||||
/*codex_apps_meta*/ None,
|
||||
original.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, original);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_apps_file_download_materialization_adds_local_path_for_marked_tools() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/codex/files/file_123/content"))
|
||||
.and(path("/download/file_123"))
|
||||
.and(header("authorization", "Bearer Access Token"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.respond_with(
|
||||
@@ -319,7 +306,7 @@ mod tests {
|
||||
"file_id": "file_123",
|
||||
"file_name": "testing-file.txt",
|
||||
"file_uri": {
|
||||
"download_url": "/api/codex/files/file_123/content",
|
||||
"download_url": format!("{}/download/file_123", server.uri()),
|
||||
"file_id": "file_123",
|
||||
"file_name": "testing-file.txt",
|
||||
"mime_type": "text/plain",
|
||||
@@ -361,9 +348,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_apps_file_download_materialization_uses_json_text_when_structured_content_is_missing()
|
||||
{
|
||||
let server = MockServer::start().await;
|
||||
async fn codex_apps_file_download_materialization_uses_chatgpt_base_for_relative_codex_urls() {
|
||||
let chatgpt_server = MockServer::start().await;
|
||||
let file_api_server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/codex/files/file_123/content"))
|
||||
.and(header("authorization", "Bearer Access Token"))
|
||||
@@ -371,29 +358,28 @@ mod tests {
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.insert_header("content-type", "text/plain")
|
||||
.set_body_bytes(b"downloaded contents".to_vec()),
|
||||
.set_body_bytes(b"downloaded via codex backend".to_vec()),
|
||||
)
|
||||
.mount(&server)
|
||||
.mount(&chatgpt_server)
|
||||
.await;
|
||||
|
||||
let (_, mut turn_context) = make_session_and_context().await;
|
||||
let mut config = (*turn_context.config).clone();
|
||||
config.chatgpt_base_url = format!("{}/backend-api/codex", server.uri());
|
||||
config.chatgpt_base_url = chatgpt_server.uri();
|
||||
config.openai_file_api_base_url = Some(format!("{}/backend-api", file_api_server.uri()));
|
||||
turn_context.config = Arc::new(config);
|
||||
let original = CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": serde_json::json!({
|
||||
content: vec![],
|
||||
structured_content: Some(serde_json::json!({
|
||||
"file_id": "file_123",
|
||||
"file_name": "testing-file.txt",
|
||||
"file_uri": {
|
||||
"download_url": "/api/codex/files/file_123/content",
|
||||
"file_id": "file_123",
|
||||
"file_name": "testing-file.txt",
|
||||
"file_uri": {
|
||||
"download_url": "/api/codex/files/file_123/content",
|
||||
"file_name": "testing-file.txt",
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
})],
|
||||
structured_content: None,
|
||||
"mime_type": "text/plain",
|
||||
}
|
||||
})),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
};
|
||||
@@ -410,29 +396,14 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let local_path = result
|
||||
.content
|
||||
.iter()
|
||||
.find_map(|item| {
|
||||
item.get("text")
|
||||
.and_then(|text| text.as_str())
|
||||
.and_then(|text| text.strip_prefix("Downloaded file to local path: "))
|
||||
})
|
||||
.expect("expected local path announcement");
|
||||
assert_eq!(
|
||||
result.structured_content,
|
||||
Some(serde_json::json!({
|
||||
"file_id": "file_123",
|
||||
"file_name": "testing-file.txt",
|
||||
"file_uri": {
|
||||
"download_url": "/api/codex/files/file_123/content",
|
||||
"file_name": "testing-file.txt",
|
||||
},
|
||||
"local_path": local_path,
|
||||
}))
|
||||
);
|
||||
.structured_content
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("local_path"))
|
||||
.and_then(JsonValue::as_str)
|
||||
.expect("local_path in structured content");
|
||||
assert_eq!(
|
||||
tokio::fs::read(local_path).await.expect("downloaded file"),
|
||||
b"downloaded contents"
|
||||
b"downloaded via codex backend"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5005,6 +5005,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
model_verbosity: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
openai_file_api_base_url: None,
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
experimental_realtime_ws_base_url: None,
|
||||
@@ -5157,6 +5158,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
model_verbosity: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
openai_file_api_base_url: None,
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
experimental_realtime_ws_base_url: None,
|
||||
@@ -5307,6 +5309,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
model_verbosity: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
openai_file_api_base_url: None,
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
experimental_realtime_ws_base_url: None,
|
||||
@@ -5442,6 +5445,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
model_verbosity: Some(Verbosity::High),
|
||||
personality: Some(Personality::Pragmatic),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
openai_file_api_base_url: None,
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
experimental_realtime_ws_base_url: None,
|
||||
|
||||
@@ -512,6 +512,8 @@ pub struct Config {
|
||||
|
||||
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
||||
pub chatgpt_base_url: String,
|
||||
/// Optional override for the OpenAI file upload/download API base.
|
||||
pub openai_file_api_base_url: Option<String>,
|
||||
|
||||
/// Machine-local realtime audio device preferences used by realtime voice.
|
||||
pub realtime_audio: RealtimeAudioConfig,
|
||||
@@ -798,6 +800,12 @@ impl ConfigBuilder {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn openai_file_api_base_url(&self) -> &str {
|
||||
self.openai_file_api_base_url
|
||||
.as_deref()
|
||||
.unwrap_or(self.chatgpt_base_url.as_str())
|
||||
}
|
||||
|
||||
pub fn to_models_manager_config(&self) -> ModelsManagerConfig {
|
||||
ModelsManagerConfig {
|
||||
model_context_window: self.model_context_window,
|
||||
@@ -2309,6 +2317,9 @@ impl Config {
|
||||
.chatgpt_base_url
|
||||
.or(cfg.chatgpt_base_url)
|
||||
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
||||
openai_file_api_base_url: config_profile
|
||||
.openai_file_api_base_url
|
||||
.or(cfg.openai_file_api_base_url),
|
||||
realtime_audio: cfg
|
||||
.audio
|
||||
.map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig {
|
||||
|
||||
@@ -141,8 +141,16 @@ async fn build_uploaded_local_argument_value(
|
||||
};
|
||||
let default_upload_options = OpenAiFileUploadOptions::default();
|
||||
let uploaded = upload_local_file(
|
||||
<<<<<<< HEAD
|
||||
turn_context.config.chatgpt_base_url.trim_end_matches('/'),
|
||||
&upload_auth,
|
||||
=======
|
||||
turn_context
|
||||
.config
|
||||
.openai_file_api_base_url()
|
||||
.trim_end_matches('/'),
|
||||
upload_auth.as_ref(),
|
||||
>>>>>>> e66c01c9f (clean)
|
||||
&resolved_path,
|
||||
upload_options.unwrap_or(&default_upload_options),
|
||||
)
|
||||
@@ -153,14 +161,25 @@ async fn build_uploaded_local_argument_value(
|
||||
}
|
||||
None => format!("failed to upload `{file_path}` for `{field_name}`: {error}"),
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"download_url": uploaded.download_url,
|
||||
"file_id": uploaded.file_id,
|
||||
"mime_type": uploaded.mime_type,
|
||||
"file_name": uploaded.file_name,
|
||||
"uri": uploaded.uri,
|
||||
"file_size_bytes": uploaded.file_size_bytes,
|
||||
}))
|
||||
let mut uploaded_value = serde_json::Map::from_iter([
|
||||
("file_id".to_string(), serde_json::json!(uploaded.file_id)),
|
||||
(
|
||||
"file_name".to_string(),
|
||||
serde_json::json!(uploaded.file_name),
|
||||
),
|
||||
("uri".to_string(), serde_json::json!(uploaded.uri)),
|
||||
(
|
||||
"file_size_bytes".to_string(),
|
||||
serde_json::json!(uploaded.file_size_bytes),
|
||||
),
|
||||
]);
|
||||
if let Some(download_url) = uploaded.download_url {
|
||||
uploaded_value.insert("download_url".to_string(), serde_json::json!(download_url));
|
||||
}
|
||||
if let Some(mime_type) = uploaded.mime_type {
|
||||
uploaded_value.insert("mime_type".to_string(), serde_json::json!(mime_type));
|
||||
}
|
||||
Ok(JsonValue::Object(uploaded_value))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -274,93 +293,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_uploaded_local_argument_value_honors_upload_options() {
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::body_json;
|
||||
use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/backend-api/files"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.and(body_json(serde_json::json!({
|
||||
"file_name": "library.txt",
|
||||
"file_size": 7,
|
||||
"use_case": "codex",
|
||||
"store_in_library": true,
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"file_id": "file_library",
|
||||
"upload_url": format!("{}/upload/file_library", server.uri()),
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/upload/file_library"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/backend-api/files/file_library/uploaded"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"status": "success",
|
||||
"download_url": format!("{}/download/file_library", server.uri()),
|
||||
"file_name": "library.txt",
|
||||
"mime_type": "text/plain",
|
||||
"file_size_bytes": 7,
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let dir = tempdir().expect("temp dir");
|
||||
let local_path = dir.path().join("library.txt");
|
||||
tokio::fs::write(&local_path, b"library")
|
||||
.await
|
||||
.expect("write local file");
|
||||
turn_context.cwd = AbsolutePathBuf::try_from(dir.path()).expect("absolute path");
|
||||
|
||||
let mut config = (*turn_context.config).clone();
|
||||
config.chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||
turn_context.config = Arc::new(config);
|
||||
|
||||
let upload_options = OpenAiFileUploadOptions {
|
||||
store_in_library: true,
|
||||
};
|
||||
let rewritten = build_uploaded_local_argument_value(
|
||||
&session,
|
||||
&turn_context,
|
||||
Some(&auth),
|
||||
"file",
|
||||
/*index*/ None,
|
||||
"library.txt",
|
||||
Some(&upload_options),
|
||||
)
|
||||
.await
|
||||
.expect("rewrite should upload the local file");
|
||||
|
||||
assert_eq!(
|
||||
rewritten,
|
||||
serde_json::json!({
|
||||
"download_url": format!("{}/download/file_library", server.uri()),
|
||||
"file_id": "file_library",
|
||||
"mime_type": "text/plain",
|
||||
"file_name": "library.txt",
|
||||
"uri": "sediment://file_library",
|
||||
"file_size_bytes": 7,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rewrite_argument_value_for_openai_files_rewrites_scalar_path() {
|
||||
use wiremock::Mock;
|
||||
|
||||
@@ -67,16 +67,6 @@ fn approval_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
fn direct_exposed_builtin_codex_apps_meta() -> serde_json::Map<String, serde_json::Value> {
|
||||
serde_json::json!({
|
||||
"provider": "builtin",
|
||||
"direct_expose": true,
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("_codex_apps metadata should be an object")
|
||||
}
|
||||
|
||||
fn prompt_options(
|
||||
allow_session_remember: bool,
|
||||
allow_persistent_approval: bool,
|
||||
@@ -550,32 +540,6 @@ fn codex_apps_connectors_support_persistent_approval() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_exposed_builtin_codex_apps_tools_do_not_support_persistent_approval() {
|
||||
let invocation = McpInvocation {
|
||||
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool: "builtin_search_file".to_string(),
|
||||
arguments: None,
|
||||
};
|
||||
let mut metadata = approval_metadata(
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
/*connector_description*/ None,
|
||||
/*tool_title*/ Some("Builtin Search File"),
|
||||
/*tool_description*/ Some("Search builtin files."),
|
||||
);
|
||||
metadata.codex_apps_meta = Some(direct_exposed_builtin_codex_apps_meta());
|
||||
|
||||
assert_eq!(
|
||||
session_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_mcp_tool_result_for_model_rewrites_image_content() {
|
||||
let result = Ok(CallToolResult {
|
||||
|
||||
@@ -280,36 +280,6 @@ async fn always_defer_feature_preserves_explicit_apps() {
|
||||
assert!(!deferred_tools.contains_key("mcp__codex_apps__calendar_create_event"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn directly_exposes_builtin_codex_apps_tools_marked_for_direct_exposure() {
|
||||
let config = test_config().await;
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
|
||||
let mcp_tools = HashMap::from([(
|
||||
"mcp__codex_apps__builtin_search_file".to_string(),
|
||||
make_mcp_tool_with_meta(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"builtin_search_file",
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
Some(direct_exposed_builtin_codex_apps_meta()),
|
||||
),
|
||||
)]);
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
&mcp_tools,
|
||||
/*connectors*/ None,
|
||||
&[],
|
||||
&config,
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
exposure.direct_tools.into_keys().collect::<Vec<_>>(),
|
||||
vec!["mcp__codex_apps__builtin_search_file".to_string()]
|
||||
);
|
||||
assert!(exposure.deferred_tools.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn keeps_direct_exposed_builtin_codex_apps_tools_direct_in_large_search_sets() {
|
||||
let config = test_config().await;
|
||||
|
||||
@@ -39,10 +39,12 @@ fn configure_apps(config: &mut Config, chatgpt_base_url: &str) {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let upload_server = start_mock_server().await;
|
||||
let upload_server_uri = upload_server.uri();
|
||||
let apps_server = AppsTestServer::mount(&server).await?;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/files"))
|
||||
.and(path("/api/files"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.and(body_json(json!({
|
||||
"file_name": "report.txt",
|
||||
@@ -51,32 +53,33 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"file_id": "file_123",
|
||||
"upload_url": format!("{}/upload/file_123", server.uri()),
|
||||
"upload_url": format!("{}/upload/file_123", upload_server_uri),
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.mount(&upload_server)
|
||||
.await;
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/upload/file_123"))
|
||||
.and(header("content-length", "11"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.mount(&upload_server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/files/file_123/uploaded"))
|
||||
.and(path("/api/files/file_123/uploaded"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"status": "success",
|
||||
"download_url": format!("{}/download/file_123", server.uri()),
|
||||
"download_url": format!("{}/download/file_123", upload_server_uri),
|
||||
"file_name": "report.txt",
|
||||
"mime_type": "text/plain",
|
||||
"file_size_bytes": 11,
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.mount(&upload_server)
|
||||
.await;
|
||||
|
||||
let call_id = "extract-call-1";
|
||||
let upload_server_api_base = format!("{}/api", upload_server_uri);
|
||||
let mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
@@ -101,7 +104,10 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| configure_apps(config, apps_server.chatgpt_base_url.as_str()));
|
||||
.with_config(move |config| {
|
||||
configure_apps(config, apps_server.chatgpt_base_url.as_str());
|
||||
config.openai_file_api_base_url = Some(upload_server_api_base.clone());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
tokio::fs::write(test.cwd.path().join("report.txt"), b"hello world").await?;
|
||||
|
||||
@@ -147,7 +153,7 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
|
||||
assert_eq!(
|
||||
apps_tool_call.pointer("/params/arguments/file"),
|
||||
Some(&json!({
|
||||
"download_url": format!("{}/download/file_123", server.uri()),
|
||||
"download_url": format!("{}/download/file_123", upload_server_uri),
|
||||
"file_id": "file_123",
|
||||
"mime_type": "text/plain",
|
||||
"file_name": "report.txt",
|
||||
|
||||
Reference in New Issue
Block a user