mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
## What changed - Add `inputAudio` content items to dynamic tool responses, app-server events, thread history, and generated protocol schemas. - Add an `audio()` code-mode helper that accepts inline data URLs, audio URL objects, and MCP audio blocks. - Convert MCP audio blocks into model input when audio is supported, and replace unsupported audio with an explanatory text item. - Reject non-data audio URLs and track audio item counts in dynamic tool analytics. ## Testing - Cover audio serialization, protocol round trips, thread-history conversion, MCP modality filtering, code-mode helper inputs, and invalid URL handling. GitOrigin-RevId: 1ed52a8f9c62d4840fb71c5ec736b4a3566243d6
112 lines
3.8 KiB
Rust
112 lines
3.8 KiB
Rust
use codex_app_server_protocol::DynamicToolCallOutputContentItem;
|
|
use codex_app_server_protocol::DynamicToolCallResponse;
|
|
use codex_core::CodexThread;
|
|
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
|
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
|
use codex_protocol::protocol::Op;
|
|
use std::sync::Arc;
|
|
use tokio::sync::oneshot;
|
|
use tracing::error;
|
|
|
|
use crate::image_url::REMOTE_IMAGE_URL_ERROR;
|
|
use crate::image_url::is_remote_image_url;
|
|
use crate::outgoing_message::ClientRequestResult;
|
|
use crate::server_request_error::is_turn_transition_server_request_error;
|
|
|
|
const INVALID_AUDIO_URL_ERROR: &str = "audio URLs must use an inline data URL";
|
|
|
|
pub(crate) async fn on_call_response(
|
|
call_id: String,
|
|
receiver: oneshot::Receiver<ClientRequestResult>,
|
|
conversation: Arc<CodexThread>,
|
|
) {
|
|
let response = receiver.await;
|
|
let (response, _error) = match response {
|
|
Ok(Ok(value)) => decode_response(value),
|
|
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
|
Ok(Err(err)) => {
|
|
error!("request failed with client error: {err:?}");
|
|
fallback_response("dynamic tool request failed")
|
|
}
|
|
Err(err) => {
|
|
error!("request failed: {err:?}");
|
|
fallback_response("dynamic tool request failed")
|
|
}
|
|
};
|
|
|
|
let DynamicToolCallResponse {
|
|
content_items,
|
|
success,
|
|
} = response.clone();
|
|
let core_response = CoreDynamicToolResponse {
|
|
content_items: content_items
|
|
.into_iter()
|
|
.map(CoreDynamicToolCallOutputContentItem::from)
|
|
.collect(),
|
|
success,
|
|
};
|
|
if let Err(err) = conversation
|
|
.submit(Op::DynamicToolResponse {
|
|
id: call_id.clone(),
|
|
response: core_response,
|
|
})
|
|
.await
|
|
{
|
|
error!("failed to submit DynamicToolResponse: {err}");
|
|
}
|
|
}
|
|
|
|
fn decode_response(value: serde_json::Value) -> (DynamicToolCallResponse, Option<String>) {
|
|
match serde_json::from_value::<DynamicToolCallResponse>(value) {
|
|
Ok(response)
|
|
if response.content_items.iter().any(|item| {
|
|
matches!(
|
|
item,
|
|
DynamicToolCallOutputContentItem::InputImage { image_url }
|
|
if is_remote_image_url(image_url)
|
|
)
|
|
}) =>
|
|
{
|
|
error!(
|
|
message = REMOTE_IMAGE_URL_ERROR,
|
|
"dynamic tool response was invalid"
|
|
);
|
|
fallback_response(REMOTE_IMAGE_URL_ERROR)
|
|
}
|
|
Ok(response)
|
|
if response.content_items.iter().any(|item| {
|
|
matches!(
|
|
item,
|
|
DynamicToolCallOutputContentItem::InputAudio { audio_url }
|
|
if !audio_url
|
|
.get(.."data:".len())
|
|
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("data:"))
|
|
)
|
|
}) =>
|
|
{
|
|
error!(
|
|
message = INVALID_AUDIO_URL_ERROR,
|
|
"dynamic tool response was invalid"
|
|
);
|
|
fallback_response(INVALID_AUDIO_URL_ERROR)
|
|
}
|
|
Ok(response) => (response, None),
|
|
Err(err) => {
|
|
error!("failed to deserialize DynamicToolCallResponse: {err}");
|
|
fallback_response("dynamic tool response was invalid")
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fallback_response(message: &str) -> (DynamicToolCallResponse, Option<String>) {
|
|
(
|
|
DynamicToolCallResponse {
|
|
content_items: vec![DynamicToolCallOutputContentItem::InputText {
|
|
text: message.to_string(),
|
|
}],
|
|
success: false,
|
|
},
|
|
Some(message.to_string()),
|
|
)
|
|
}
|