diff --git a/src/utils/ai.js b/src/utils/ai.js index 81cd75e6..7cbc48ee 100644 --- a/src/utils/ai.js +++ b/src/utils/ai.js @@ -366,14 +366,24 @@ async function callLiteLLM(messages, model, temperature, maxTokens, generation) max_tokens: maxTokens }, generation || {}), generation)); + var choice = completion.choices[0]; + // An empty reply with a 200 is the one failure nothing else reports: the + // gateway is happy, the caller gets '' and a JSON task keeps nothing. Say + // what the model said about it — the finish reason and whether the tokens + // went into reasoning instead of the answer. + if (!choice.message.content && !choice.message.tool_calls) { + var reasoningChars = String(choice.message.reasoning_content || choice.message.reasoning || '').length; + console.warn('[ai] empty reply from ' + model + ': finish_reason=' + (choice.finish_reason || '?') + + ' completion_tokens=' + (completion.usage && completion.usage.completion_tokens) + ' reasoning_chars=' + reasoningChars); + } return { success: true, - content: completion.choices[0].message.content, - ...(completion.choices[0].message.tool_calls ? { toolCalls: completion.choices[0].message.tool_calls } : {}), + content: choice.message.content, + ...(choice.message.tool_calls ? { toolCalls: choice.message.tool_calls } : {}), model: model, provider: 'litellm', usage: completion.usage || null, - finishReason: completion.choices[0].finish_reason || null + finishReason: choice.finish_reason || null }; } diff --git a/src/utils/clinicalPromptPool.js b/src/utils/clinicalPromptPool.js index bed5a653..dcb468e9 100644 --- a/src/utils/clinicalPromptPool.js +++ b/src/utils/clinicalPromptPool.js @@ -360,7 +360,14 @@ function createClinicalPromptPool(opts) { }); var parsed = parseJsonObject(String(ai.content || '')); var offered = Array.isArray(parsed.questions) ? parsed.questions.length : 0; - if (!offered) console.warn('[clinical-assistant] prompt pool ' + item.category + ' batch ' + (i + 1) + ': nothing parseable in ' + String(ai.content || '').length + ' chars'); + if (!offered) { + // The model answers a JSON task with an empty message now and then — + // two batches in five came back with nothing. One more try with the + // same snippets is cheap and usually enough. + console.warn('[clinical-assistant] prompt pool ' + item.category + ' batch ' + (i + 1) + ': nothing parseable in ' + String(ai.content || '').length + ' chars (finish ' + (ai.finishReason || '?') + '); retrying once'); + i--; batches++; if (batches > 12) break; + continue; + } else if (parsed.truncated) console.warn('[clinical-assistant] prompt pool ' + item.category + ' batch ' + (i + 1) + ': reply was cut off; ' + offered + ' questions salvaged'); categoryExamples = normalizeGeneratedExamples(categoryExamples.concat(parsed.questions || []), item).slice(0, item.quota); }