Skip to content

Fix OpenAI strict-mode schema for /api/memory/extract + /api/decide#5

Draft
wdwd720 wants to merge 1 commit into
cursor/llm-memory-and-reasoning-c3fafrom
cursor/fix-openai-strict-schema-c3fa
Draft

Fix OpenAI strict-mode schema for /api/memory/extract + /api/decide#5
wdwd720 wants to merge 1 commit into
cursor/llm-memory-and-reasoning-c3fafrom
cursor/fix-openai-strict-schema-c3fa

Conversation

@wdwd720

@wdwd720 wdwd720 commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Stacked on PR #4 (cursor/llm-memory-and-reasoning-c3fa) so this PR's diff is just the schema fix.

The bug

OpenAI's strict structured outputs require every object schema to list every property key in required (optional fields stay required but nullable). Our nested items[] schema in MEMORY_EXTRACTION_SCHEMA only listed 5 of its 12 keys in required, so the API rejected the request with:

Invalid schema for response_format 'hearer_response':
  In context=('properties', 'items', 'items'), 'required' is required to be supplied

Result: /api/memory/extract silently fell back to mode: "rule_based", even with enabled=true configured=true provider=openai_compatible.

The fix

server/src/llm/llmSchemas.ts

  • New strictObject(properties) helper. It auto-derives required from Object.keys(properties) so it's impossible to forget a field. Sets type: "object", additionalProperties: false, required, and properties.
  • MEMORY_EXTRACTION_SCHEMA rebuilt with strictObject(). The inner item schema now lists all 12 keys (kind, label, triggerDescription, actionType, actionLabel, person, deadlineText, locationLabel, priority, confidence, save, privacyNote) in required. Optional fields stay required but use nullable union types (type: ["string", "null"], or enum: [..., null] for priority).
  • CUE_REASONING_SCHEMA rebuilt the same way for safety.
  • New exported assertStrictOpenAiSchema(schema, path?) walks a schema and throws on the first violation (missing properties / required / additionalProperties, mismatch between required and properties keys, recurses into array items).

server/src/llm/openAiCompatibleProvider.ts

  • chatJson now takes a schemaName, so each request gets its own name in the OpenAI payload: "hearer_memory_extraction" / "hearer_cue_reasoning".

  • On a non-2xx response we parse OpenAI's error.message + error.code and surface a compact line in the server log:

    [hearer-llm] provider HTTP 400 on schema='hearer_memory_extraction': invalid_request_error: …
    

    We never log the API key (it's in the Authorization header) and we never log the user transcript.

Tests

server/src/__tests__/openAiSchemaStrictness.test.ts (new):

  • MEMORY_EXTRACTION_SCHEMA passes assertStrictOpenAiSchema.
  • CUE_REASONING_SCHEMA passes assertStrictOpenAiSchema.
  • The nested memory item schema's required matches its properties keys (every one of the 12 fields).
  • The assertion throws when required is missing a property.
  • The assertion throws when additionalProperties is missing.
  • The assertion recurses into array item schemas.

README

Smoke commands added with the expected mode: "llm", fallbackUsed: false response.

Results

  • npm run build → success.
  • npm test71 / 71 passing across 19 suites (was 65 / 18). All prior tests still pass.

After merging this stack

curl -s http://localhost:8788/api/llm/status
# { "enabled": true, "configured": true, "provider": "openai_compatible", "model": "gpt-4.1-mini", ... }

curl -s -X POST http://localhost:8788/api/memory/extract \
  -H 'content-type: application/json' \
  -d '{"text":"Usually after dinner I cook, so if you hear beeping remind me to check the stove."}'
# {
#   "mode": "llm",
#   "fallbackUsed": false,
#   "persisted": { "routines": [ { "name": "Cooking timer safety cue", "actionLabel": "check stove", ... } ] },
#   ...
# }
Open in Web Open in Cursor 

OpenAI's strict structured outputs require every object schema to:
  1. set type="object", properties, additionalProperties:false
  2. include EVERY property key in "required"
  3. apply the same recursively to nested objects (incl. array items)

The MEMORY_EXTRACTION_SCHEMA's nested item schema only listed 5 of its
12 keys in required, so OpenAI rejected the request with
  Invalid schema for response_format 'hearer_response':
    In context=('properties','items','items'), 'required' is required to
    be supplied
and /api/memory/extract silently fell back to mode='rule_based'.

Changes:
- server/src/llm/llmSchemas.ts:
  - new strictObject() helper that auto-derives "required" from the
    properties keys so it's impossible to forget a field;
  - rebuilt MEMORY_EXTRACTION_SCHEMA with strictObject(), so the inner
    item schema now lists kind/label/triggerDescription/actionType/
    actionLabel/person/deadlineText/locationLabel/priority/confidence/
    save/privacyNote in required (optional fields stay required but
    nullable via type:["string","null"] / enum:[...,null]);
  - rebuilt CUE_REASONING_SCHEMA the same way;
  - new exported assertStrictOpenAiSchema(schema, path?) walks a schema
    and throws on the first violation (missing properties, missing
    required, missing additionalProperties:false, required vs properties
    mismatch, recurses into array items).

- server/src/llm/openAiCompatibleProvider.ts:
  - chatJson now takes a schemaName so each schema gets its own name in
    the OpenAI request ("hearer_memory_extraction" /
    "hearer_cue_reasoning");
  - on a non-2xx response we parse OpenAI's error.message + error.code,
    print [hearer-llm] provider HTTP <status> on schema='<name>': <msg>
    to the server log, and propagate the same compact message in the
    LlmError. We never log the API key (it's in the Authorization
    header) and we never log the user prompt.

- server/src/__tests__/openAiSchemaStrictness.test.ts (new, 5 tests):
  - MEMORY_EXTRACTION_SCHEMA passes assertStrictOpenAiSchema;
  - CUE_REASONING_SCHEMA passes assertStrictOpenAiSchema;
  - the nested memory item schema's required matches its properties keys
    (lists every one of the 12 fields);
  - the assertion throws when required is missing a property;
  - the assertion throws when additionalProperties is missing;
  - the assertion recurses into array items.

- README.md updated with the smoke curl + the expected
  mode='llm', fallbackUsed=false response.

Tests: 71/71 passing across 19 suites (was 65/18). Build passes.

Co-authored-by: wdwd720 <wdwd720@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants