Keep guardian output schema strict

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Dylan Hurd
2026-04-20 20:11:27 -07:00
parent 8db0716c52
commit a04b390dbf
11 changed files with 52 additions and 91 deletions

View File

@@ -262,7 +262,6 @@ pub enum ResponsesWsRequest {
pub fn create_text_param_for_request(
verbosity: Option<VerbosityConfig>,
output_schema: &Option<Value>,
output_schema_strict: bool,
) -> Option<TextControls> {
if verbosity.is_none() && output_schema.is_none() {
return None;
@@ -272,7 +271,7 @@ pub fn create_text_param_for_request(
verbosity: verbosity.map(std::convert::Into::into),
format: output_schema.as_ref().map(|schema| TextFormat {
r#type: TextFormatType::JsonSchema,
strict: output_schema_strict,
strict: true,
schema: schema.clone(),
name: "codex_output_schema".to_string(),
}),

View File

@@ -445,11 +445,7 @@ impl ModelClient {
}
None
};
let text = create_text_param_for_request(
verbosity,
&prompt.output_schema,
prompt.output_schema_strict,
);
let text = create_text_param_for_request(verbosity, &prompt.output_schema);
let payload = ApiCompactionInput {
model: &model_info.slug,
input: &input,
@@ -853,11 +849,7 @@ impl ModelClientSession {
}
None
};
let text = create_text_param_for_request(
verbosity,
&prompt.output_schema,
prompt.output_schema_strict,
);
let text = create_text_param_for_request(verbosity, &prompt.output_schema);
let prompt_cache_key = Some(self.client.state.conversation_id.to_string());
let request = ResponsesApiRequest {
model: model_info.slug.clone(),

View File

@@ -23,7 +23,7 @@ pub const REVIEW_EXIT_INTERRUPTED_TMPL: &str =
include_str!("../templates/review/exit_interrupted.xml");
/// API request payload for a single model turn
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct Prompt {
/// Conversation context input items.
pub input: Vec<ResponseItem>,
@@ -42,23 +42,6 @@ pub struct Prompt {
/// Optional the output schema for the model's response.
pub output_schema: Option<Value>,
/// Whether the Responses API should strictly validate `output_schema`.
pub output_schema_strict: bool,
}
impl Default for Prompt {
fn default() -> Self {
Self {
input: Vec::new(),
tools: Vec::new(),
parallel_tool_calls: false,
base_instructions: BaseInstructions::default(),
personality: None,
output_schema: None,
output_schema_strict: true,
}
}
}
impl Prompt {

View File

@@ -52,12 +52,9 @@ fn serializes_text_schema_with_strict_format() {
},
"required": ["answer"],
});
let text_controls = create_text_param_for_request(
/*verbosity*/ None,
&Some(schema.clone()),
/*output_schema_strict*/ true,
)
.expect("text controls");
let text_controls =
create_text_param_for_request(/*verbosity*/ None, &Some(schema.clone()))
.expect("text controls");
let req = ResponsesApiRequest {
model: "gpt-5.1".to_string(),
@@ -93,29 +90,6 @@ fn serializes_text_schema_with_strict_format() {
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![];

View File

@@ -156,7 +156,6 @@ async fn run_remote_compact_task_inner_impl(
base_instructions,
personality: turn_context.personality,
output_schema: None,
output_schema_strict: true,
};
let mut new_history = sess

View File

@@ -551,22 +551,22 @@ pub(crate) fn guardian_output_schema() -> Value {
"additionalProperties": false,
"properties": {
"risk_level": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
"type": ["string", "null"],
"enum": ["low", "medium", "high", "critical", null]
},
"user_authorization": {
"type": "string",
"enum": ["unknown", "low", "medium", "high"]
"type": ["string", "null"],
"enum": ["unknown", "low", "medium", "high", null]
},
"outcome": {
"type": "string",
"enum": ["allow", "deny"]
},
"rationale": {
"type": "string"
"type": ["string", "null"]
}
},
"required": ["outcome"]
"required": ["risk_level", "user_authorization", "outcome", "rationale"]
})
}
@@ -575,14 +575,14 @@ pub(crate) fn guardian_output_schema() -> Value {
fn guardian_output_contract_prompt() -> &'static str {
r#"You may use read-only tool checks to gather any additional context you need before deciding. When you are ready to answer, your final message must be strict JSON.
For low-risk actions, give the final answer directly: {"outcome":"allow"}.
For low-risk actions, use null for optional details: {"risk_level":null,"user_authorization":null,"outcome":"allow","rationale":null}.
For anything else, use this JSON schema:
{
"risk_level": "low" | "medium" | "high" | "critical",
"user_authorization": "unknown" | "low" | "medium" | "high",
"risk_level": "low" | "medium" | "high" | "critical" | null,
"user_authorization": "unknown" | "low" | "medium" | "high" | null,
"outcome": "allow" | "deny",
"rationale": string
"rationale": string | null
}"#
}

View File

@@ -876,7 +876,25 @@ fn parse_guardian_assessment_treats_bare_allow_as_low_risk() {
}
#[test]
fn guardian_output_schema_requires_only_outcome_and_allows_optional_details() {
fn parse_guardian_assessment_treats_nullable_allow_as_low_risk() {
let parsed = parse_guardian_assessment(Some(
r#"{"risk_level":null,"user_authorization":null,"outcome":"allow","rationale":null}"#,
))
.expect("guardian assessment");
assert_eq!(
parsed,
GuardianAssessment {
risk_level: GuardianRiskLevel::Low,
user_authorization: GuardianUserAuthorization::Unknown,
outcome: GuardianAssessmentOutcome::Allow,
rationale: "Guardian returned a low-risk allow decision.".to_string(),
}
);
}
#[test]
fn guardian_output_schema_uses_strict_nullable_details() {
let schema = guardian_output_schema();
assert_eq!(
@@ -886,22 +904,22 @@ fn guardian_output_schema_requires_only_outcome_and_allows_optional_details() {
"additionalProperties": false,
"properties": {
"risk_level": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
"type": ["string", "null"],
"enum": ["low", "medium", "high", "critical", null]
},
"user_authorization": {
"type": "string",
"enum": ["unknown", "low", "medium", "high"]
"type": ["string", "null"],
"enum": ["unknown", "low", "medium", "high", null]
},
"outcome": {
"type": "string",
"enum": ["allow", "deny"]
},
"rationale": {
"type": "string"
"type": ["string", "null"]
}
},
"required": ["outcome"]
"required": ["risk_level", "user_authorization", "outcome", "rationale"]
})
);
}
@@ -980,7 +998,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
let request_body = request.body_json();
assert_eq!(
request_body.pointer("/text/format/strict"),
Some(&serde_json::json!(false))
Some(&serde_json::json!(true))
);
assert_eq!(
request_body.pointer("/text/format/schema"),
@@ -989,22 +1007,22 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
"additionalProperties": false,
"properties": {
"risk_level": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
"type": ["string", "null"],
"enum": ["low", "medium", "high", "critical", null]
},
"user_authorization": {
"type": "string",
"enum": ["unknown", "low", "medium", "high"]
"type": ["string", "null"],
"enum": ["unknown", "low", "medium", "high", null]
},
"outcome": {
"type": "string",
"enum": ["allow", "deny"]
},
"rationale": {
"type": "string"
"type": ["string", "null"]
}
},
"required": ["outcome"]
"required": ["risk_level", "user_authorization", "outcome", "rationale"]
}))
);
let mut settings = Settings::clone_current();

View File

@@ -341,7 +341,6 @@ mod job {
},
personality: None,
output_schema: Some(output_schema()),
output_schema_strict: true,
};
let mut client_session = session.services.model_client.new_session();

View File

@@ -971,9 +971,6 @@ pub(crate) fn build_prompt(
base_instructions,
personality: turn_context.personality,
output_schema: turn_context.final_output_json_schema.clone(),
output_schema_strict: !crate::guardian::is_guardian_reviewer_source(
&turn_context.session_source,
),
}
}