mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
refactor
This commit is contained in:
@@ -14,7 +14,6 @@ use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand;
|
||||
@@ -2692,9 +2691,10 @@ pub enum DynamicToolCallResult {
|
||||
|
||||
/// App-server-facing dynamic tool output items.
|
||||
///
|
||||
/// This mirrors `FunctionCallOutputContentItem` today, but is intentionally
|
||||
/// defined separately so the app-server API can evolve without forcing matching
|
||||
/// protocol changes at the same time.
|
||||
/// This is intentionally defined separately from
|
||||
/// `codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem` and
|
||||
/// `codex_protocol::models::FunctionCallOutputContentItem` so the app-server API
|
||||
/// can evolve independently.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
@@ -2710,28 +2710,6 @@ pub enum DynamicToolCallOutputContentItem {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<DynamicToolCallOutputContentItem> for FunctionCallOutputContentItem {
|
||||
fn from(item: DynamicToolCallOutputContentItem) -> Self {
|
||||
match item {
|
||||
DynamicToolCallOutputContentItem::InputText { text } => Self::InputText { text },
|
||||
DynamicToolCallOutputContentItem::InputImage { image_url } => {
|
||||
Self::InputImage { image_url }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FunctionCallOutputContentItem> for DynamicToolCallOutputContentItem {
|
||||
fn from(item: FunctionCallOutputContentItem) -> Self {
|
||||
match item {
|
||||
FunctionCallOutputContentItem::InputText { text } => Self::InputText { text },
|
||||
FunctionCallOutputContentItem::InputImage { image_url } => {
|
||||
Self::InputImage { image_url }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -2917,7 +2895,6 @@ mod tests {
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::items::WebSearchItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
|
||||
use codex_protocol::user_input::UserInput as CoreUserInput;
|
||||
@@ -3119,21 +3096,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_tool_content_item_maps_to_function_call_output_content_item() {
|
||||
let item = DynamicToolCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,AAA".to_string(),
|
||||
};
|
||||
|
||||
let converted: FunctionCallOutputContentItem = item.into();
|
||||
assert_eq!(
|
||||
converted,
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,AAA".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_tool_content_item_accepts_legacy_snake_case_payloads() {
|
||||
let item = serde_json::from_value::<DynamicToolCallOutputContentItem>(json!({
|
||||
|
||||
@@ -89,6 +89,7 @@ use codex_core::review_format::format_review_findings_block;
|
||||
use codex_core::review_prompts;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResult as CoreDynamicToolResult;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer;
|
||||
@@ -352,9 +353,10 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
id: call_id.clone(),
|
||||
response: CoreDynamicToolResponse {
|
||||
call_id,
|
||||
output: Some("dynamic tool calls require api v2".to_string()),
|
||||
result: CoreDynamicToolResult::Output {
|
||||
output: "dynamic tool calls require api v2".to_string(),
|
||||
},
|
||||
success: false,
|
||||
content_items: None,
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use codex_app_server_protocol::DynamicToolCallResponse;
|
||||
use codex_app_server_protocol::DynamicToolCallResult;
|
||||
use codex_core::CodexThread;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResult as CoreDynamicToolResult;
|
||||
use codex_protocol::protocol::Op;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -19,9 +21,10 @@ pub(crate) async fn on_call_response(
|
||||
error!("request failed: {err:?}");
|
||||
let fallback = CoreDynamicToolResponse {
|
||||
call_id: call_id.clone(),
|
||||
output: Some("dynamic tool request failed".to_string()),
|
||||
result: CoreDynamicToolResult::Output {
|
||||
output: "dynamic tool request failed".to_string(),
|
||||
},
|
||||
success: false,
|
||||
content_items: None,
|
||||
};
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::DynamicToolResponse {
|
||||
@@ -46,18 +49,18 @@ pub(crate) async fn on_call_response(
|
||||
}
|
||||
});
|
||||
|
||||
let (output, content_items) = match response.result {
|
||||
DynamicToolCallResult::ContentItems { content_items } => (
|
||||
None,
|
||||
Some(content_items.into_iter().map(Into::into).collect()),
|
||||
),
|
||||
DynamicToolCallResult::Output { output } => (Some(output), None),
|
||||
let result = match response.result {
|
||||
DynamicToolCallResult::ContentItems { content_items } => {
|
||||
CoreDynamicToolResult::ContentItems {
|
||||
content_items: content_items.into_iter().map(map_content_item).collect(),
|
||||
}
|
||||
}
|
||||
DynamicToolCallResult::Output { output } => CoreDynamicToolResult::Output { output },
|
||||
};
|
||||
let response = CoreDynamicToolResponse {
|
||||
call_id: call_id.clone(),
|
||||
output,
|
||||
result,
|
||||
success: response.success,
|
||||
content_items,
|
||||
};
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::DynamicToolResponse {
|
||||
@@ -69,3 +72,16 @@ pub(crate) async fn on_call_response(
|
||||
error!("failed to submit DynamicToolResponse: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn map_content_item(
|
||||
item: codex_app_server_protocol::DynamicToolCallOutputContentItem,
|
||||
) -> CoreDynamicToolCallOutputContentItem {
|
||||
match item {
|
||||
codex_app_server_protocol::DynamicToolCallOutputContentItem::InputText { text } => {
|
||||
CoreDynamicToolCallOutputContentItem::InputText { text }
|
||||
}
|
||||
codex_app_server_protocol::DynamicToolCallOutputContentItem::InputImage { image_url } => {
|
||||
CoreDynamicToolCallOutputContentItem::InputImage { image_url }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +330,14 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
let content_items = response_content_items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.map(|item| match item {
|
||||
DynamicToolCallOutputContentItem::InputText { text } => {
|
||||
FunctionCallOutputContentItem::InputText { text }
|
||||
}
|
||||
DynamicToolCallOutputContentItem::InputImage { image_url } => {
|
||||
FunctionCallOutputContentItem::InputImage { image_url }
|
||||
}
|
||||
})
|
||||
.collect::<Vec<FunctionCallOutputContentItem>>();
|
||||
let response = DynamicToolCallResponse {
|
||||
result: DynamicToolCallResult::ContentItems {
|
||||
|
||||
@@ -8,8 +8,10 @@ use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResult;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use serde_json::Value;
|
||||
@@ -57,15 +59,18 @@ impl ToolHandler for DynamicToolHandler {
|
||||
})?;
|
||||
|
||||
let DynamicToolResponse {
|
||||
output,
|
||||
content_items,
|
||||
success,
|
||||
..
|
||||
result, success, ..
|
||||
} = response;
|
||||
let output = normalize_output(output, content_items.as_deref());
|
||||
let (content, content_items) = match result {
|
||||
DynamicToolResult::Output { output } => (output, None),
|
||||
DynamicToolResult::ContentItems { content_items } => (
|
||||
content_items_to_text(Some(&content_items)).unwrap_or_default(),
|
||||
Some(content_items.into_iter().map(map_content_item).collect()),
|
||||
),
|
||||
};
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
content: output,
|
||||
content,
|
||||
content_items,
|
||||
success: Some(success),
|
||||
})
|
||||
@@ -106,24 +111,13 @@ async fn request_dynamic_tool(
|
||||
rx_response.await.ok()
|
||||
}
|
||||
|
||||
fn normalize_output(
|
||||
output: Option<String>,
|
||||
content_items: Option<&[FunctionCallOutputContentItem]>,
|
||||
) -> String {
|
||||
if let Some(output) = output {
|
||||
return output;
|
||||
}
|
||||
|
||||
content_items_to_text(content_items).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn content_items_to_text(
|
||||
content_items: Option<&[FunctionCallOutputContentItem]>,
|
||||
content_items: Option<&[DynamicToolCallOutputContentItem]>,
|
||||
) -> Option<String> {
|
||||
let mut text = Vec::new();
|
||||
|
||||
for item in content_items.unwrap_or_default() {
|
||||
if let FunctionCallOutputContentItem::InputText { text: segment } = item
|
||||
if let DynamicToolCallOutputContentItem::InputText { text: segment } = item
|
||||
&& !segment.trim().is_empty()
|
||||
{
|
||||
text.push(segment.as_str());
|
||||
@@ -137,51 +131,52 @@ fn content_items_to_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_content_item(item: DynamicToolCallOutputContentItem) -> FunctionCallOutputContentItem {
|
||||
match item {
|
||||
DynamicToolCallOutputContentItem::InputText { text } => {
|
||||
FunctionCallOutputContentItem::InputText { text }
|
||||
}
|
||||
DynamicToolCallOutputContentItem::InputImage { image_url } => {
|
||||
FunctionCallOutputContentItem::InputImage { image_url }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn normalize_output_prefers_explicit_output() {
|
||||
let content_items = vec![FunctionCallOutputContentItem::InputText {
|
||||
text: "fallback".to_string(),
|
||||
}];
|
||||
|
||||
let output = normalize_output(Some("explicit".to_string()), Some(&content_items));
|
||||
assert_eq!(output, "explicit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_output_uses_text_content_items() {
|
||||
fn content_items_to_text_uses_text_content_items() {
|
||||
let content_items = vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
DynamicToolCallOutputContentItem::InputText {
|
||||
text: "line 1".to_string(),
|
||||
},
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
DynamicToolCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,AAA".to_string(),
|
||||
},
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
DynamicToolCallOutputContentItem::InputText {
|
||||
text: "line 2".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let output = normalize_output(None, Some(&content_items));
|
||||
let output = content_items_to_text(Some(&content_items)).unwrap_or_default();
|
||||
assert_eq!(output, "line 1\nline 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_output_ignores_empty_and_image_only_content_items() {
|
||||
fn content_items_to_text_ignores_empty_and_image_only_content_items() {
|
||||
let content_items = vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
DynamicToolCallOutputContentItem::InputText {
|
||||
text: " ".to_string(),
|
||||
},
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
DynamicToolCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,AAA".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let output = normalize_output(None, Some(&content_items));
|
||||
assert_eq!(output, "");
|
||||
let output = content_items_to_text(Some(&content_items));
|
||||
assert_eq!(output, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::models::FunctionCallOutputContentItem;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamicToolSpec {
|
||||
@@ -27,9 +25,33 @@ pub struct DynamicToolCallRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamicToolResponse {
|
||||
pub call_id: String,
|
||||
pub output: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub result: DynamicToolResult,
|
||||
pub success: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub content_items: Option<Vec<FunctionCallOutputContentItem>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(untagged, rename_all = "camelCase")]
|
||||
pub enum DynamicToolResult {
|
||||
ContentItems {
|
||||
#[serde(rename = "contentItems")]
|
||||
content_items: Vec<DynamicToolCallOutputContentItem>,
|
||||
},
|
||||
Output {
|
||||
output: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum DynamicToolCallOutputContentItem {
|
||||
#[serde(alias = "input_text")]
|
||||
InputText { text: String },
|
||||
#[serde(alias = "input_image", rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
InputImage {
|
||||
#[serde(alias = "image_url")]
|
||||
image_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user