fix: an empty model reply is logged with its finish reason, and a pool batch that gets one is retried once
Some checks failed
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Docker Build / Build Docker image (push) Successful in 8s
Forgejo Docker Build / End-to-end (browser) (push) Failing after 16s

Two batches in five came back as a 200 with an empty message — nothing in
the gateway log, nothing in ours. callLiteLLM now says when that happens
(finish reason, completion tokens, whether the tokens went into reasoning),
and the starter-question build tries such a batch once more instead of
writing the category off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-13 06:41:49 +02:00
parent a528986a2d
commit d517a3fdf6
2 changed files with 21 additions and 4 deletions

View file

@ -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
};
}

View file

@ -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);
}