mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
33cc928d33
When using Responses Lite, we should all use `additional_tools` and a developer item instead of the top level tools array & instructions field. This keeps things 1-to-1. Forced namespacing for _all_ tools will land in a following PR after some coordination & fixes in Responses API (around collisions & return items). The goal is to eventually expand the scope of this to _all_ requests from codex, but that will require larger coordination across providers & slower rollout.
263 lines
8.2 KiB
Rust
263 lines
8.2 KiB
Rust
use codex_api::OpenAiVerbosity;
|
|
use codex_api::ResponsesApiRequest;
|
|
use codex_api::TextControls;
|
|
use codex_api::create_text_param_for_request;
|
|
use codex_protocol::config_types::ServiceTier;
|
|
use codex_protocol::models::FunctionCallOutputPayload;
|
|
use codex_protocol::models::ImageDetail;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
fn prompt_with_image_outputs() -> Prompt {
|
|
Prompt {
|
|
input: vec![
|
|
ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputImage {
|
|
image_url: "https://example.com/image.png".to_string(),
|
|
detail: Some(ImageDetail::Original),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
},
|
|
ResponseItem::FunctionCallOutput {
|
|
id: None,
|
|
call_id: "function-call".to_string(),
|
|
output: FunctionCallOutputPayload::from_content_items(vec![
|
|
FunctionCallOutputContentItem::InputImage {
|
|
image_url: "data:image/png;base64,function".to_string(),
|
|
detail: Some(ImageDetail::High),
|
|
},
|
|
]),
|
|
internal_chat_message_metadata_passthrough: None,
|
|
},
|
|
ResponseItem::CustomToolCallOutput {
|
|
id: None,
|
|
call_id: "custom-call".to_string(),
|
|
name: None,
|
|
output: FunctionCallOutputPayload::from_content_items(vec![
|
|
FunctionCallOutputContentItem::InputImage {
|
|
image_url: "data:image/png;base64,custom".to_string(),
|
|
detail: Some(ImageDetail::Auto),
|
|
},
|
|
]),
|
|
internal_chat_message_metadata_passthrough: None,
|
|
},
|
|
],
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn responses_lite_request_copies_strip_image_details() {
|
|
let prompt = prompt_with_image_outputs();
|
|
let original = prompt.input.clone();
|
|
|
|
let stripped = prompt.get_formatted_input_for_request(/*use_responses_lite*/ true);
|
|
|
|
assert_eq!(
|
|
stripped,
|
|
vec![
|
|
ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputImage {
|
|
image_url: "https://example.com/image.png".to_string(),
|
|
detail: None,
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
},
|
|
ResponseItem::FunctionCallOutput {
|
|
id: None,
|
|
call_id: "function-call".to_string(),
|
|
output: FunctionCallOutputPayload::from_content_items(vec![
|
|
FunctionCallOutputContentItem::InputImage {
|
|
image_url: "data:image/png;base64,function".to_string(),
|
|
detail: None,
|
|
},
|
|
]),
|
|
internal_chat_message_metadata_passthrough: None,
|
|
},
|
|
ResponseItem::CustomToolCallOutput {
|
|
id: None,
|
|
call_id: "custom-call".to_string(),
|
|
name: None,
|
|
output: FunctionCallOutputPayload::from_content_items(vec![
|
|
FunctionCallOutputContentItem::InputImage {
|
|
image_url: "data:image/png;base64,custom".to_string(),
|
|
detail: None,
|
|
},
|
|
]),
|
|
internal_chat_message_metadata_passthrough: None,
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(prompt.input, original);
|
|
assert_eq!(
|
|
prompt.get_formatted_input_for_request(/*use_responses_lite*/ false),
|
|
original
|
|
);
|
|
}
|
|
|
|
#[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.4".to_string(),
|
|
instructions: "i".to_string(),
|
|
input,
|
|
tools: Some(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,
|
|
}),
|
|
client_metadata: 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(
|
|
/*verbosity*/ None,
|
|
&Some(schema.clone()),
|
|
/*output_schema_strict*/ true,
|
|
)
|
|
.expect("text controls");
|
|
|
|
let req = ResponsesApiRequest {
|
|
model: "gpt-5.4".to_string(),
|
|
instructions: "i".to_string(),
|
|
input,
|
|
tools: Some(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),
|
|
client_metadata: None,
|
|
};
|
|
|
|
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 serializes_text_schema_with_non_strict_format() {
|
|
let schema = serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"answer": {"type": "string"},
|
|
"rationale": {"type": "string"}
|
|
},
|
|
"required": ["answer"],
|
|
"additionalProperties": false
|
|
});
|
|
let text_controls = create_text_param_for_request(
|
|
/*verbosity*/ None,
|
|
&Some(schema.clone()),
|
|
/*output_schema_strict*/ false,
|
|
)
|
|
.expect("text controls");
|
|
|
|
let format = text_controls.format.expect("format field");
|
|
assert!(!format.strict);
|
|
assert_eq!(format.schema, 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.4".to_string(),
|
|
instructions: "i".to_string(),
|
|
input,
|
|
tools: Some(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,
|
|
client_metadata: 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.4".to_string(),
|
|
instructions: "i".to_string(),
|
|
input: vec![],
|
|
tools: Some(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,
|
|
client_metadata: None,
|
|
};
|
|
|
|
let v = serde_json::to_value(&req).expect("json");
|
|
assert_eq!(
|
|
v.get("service_tier").and_then(|tier| tier.as_str()),
|
|
Some("flex")
|
|
);
|
|
}
|