LLM: fix strict schema 400 — correct providerOptions + chat model

Previous attempt set `strictJsonSchema:false` on createOpenAI()
(factory ignores unknown keys), so wire was still strict:true and
sparse edits {brightness:0.3} were rejected: Missing 'arrangement'.

- src/llm/llmTweak.js: use provider.chat(modelId) (/chat/completions,
  correct for Meta via gateway, not /responses) and pass
  `providerOptions:{ openai:{ strictJsonSchema:false } }` on
  generateObject where the SDK actually reads it.
- src/llm/spec.js: edits fields nullable instead of optional — strict
  requires every key in `required` when `additionalProperties:false`;
  nullable makes required = all 7 (model emits null for unchanged),
  clampEdits already strips nulls so NO_EDITS still works. Prompt now
  instructs "use null for unchanged, example { bpm:null,... }".
- test/llm.test.js: minimal valid result now all-null with one edit.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-09-13 20:48:14 +02:00
co-authored by Claude
parent e8c04e7798
commit 82ab88e6b0
3 changed files with 17 additions and 11 deletions
+9 -8
View File
@@ -12,13 +12,13 @@ const ARRANGEMENT_VALUES = Object.keys(ARRANGEMENTS);
export { ARRANGEMENT_VALUES };
export const TweakEditsSchema = z.object({
bpm: z.number().int().min(60).max(180).optional(),
brightness: z.number().min(0).max(1).optional(),
noise: z.number().min(0).max(1).optional(),
noiseColour: z.number().min(0).max(1).nullable().optional(),
dynamics: z.number().min(0).max(1).optional(),
transition: z.number().min(0).max(1).optional(),
arrangement: z.enum(ARRANGEMENT_VALUES).optional(),
bpm: z.number().int().min(60).max(180).nullable(),
brightness: z.number().min(0).max(1).nullable(),
noise: z.number().min(0).max(1).nullable(),
noiseColour: z.number().min(0).max(1).nullable(),
dynamics: z.number().min(0).max(1).nullable(),
transition: z.number().min(0).max(1).nullable(),
arrangement: z.enum(ARRANGEMENT_VALUES).nullable(),
});
export const TweakResultSchema = z.object({
@@ -106,8 +106,9 @@ export function buildTweakPrompt({ spec, track, look, lookDigest = null, hint =
'- Prefer lowering agitation for breakdowns/intros; keep section count ≥3 if possible.',
'- Valid bpm 60-180 int, brightness/noise/dynamics/transition/noiseColour 0-1, arrangement one of: ' + ARRANGEMENT_VALUES.join(', ') + '.',
'- noiseColour may be null (follows brightness) or 0-1; leave it null unless you have a reason.',
'- All 7 edit fields MUST be present in edits — use null for unchanged (not omitted). Example: { bpm: null, brightness: 0.6, noise: null, noiseColour: null, dynamics: null, transition: null, arrangement: null }',
'- Explain your reasoning in 1-2 sentences (rationale).',
'- If the look is already good, return empty edits {}.',
'- If the look is already good, return all-null edits.',
hintBlock,
].join('\n');