diff --git a/public/js/assistant/citations.js b/public/js/assistant/citations.js
index 6ca822b..c33db86 100644
--- a/public/js/assistant/citations.js
+++ b/public/js/assistant/citations.js
@@ -22,7 +22,11 @@ export function safeImageUrl(src) {
export function renderAssistantMarkdown(md, sources, options) {
var opts = options || {};
- var text = String(md || '');
+ var text = String(md || '')
+ // The model occasionally writes [src]/[source] placeholders instead of
+ // real citation numbers. Never invent numbers — drop the tokens; the
+ // sources panel still lists the actual sources.
+ .replace(/\[(?:src|source)\]/gi, '');
var embedded = textProtection(text, 'html');
// Literal/embedded content bypasses prose, math and citation rewriting, then
// rejoins the parsed HTML before the single sanitization boundary.
diff --git a/test/assistant-citations.test.js b/test/assistant-citations.test.js
index 7214aac..88e2b22 100644
--- a/test/assistant-citations.test.js
+++ b/test/assistant-citations.test.js
@@ -45,6 +45,15 @@ test('can render citation labels as numbers for PDF export', async () => {
assert.match(html, /]*>1<\/a> ]*>2<\/a>/);
});
+test('strips [src]/[source] placeholder tokens instead of inventing numbers', async () => {
+ const { renderAssistantMarkdown } = await loadCitationModule();
+ const html = renderAssistantMarkdown('Give fluids [src] and review in 24h [source].', []);
+ assert.doesNotMatch(html, /\[src\]|\[source\]/i);
+ assert.match(html, /Give fluids/);
+ const kept = renderAssistantMarkdown('Give fluids [1] and review.', [{ number: 1, title: 'S', page: 2 }]);
+ assert.match(kept, /data-source-number="1"/, 'real citation numbers still render');
+});
+
test('leaves unknown citation clusters untouched rather than guessing', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Dose statement [4].', sources);