mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
This brings us into better alignment with the JSON schema subset that is supported in <https://developers.openai.com/api/docs/guides/structured-outputs#supported-schemas>, and also allows us to render richer function signatures in code mode (e.g., anyOf{null, OtherObjectType})
279 lines
8.6 KiB
Rust
279 lines
8.6 KiB
Rust
use super::*;
|
|
use core_test_support::assert_regex_match;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn custom_tool_calls_should_roundtrip_as_custom_outputs() {
|
|
let payload = ToolPayload::Custom {
|
|
input: "patch".to_string(),
|
|
};
|
|
let response = FunctionToolOutput::from_text("patched".to_string(), Some(true))
|
|
.to_response_item("call-42", &payload);
|
|
|
|
match response {
|
|
ResponseInputItem::CustomToolCallOutput {
|
|
call_id, output, ..
|
|
} => {
|
|
assert_eq!(call_id, "call-42");
|
|
assert_eq!(output.content_items(), None);
|
|
assert_eq!(output.body.to_text().as_deref(), Some("patched"));
|
|
assert_eq!(output.success, Some(true));
|
|
}
|
|
other => panic!("expected CustomToolCallOutput, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn function_payloads_remain_function_outputs() {
|
|
let payload = ToolPayload::Function {
|
|
arguments: "{}".to_string(),
|
|
};
|
|
let response = FunctionToolOutput::from_text("ok".to_string(), Some(true))
|
|
.to_response_item("fn-1", &payload);
|
|
|
|
match response {
|
|
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
|
assert_eq!(call_id, "fn-1");
|
|
assert_eq!(output.content_items(), None);
|
|
assert_eq!(output.body.to_text().as_deref(), Some("ok"));
|
|
assert_eq!(output.success, Some(true));
|
|
}
|
|
other => panic!("expected FunctionCallOutput, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_code_mode_result_serializes_full_call_tool_result() {
|
|
let output = CallToolResult {
|
|
content: vec![serde_json::json!({
|
|
"type": "text",
|
|
"text": "ignored",
|
|
})],
|
|
structured_content: Some(serde_json::json!({
|
|
"threadId": "thread_123",
|
|
"content": "done",
|
|
})),
|
|
is_error: Some(false),
|
|
meta: Some(serde_json::json!({
|
|
"source": "mcp",
|
|
})),
|
|
};
|
|
|
|
let result = output.code_mode_result(&ToolPayload::Mcp {
|
|
server: "server".to_string(),
|
|
tool: "tool".to_string(),
|
|
raw_arguments: "{}".to_string(),
|
|
});
|
|
|
|
assert_eq!(
|
|
result,
|
|
serde_json::json!({
|
|
"content": [{
|
|
"type": "text",
|
|
"text": "ignored",
|
|
}],
|
|
"structuredContent": {
|
|
"threadId": "thread_123",
|
|
"content": "done",
|
|
},
|
|
"isError": false,
|
|
"_meta": {
|
|
"source": "mcp",
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn custom_tool_calls_can_derive_text_from_content_items() {
|
|
let payload = ToolPayload::Custom {
|
|
input: "patch".to_string(),
|
|
};
|
|
let response = FunctionToolOutput::from_content(
|
|
vec![
|
|
FunctionCallOutputContentItem::InputText {
|
|
text: "line 1".to_string(),
|
|
},
|
|
FunctionCallOutputContentItem::InputImage {
|
|
image_url: "data:image/png;base64,AAA".to_string(),
|
|
detail: None,
|
|
},
|
|
FunctionCallOutputContentItem::InputText {
|
|
text: "line 2".to_string(),
|
|
},
|
|
],
|
|
Some(true),
|
|
)
|
|
.to_response_item("call-99", &payload);
|
|
|
|
match response {
|
|
ResponseInputItem::CustomToolCallOutput {
|
|
call_id, output, ..
|
|
} => {
|
|
let expected = vec![
|
|
FunctionCallOutputContentItem::InputText {
|
|
text: "line 1".to_string(),
|
|
},
|
|
FunctionCallOutputContentItem::InputImage {
|
|
image_url: "data:image/png;base64,AAA".to_string(),
|
|
detail: None,
|
|
},
|
|
FunctionCallOutputContentItem::InputText {
|
|
text: "line 2".to_string(),
|
|
},
|
|
];
|
|
assert_eq!(call_id, "call-99");
|
|
assert_eq!(output.content_items(), Some(expected.as_slice()));
|
|
assert_eq!(output.body.to_text().as_deref(), Some("line 1\nline 2"));
|
|
assert_eq!(output.success, Some(true));
|
|
}
|
|
other => panic!("expected CustomToolCallOutput, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tool_search_payloads_roundtrip_as_tool_search_outputs() {
|
|
let payload = ToolPayload::ToolSearch {
|
|
arguments: SearchToolCallParams {
|
|
query: "calendar".to_string(),
|
|
limit: None,
|
|
},
|
|
};
|
|
let response = ToolSearchOutput {
|
|
tools: vec![ToolSearchOutputTool::Function(
|
|
codex_tools::ResponsesApiTool {
|
|
name: "create_event".to_string(),
|
|
description: String::new(),
|
|
strict: false,
|
|
defer_loading: Some(true),
|
|
parameters: codex_tools::JsonSchema::object(
|
|
/*properties*/ Default::default(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None,
|
|
),
|
|
output_schema: None,
|
|
},
|
|
)],
|
|
}
|
|
.to_response_item("search-1", &payload);
|
|
|
|
match response {
|
|
ResponseInputItem::ToolSearchOutput {
|
|
call_id,
|
|
status,
|
|
execution,
|
|
tools,
|
|
} => {
|
|
assert_eq!(call_id, "search-1");
|
|
assert_eq!(status, "completed");
|
|
assert_eq!(execution, "client");
|
|
assert_eq!(
|
|
tools,
|
|
vec![json!({
|
|
"type": "function",
|
|
"name": "create_event",
|
|
"description": "",
|
|
"strict": false,
|
|
"defer_loading": true,
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
})]
|
|
);
|
|
}
|
|
other => panic!("expected ToolSearchOutput, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn log_preview_uses_content_items_when_plain_text_is_missing() {
|
|
let output = FunctionToolOutput::from_content(
|
|
vec![FunctionCallOutputContentItem::InputText {
|
|
text: "preview".to_string(),
|
|
}],
|
|
Some(true),
|
|
);
|
|
|
|
assert_eq!(output.log_preview(), "preview");
|
|
assert_eq!(
|
|
function_call_output_content_items_to_text(&output.body),
|
|
Some("preview".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn telemetry_preview_returns_original_within_limits() {
|
|
let content = "short output";
|
|
assert_eq!(telemetry_preview(content), content);
|
|
}
|
|
|
|
#[test]
|
|
fn telemetry_preview_truncates_by_bytes() {
|
|
let content = "x".repeat(TELEMETRY_PREVIEW_MAX_BYTES + 8);
|
|
let preview = telemetry_preview(&content);
|
|
|
|
assert!(preview.contains(TELEMETRY_PREVIEW_TRUNCATION_NOTICE));
|
|
assert!(
|
|
preview.len()
|
|
<= TELEMETRY_PREVIEW_MAX_BYTES + TELEMETRY_PREVIEW_TRUNCATION_NOTICE.len() + 1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn telemetry_preview_truncates_by_lines() {
|
|
let content = (0..(TELEMETRY_PREVIEW_MAX_LINES + 5))
|
|
.map(|idx| format!("line {idx}"))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
let preview = telemetry_preview(&content);
|
|
let lines: Vec<&str> = preview.lines().collect();
|
|
|
|
assert!(lines.len() <= TELEMETRY_PREVIEW_MAX_LINES + 1);
|
|
assert_eq!(lines.last(), Some(&TELEMETRY_PREVIEW_TRUNCATION_NOTICE));
|
|
}
|
|
|
|
#[test]
|
|
fn exec_command_tool_output_formats_truncated_response() {
|
|
let payload = ToolPayload::Function {
|
|
arguments: "{}".to_string(),
|
|
};
|
|
let response = ExecCommandToolOutput {
|
|
event_call_id: "call-42".to_string(),
|
|
chunk_id: "abc123".to_string(),
|
|
wall_time: std::time::Duration::from_millis(1250),
|
|
raw_output: b"token one token two token three token four token five".to_vec(),
|
|
max_output_tokens: Some(4),
|
|
process_id: None,
|
|
exit_code: Some(0),
|
|
original_token_count: Some(10),
|
|
session_command: None,
|
|
}
|
|
.to_response_item("call-42", &payload);
|
|
|
|
match response {
|
|
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
|
assert_eq!(call_id, "call-42");
|
|
assert_eq!(output.success, Some(true));
|
|
let text = output
|
|
.body
|
|
.to_text()
|
|
.expect("exec output should serialize as text");
|
|
assert_regex_match(
|
|
r#"(?sx)
|
|
^Chunk\ ID:\ abc123
|
|
\nWall\ time:\ \d+\.\d{4}\ seconds
|
|
\nProcess\ exited\ with\ code\ 0
|
|
\nOriginal\ token\ count:\ 10
|
|
\nOutput:
|
|
\n.*tokens\ truncated.*
|
|
$"#,
|
|
&text,
|
|
);
|
|
}
|
|
other => panic!("expected FunctionCallOutput, got {other:?}"),
|
|
}
|
|
}
|