Attach hosted app context to file uploads (#38101)

## What changed

- Include the connector ID, action name, and model in file creation requests for hosted app tool calls.
- Use the file size returned by upload finalization when available, while falling back to the local size for older server responses.
- Keep upload finalization requests empty for compatibility with older servers.

## Testing

- Cover hosted app upload metadata and legacy finalization responses.

GitOrigin-RevId: 1bc834736db4fe3515f8224965cde0e5d6955ca3
This commit is contained in:
wencongli-oai
2026-08-12 01:38:44 +00:00
committed by copyberry
parent ca4d532b2a
commit c909d1bc04
5 changed files with 149 additions and 15 deletions

View File

@@ -22,6 +22,13 @@ const OPENAI_FILE_FINALIZE_TIMEOUT: Duration = Duration::from_secs(30);
const OPENAI_FILE_FINALIZE_RETRY_DELAY: Duration = Duration::from_millis(250);
const OPENAI_FILE_USE_CASE: &str = "codex";
#[derive(Debug)]
pub struct HostedFileUploadContext {
pub connector_id: String,
pub action_name: String,
pub model: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UploadedOpenAiFile {
pub file_id: String,
@@ -101,6 +108,8 @@ struct DownloadLinkResponse {
file_name: Option<String>,
mime_type: Option<String>,
error_message: Option<String>,
#[serde(default)]
file_size_bytes: Option<u64>,
}
pub fn openai_file_uri(file_id: &str) -> String {
@@ -114,6 +123,7 @@ pub async fn upload_openai_file(
file_name: String,
file_size_bytes: u64,
contents: impl Stream<Item = std::io::Result<Bytes>> + Send + 'static,
hosted_upload: Option<&HostedFileUploadContext>,
) -> Result<UploadedOpenAiFile, OpenAiFileError> {
if file_size_bytes > OPENAI_FILE_UPLOAD_LIMIT_BYTES {
return Err(OpenAiFileError::FileTooLarge {
@@ -124,12 +134,25 @@ pub async fn upload_openai_file(
}
let create_url = format!("{}/files", base_url.trim_end_matches('/'));
let create_response = authorized_request(client_pool, auth, Method::POST, &create_url)
.json(&serde_json::json!({
let create_request = serde_json::json!({
"file_name": file_name.as_str(),
"file_size": file_size_bytes,
"use_case": OPENAI_FILE_USE_CASE,
});
let request = authorized_request(client_pool, auth, Method::POST, &create_url);
let create_request = match hosted_upload {
Some(context) => serde_json::json!({
"file_name": file_name.as_str(),
"file_size": file_size_bytes,
"use_case": OPENAI_FILE_USE_CASE,
}))
"codex_connector_id": context.connector_id,
"codex_action_name": context.action_name,
"codex_model": context.model,
}),
None => create_request,
};
let create_response = request
.json(&create_request)
.send()
.await
.map_err(|source| OpenAiFileError::Request {
@@ -233,10 +256,11 @@ pub async fn upload_openai_file(
base_url.trim_end_matches('/'),
create_payload.file_id,
);
let finalize_request = serde_json::json!({});
let finalize_started_at = Instant::now();
loop {
let finalize_response = authorized_request(client_pool, auth, Method::POST, &finalize_url)
.json(&serde_json::json!({}))
.json(&finalize_request)
.send()
.await
.map_err(|source| OpenAiFileError::Request {
@@ -260,6 +284,7 @@ pub async fn upload_openai_file(
match finalize_payload.status.as_str() {
"success" => {
let file_size_bytes = finalize_payload.file_size_bytes.unwrap_or(file_size_bytes);
return Ok(UploadedOpenAiFile {
file_id: create_payload.file_id.clone(),
uri: openai_file_uri(&create_payload.file_id),
@@ -425,6 +450,7 @@ mod tests {
"hello.txt".to_string(),
/*file_size_bytes*/ 5,
contents,
/*hosted_upload*/ None,
)
.await
.expect("upload succeeds");
@@ -440,6 +466,58 @@ mod tests {
assert_eq!(finalize_attempts.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn upload_hosted_app_preserves_empty_finalization_for_older_servers() {
let server = MockServer::start().await;
let hosted_upload = HostedFileUploadContext {
connector_id: "library".to_string(),
action_name: "create_library_file".to_string(),
model: "gpt-work".to_string(),
};
Mock::given(method("POST"))
.and(path("/backend-api/files"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"file_id": "file_pdf",
"upload_url": format!("{}/upload/file_pdf", server.uri()),
})))
.expect(1)
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/upload/file_pdf"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/backend-api/files/file_pdf/uploaded"))
.and(body_json(serde_json::json!({})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "success",
"download_url": format!("{}/download/file_pdf", server.uri()),
"file_name": "report.pdf",
"mime_type": "application/pdf",
})))
.expect(1)
.mount(&server)
.await;
let uploaded = upload_openai_file(
&base_url_for(&server),
&chatgpt_auth(),
&default_http_client_pool(),
"report.pdf".to_string(),
/*file_size_bytes*/ 8,
futures::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(b"%PDF-1.4"))]),
Some(&hosted_upload),
)
.await
.expect("older servers retain the normal upload behavior");
assert_eq!(uploaded.file_size_bytes, 8);
server.verify().await;
}
#[tokio::test]
async fn upload_openai_file_reuses_client_pool_across_uploads() {
let server = MockServer::start().await;
@@ -497,6 +575,7 @@ mod tests {
file_name.to_string(),
u64::try_from(contents.len()).expect("file size should fit in a u64"),
contents_stream,
/*hosted_upload*/ None,
)
.await
.expect("upload succeeds with the shared client pool");
@@ -556,6 +635,7 @@ mod tests {
"hello.txt".to_string(),
/*file_size_bytes*/ 5,
futures::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(b"hello"))]),
/*hosted_upload*/ None,
)
.await
.expect_err("blob response failure should be returned");
@@ -593,6 +673,7 @@ mod tests {
"hello.txt".to_string(),
/*file_size_bytes*/ 5,
futures::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(b"hello"))]),
/*hosted_upload*/ None,
)
.await
.expect_err("blob transport failure should be returned");

View File

@@ -71,6 +71,7 @@ pub use crate::endpoint::ResponsesWebsocketProbe;
pub use crate::endpoint::SearchClient;
pub use crate::endpoint::session_update_session_json;
pub use crate::error::ApiError;
pub use crate::files::HostedFileUploadContext;
pub use crate::files::OPENAI_FILE_UPLOAD_LIMIT_BYTES;
pub use crate::files::upload_openai_file;
pub use crate::images::ImageBackground;