Bug
Graph entity extraction fails with 400 Bad Request when using OpenAI providers (gpt-4o-mini, gpt-4o).
Root Cause
GraphExtractor::extract() calls chat_typed_erased::<ExtractionResult> which uses cached_schema::<T>() to generate a JSON Schema via schemars::schema_for!(). The generated schema is sent as response_format: {type: "json_schema", strict: true, ...} to OpenAI.
OpenAI's structured output requirements for strict: true mode mandate:
additionalProperties: false on all object schemas — schemars does not add this
- All fields must appear in
required — optional fields (summary, temporal_hint: Option<String>) are generated as non-required by schemars
When these conditions are violated, OpenAI returns 400 Bad Request.
Reproduction
Run graph test with extract_model = "gpt-4o-mini" via ACP. Both conversation turns trigger extraction; all extraction calls fail with:
WARN zeph_memory::semantic: graph extraction failed: LLM error: OpenAI API request failed (status 400 Bad Request)
Graph works in CLI mode with Claude Haiku (Claude handles $defs/optional differently).
Fix
cached_schema::<T>() must post-process the generated schema before sending to OpenAI:
- Recursively add
"additionalProperties": false to all type: "object" schemas
- Move all properties to
required, and for Option<T> fields use anyOf: [{...T schema}, {"type": "null"}]
The Gemini provider already handles schema normalization via normalize_schema() in gemini.rs (PR #1635). A similar transform is needed for OpenAI's structured output path.
Alternatively: use response_format: {type: "json_object"} (no-strict mode) with explicit schema in system prompt for extraction — this avoids strict-mode requirements and works with all OpenAI models.
Impact
Severity
High — graph memory feature non-functional with OpenAI providers
Bug
Graph entity extraction fails with
400 Bad Requestwhen using OpenAI providers (gpt-4o-mini,gpt-4o).Root Cause
GraphExtractor::extract()callschat_typed_erased::<ExtractionResult>which usescached_schema::<T>()to generate a JSON Schema viaschemars::schema_for!(). The generated schema is sent asresponse_format: {type: "json_schema", strict: true, ...}to OpenAI.OpenAI's structured output requirements for
strict: truemode mandate:additionalProperties: falseon all object schemas —schemarsdoes not add thisrequired— optional fields (summary,temporal_hint: Option<String>) are generated as non-required byschemarsWhen these conditions are violated, OpenAI returns
400 Bad Request.Reproduction
Run graph test with
extract_model = "gpt-4o-mini"via ACP. Both conversation turns trigger extraction; all extraction calls fail with:Graph works in CLI mode with Claude Haiku (Claude handles
$defs/optional differently).Fix
cached_schema::<T>()must post-process the generated schema before sending to OpenAI:"additionalProperties": falseto alltype: "object"schemasrequired, and forOption<T>fields useanyOf: [{...T schema}, {"type": "null"}]The Gemini provider already handles schema normalization via
normalize_schema()ingemini.rs(PR #1635). A similar transform is needed for OpenAI's structured output path.Alternatively: use
response_format: {type: "json_object"}(no-strict mode) with explicit schema in system prompt for extraction — this avoids strict-mode requirements and works with all OpenAI models.Impact
Severity
High — graph memory feature non-functional with OpenAI providers