mirror of
https://github.com/openai/codex.git
synced 2026-05-29 23:40:29 +00:00
## Why The previous extraction steps moved shared tool-schema parsing into `codex-tools`, but `codex-core` still owned the generic Responses API tool models and the last adapter layer that turned parsed tool definitions into `ResponsesApiTool` values. That left `core/src/tools/spec.rs` and `core/src/client_common.rs` holding a chunk of tool-shaping code that does not need session state, runtime plumbing, or any other `codex-core`-specific dependency. As a result, `codex-tools` owned the parsed tool definition, but `codex-core` still owned the generic wire model that those definitions are converted into. This change moves that boundary one step further. `codex-tools` now owns the reusable Responses/tool wire structs and the shared conversion helpers for dynamic tools, MCP tools, and deferred MCP aliases. `codex-core` continues to own `ToolSpec` orchestration and the remaining web-search-specific request shapes. ## What changed - added `tools/src/responses_api.rs` to own `ResponsesApiTool`, `FreeformTool`, `ToolSearchOutputTool`, namespace output types, and the shared `ToolDefinition -> ResponsesApiTool` adapter helpers - added `tools/src/responses_api_tests.rs` for deferred-loading behavior, adapter coverage, and namespace serialization coverage - rewired `core/src/tools/spec.rs` to use the extracted dynamic/MCP adapter helpers instead of defining those conversions locally - rewired `core/src/tools/handlers/tool_search.rs` to use the extracted deferred MCP adapter and namespace output types directly - slimmed `core/src/client_common.rs` so it now keeps `ToolSpec` and the web-search-specific wire types, while reusing the extracted tool models from `codex-tools` - moved the extracted seam tests out of `core` and updated `codex-rs/tools/README.md` plus `tools/src/lib.rs` to reflect the expanded `codex-tools` boundary ## Test plan - `cargo test -p codex-tools` - `cargo test -p codex-core --lib tools::spec::` - `cargo test -p codex-core --lib tools::handlers::tool_search::` - `just fix -p codex-tools -p codex-core` - `just argument-comment-lint` ## References - [#15923](https://github.com/openai/codex/pull/15923) `codex-tools: extract shared tool schema parsing` - [#15928](https://github.com/openai/codex/pull/15928) `codex-tools: extract MCP schema adapters` - [#15944](https://github.com/openai/codex/pull/15944) `codex-tools: extract dynamic tool adapters` - [#15953](https://github.com/openai/codex/pull/15953) `codex-tools: introduce named tool definitions`
200 lines
5.9 KiB
Rust
200 lines
5.9 KiB
Rust
use codex_api::ResponsesApiRequest;
|
|
use codex_api::common::OpenAiVerbosity;
|
|
use codex_api::common::TextControls;
|
|
use codex_api::create_text_param_for_request;
|
|
use codex_protocol::config_types::ServiceTier;
|
|
use codex_protocol::models::FunctionCallOutputPayload;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn serializes_text_verbosity_when_set() {
|
|
let input: Vec<ResponseItem> = vec![];
|
|
let tools: Vec<serde_json::Value> = vec![];
|
|
let req = ResponsesApiRequest {
|
|
model: "gpt-5.1".to_string(),
|
|
instructions: "i".to_string(),
|
|
input,
|
|
tools,
|
|
tool_choice: "auto".to_string(),
|
|
parallel_tool_calls: true,
|
|
reasoning: None,
|
|
store: false,
|
|
stream: true,
|
|
include: vec![],
|
|
prompt_cache_key: None,
|
|
service_tier: None,
|
|
text: Some(TextControls {
|
|
verbosity: Some(OpenAiVerbosity::Low),
|
|
format: None,
|
|
}),
|
|
};
|
|
|
|
let v = serde_json::to_value(&req).expect("json");
|
|
assert_eq!(
|
|
v.get("text")
|
|
.and_then(|t| t.get("verbosity"))
|
|
.and_then(|s| s.as_str()),
|
|
Some("low")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serializes_text_schema_with_strict_format() {
|
|
let input: Vec<ResponseItem> = vec![];
|
|
let tools: Vec<serde_json::Value> = vec![];
|
|
let schema = serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"answer": {"type": "string"}
|
|
},
|
|
"required": ["answer"],
|
|
});
|
|
let text_controls =
|
|
create_text_param_for_request(None, &Some(schema.clone())).expect("text controls");
|
|
|
|
let req = ResponsesApiRequest {
|
|
model: "gpt-5.1".to_string(),
|
|
instructions: "i".to_string(),
|
|
input,
|
|
tools,
|
|
tool_choice: "auto".to_string(),
|
|
parallel_tool_calls: true,
|
|
reasoning: None,
|
|
store: false,
|
|
stream: true,
|
|
include: vec![],
|
|
prompt_cache_key: None,
|
|
service_tier: None,
|
|
text: Some(text_controls),
|
|
};
|
|
|
|
let v = serde_json::to_value(&req).expect("json");
|
|
let text = v.get("text").expect("text field");
|
|
assert!(text.get("verbosity").is_none());
|
|
let format = text.get("format").expect("format field");
|
|
|
|
assert_eq!(
|
|
format.get("name"),
|
|
Some(&serde_json::Value::String("codex_output_schema".into()))
|
|
);
|
|
assert_eq!(
|
|
format.get("type"),
|
|
Some(&serde_json::Value::String("json_schema".into()))
|
|
);
|
|
assert_eq!(format.get("strict"), Some(&serde_json::Value::Bool(true)));
|
|
assert_eq!(format.get("schema"), Some(&schema));
|
|
}
|
|
|
|
#[test]
|
|
fn omits_text_when_not_set() {
|
|
let input: Vec<ResponseItem> = vec![];
|
|
let tools: Vec<serde_json::Value> = vec![];
|
|
let req = ResponsesApiRequest {
|
|
model: "gpt-5.1".to_string(),
|
|
instructions: "i".to_string(),
|
|
input,
|
|
tools,
|
|
tool_choice: "auto".to_string(),
|
|
parallel_tool_calls: true,
|
|
reasoning: None,
|
|
store: false,
|
|
stream: true,
|
|
include: vec![],
|
|
prompt_cache_key: None,
|
|
service_tier: None,
|
|
text: None,
|
|
};
|
|
|
|
let v = serde_json::to_value(&req).expect("json");
|
|
assert!(v.get("text").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn serializes_flex_service_tier_when_set() {
|
|
let req = ResponsesApiRequest {
|
|
model: "gpt-5.1".to_string(),
|
|
instructions: "i".to_string(),
|
|
input: vec![],
|
|
tools: vec![],
|
|
tool_choice: "auto".to_string(),
|
|
parallel_tool_calls: true,
|
|
reasoning: None,
|
|
store: false,
|
|
stream: true,
|
|
include: vec![],
|
|
prompt_cache_key: None,
|
|
service_tier: Some(ServiceTier::Flex.to_string()),
|
|
text: None,
|
|
};
|
|
|
|
let v = serde_json::to_value(&req).expect("json");
|
|
assert_eq!(
|
|
v.get("service_tier").and_then(|tier| tier.as_str()),
|
|
Some("flex")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
|
|
let raw_output = r#"{"output":"hello","metadata":{"exit_code":0,"duration_seconds":0.5}}"#;
|
|
let expected_output = "Exit code: 0\nWall time: 0.5 seconds\nOutput:\nhello";
|
|
let mut items = vec![
|
|
ResponseItem::FunctionCall {
|
|
id: None,
|
|
name: "shell".to_string(),
|
|
namespace: None,
|
|
arguments: "{}".to_string(),
|
|
call_id: "call-1".to_string(),
|
|
},
|
|
ResponseItem::FunctionCallOutput {
|
|
call_id: "call-1".to_string(),
|
|
output: FunctionCallOutputPayload::from_text(raw_output.to_string()),
|
|
},
|
|
ResponseItem::CustomToolCall {
|
|
id: None,
|
|
status: None,
|
|
call_id: "call-2".to_string(),
|
|
name: "apply_patch".to_string(),
|
|
input: "*** Begin Patch".to_string(),
|
|
},
|
|
ResponseItem::CustomToolCallOutput {
|
|
call_id: "call-2".to_string(),
|
|
name: None,
|
|
output: FunctionCallOutputPayload::from_text(raw_output.to_string()),
|
|
},
|
|
];
|
|
|
|
reserialize_shell_outputs(&mut items);
|
|
|
|
assert_eq!(
|
|
items,
|
|
vec![
|
|
ResponseItem::FunctionCall {
|
|
id: None,
|
|
name: "shell".to_string(),
|
|
namespace: None,
|
|
arguments: "{}".to_string(),
|
|
call_id: "call-1".to_string(),
|
|
},
|
|
ResponseItem::FunctionCallOutput {
|
|
call_id: "call-1".to_string(),
|
|
output: FunctionCallOutputPayload::from_text(expected_output.to_string()),
|
|
},
|
|
ResponseItem::CustomToolCall {
|
|
id: None,
|
|
status: None,
|
|
call_id: "call-2".to_string(),
|
|
name: "apply_patch".to_string(),
|
|
input: "*** Begin Patch".to_string(),
|
|
},
|
|
ResponseItem::CustomToolCallOutput {
|
|
call_id: "call-2".to_string(),
|
|
name: None,
|
|
output: FunctionCallOutputPayload::from_text(expected_output.to_string()),
|
|
},
|
|
]
|
|
);
|
|
}
|