From 82ab88e6b05bbf393221248e8359e4f55d85a725 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 13 Sep 2026 20:48:14 +0200 Subject: [PATCH] =?UTF-8?q?LLM:=20fix=20strict=20schema=20400=20=E2=80=94?= =?UTF-8?q?=20correct=20providerOptions=20+=20chat=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- flow-state/src/llm/llmTweak.js | 4 ++-- flow-state/src/llm/spec.js | 17 +++++++++-------- flow-state/test/llm.test.js | 7 ++++++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/flow-state/src/llm/llmTweak.js b/flow-state/src/llm/llmTweak.js index 859f7d8..b40bc1b 100644 --- a/flow-state/src/llm/llmTweak.js +++ b/flow-state/src/llm/llmTweak.js @@ -97,17 +97,17 @@ export async function requestSongTweak({ spec, track, look, hint = '', config = baseURL: isDev ? '/__llm/v1' : cfg.gatewayUrl, apiKey: cfg.apiKey, fetch: isDev ? proxyFetch : undefined, - strictJsonSchema: false, }); const modelId = cfg.model; try { const result = await generateObject({ - model: provider(modelId), + model: provider.chat(modelId), schema: TweakResultSchema, messages, temperature: 0.4, maxRetries: 0, + providerOptions: { openai: { strictJsonSchema: false } }, }); const raw = result.object || {}; diff --git a/flow-state/src/llm/spec.js b/flow-state/src/llm/spec.js index 72a5ae9..1db83b0 100644 --- a/flow-state/src/llm/spec.js +++ b/flow-state/src/llm/spec.js @@ -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'); diff --git a/flow-state/test/llm.test.js b/flow-state/test/llm.test.js index 19ca39f..0a58c61 100644 --- a/flow-state/test/llm.test.js +++ b/flow-state/test/llm.test.js @@ -52,8 +52,13 @@ test('buildTweakPrompt truncates long hint in system', () => { }); test('TweakResultSchema validates a minimal valid result', () => { - const parsed = TweakResultSchema.parse({ rationale: 'Lower noise to calm the drop.', edits: { noise: 0.3 }, confidence: 0.8 }); + const parsed = TweakResultSchema.parse({ + rationale: 'Lower noise to calm the drop.', + edits: { bpm: null, brightness: null, noise: 0.3, noiseColour: null, dynamics: null, transition: null, arrangement: null }, + confidence: 0.8, + }); assert.equal(parsed.edits.noise, 0.3); + assert.equal(parsed.edits.bpm, null); assert.ok(parsed.rationale.length > 0); });