Files
codex/codex-rs/core/src/client_common_tests.rs
T
pakrym-oai dbd2857f4b [codex] Add optional IDs to response items (#28812)
## Why

`ResponseItem` variants do not have a consistent internal ID shape: some
variants carry required IDs, some carry optional IDs, and some cannot
represent an ID at all. The existing fields also use inconsistent serde,
TypeScript, and JSON-schema annotations. A single enum-level access path
is needed before history recording can assign and retain IDs.

This PR establishes that internal model only. It intentionally does not
generate or serialize IDs; allocation and wire persistence are isolated
in the stacked follow-up.

## What changed

- Give every concrete `ResponseItem` variant an `Option<String>` ID
field.
- Apply the same internal-only annotations to every ID field:
`#[serde(default, skip_serializing)]`, `#[ts(skip)]`, and
`#[schemars(skip)]`.
- Add `ResponseItem::id()` and `ResponseItem::set_id()` as the shared
accessors.
- Preserve IDs when history items are rewritten for truncation.
- Adapt consumers that previously assumed reasoning and image-generation
IDs were required.
- Regenerate app-server schemas so the hidden fields are represented
consistently.

The serde catch-all `ResponseItem::Other` remains ID-less because it
must remain a unit variant.

## Test plan

- `cargo check --tests -p codex-core -p codex-api -p codex-rollout-trace
-p codex-image-generation-extension`
- `just test -p codex-protocol`
- `just test -p codex-app-server-protocol`
- `just test -p codex-api -p codex-rollout-trace -p
codex-image-generation-extension`
- `just test -p codex-core event_mapping`
2026-06-17 18:27:43 -07:00

263 lines
7.9 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,
metadata: 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),
},
]),
metadata: 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),
},
]),
metadata: 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,
metadata: 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,
},
]),
metadata: 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,
},
]),
metadata: 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,
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,
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,
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: 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")
);
}