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:
@@ -97,17 +97,17 @@ export async function requestSongTweak({ spec, track, look, hint = '', config =
|
|||||||
baseURL: isDev ? '/__llm/v1' : cfg.gatewayUrl,
|
baseURL: isDev ? '/__llm/v1' : cfg.gatewayUrl,
|
||||||
apiKey: cfg.apiKey,
|
apiKey: cfg.apiKey,
|
||||||
fetch: isDev ? proxyFetch : undefined,
|
fetch: isDev ? proxyFetch : undefined,
|
||||||
strictJsonSchema: false,
|
|
||||||
});
|
});
|
||||||
const modelId = cfg.model;
|
const modelId = cfg.model;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await generateObject({
|
const result = await generateObject({
|
||||||
model: provider(modelId),
|
model: provider.chat(modelId),
|
||||||
schema: TweakResultSchema,
|
schema: TweakResultSchema,
|
||||||
messages,
|
messages,
|
||||||
temperature: 0.4,
|
temperature: 0.4,
|
||||||
maxRetries: 0,
|
maxRetries: 0,
|
||||||
|
providerOptions: { openai: { strictJsonSchema: false } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const raw = result.object || {};
|
const raw = result.object || {};
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ const ARRANGEMENT_VALUES = Object.keys(ARRANGEMENTS);
|
|||||||
export { ARRANGEMENT_VALUES };
|
export { ARRANGEMENT_VALUES };
|
||||||
|
|
||||||
export const TweakEditsSchema = z.object({
|
export const TweakEditsSchema = z.object({
|
||||||
bpm: z.number().int().min(60).max(180).optional(),
|
bpm: z.number().int().min(60).max(180).nullable(),
|
||||||
brightness: z.number().min(0).max(1).optional(),
|
brightness: z.number().min(0).max(1).nullable(),
|
||||||
noise: z.number().min(0).max(1).optional(),
|
noise: z.number().min(0).max(1).nullable(),
|
||||||
noiseColour: z.number().min(0).max(1).nullable().optional(),
|
noiseColour: z.number().min(0).max(1).nullable(),
|
||||||
dynamics: z.number().min(0).max(1).optional(),
|
dynamics: z.number().min(0).max(1).nullable(),
|
||||||
transition: z.number().min(0).max(1).optional(),
|
transition: z.number().min(0).max(1).nullable(),
|
||||||
arrangement: z.enum(ARRANGEMENT_VALUES).optional(),
|
arrangement: z.enum(ARRANGEMENT_VALUES).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TweakResultSchema = z.object({
|
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.',
|
'- 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(', ') + '.',
|
'- 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.',
|
'- 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).',
|
'- 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,
|
hintBlock,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,13 @@ test('buildTweakPrompt truncates long hint in system', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('TweakResultSchema validates a minimal valid result', () => {
|
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.noise, 0.3);
|
||||||
|
assert.equal(parsed.edits.bpm, null);
|
||||||
assert.ok(parsed.rationale.length > 0);
|
assert.ok(parsed.rationale.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user