koala73/worldmonitor · error
classify-eval: is interpolated; the eval can only read a…
Error message
classify-eval: ${what} is interpolated; the eval can only read a static template literal What it means
staticTemplate guards classify-eval's core invariant: prompts are hashed and sent as literal source text, so a template literal containing '${' would hash and send a literal interpolation placeholder instead of the real runtime prompt. If the extracted prompt text includes '${', it throws naming the offending item. This prevents the eval from silently pinning an incorrect prompt.
Solutions
- Rewrite the production prompt as a static template literal with no ${...} interpolation.
- If runtime values are needed, move them into the completion request as separate parameters/messages, not into the system prompt text.
- Concatenate fixed string pieces instead of interpolating, only if the concatenation itself is static at the definition site.
- Re-run classify-eval to confirm the extracted prompt is static before trusting captured results.
Example fix
// before
const CLASSIFY_SYSTEM_PROMPT = `Classify for model ${MODEL_NAME}.`;
// after
const CLASSIFY_SYSTEM_PROMPT = `Classify the title.`; // model passed as a request parameter Defensive patterns
Strategy: validation
Validate before calling
const m = relaySrc.match(/const CLASSIFY_SYSTEM_PROMPT = `([\s\S]*?)`;/);
if (m && m[1].includes('${')) throw new Error('CLASSIFY_SYSTEM_PROMPT is interpolated'); Type guard
const isStaticTemplate = (text) => typeof text === 'string' && !text.includes('${'); Try / catch
try { const prompt = extractRelayPrompt(src); } catch (e) { if (e.message.includes('is interpolated')) { console.error('Remove ${...} from the production prompt; pass dynamic values as request parameters'); } throw e; } Prevention
- Never interpolate values into system prompt constants
- Pass dynamic data (model name, thresholds) as separate API parameters or user messages
- Add a lint/test that scans prompt constants for '${'
- Review prompt-related PRs for template interpolation before merge
When it happens
Trigger: Running classify-eval when the production prompt constant was converted into an interpolated template literal, e.g. `Prompt for ${modelName}` or a template embedding config values, so the extracted source contains '${'.
Common situations: A developer refactoring prompts to interpolate model names, dates, or thresholds; templating added for localization; generated prompt files where an outer template leaks inner '${'.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- classify-eval: expected one openrouter classify provider…
- classify-eval: could not find
- COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID
- Exa returned malformed structured summary for
- Generated internal probe filters incorrectly cover public…
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22).
Data as JSON: /api/errors/86274abb5972abad.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/classify-eval.mjs:19
// Pure helpers behind scripts/eval-classify-labels.mjs and its tests: read the two
// headline-classification prompts and their OpenRouter model straight out of the
// source files (so the eval can never drift from what production sends), and score a
// set of labels against blind-judged levels.
import { createHash } from 'node:crypto';
export const ALERT_LEVELS = ['critical', 'high'];
export const isAlertLevel = (level) => ALERT_LEVELS.includes(level);
function mustMatch(src, re, what) {
const m = src.match(re);
if (!m) throw new Error(`classify-eval: could not find ${what}`);
return m[1];
}
// A prompt is hashed and sent as source text, so it must BE the text production sends:
// an interpolated template would pin (and send) a literal `${...}`.
function staticTemplate(text, what) {
if (text.includes('${')) throw new Error(`classify-eval: ${what} is interpolated; the eval can only read a static template literal`);
return text;
}
// scripts/ais-relay.cjs — the batch prompt (50 titles per completion).
export function extractRelayPrompt(relaySrc) {
return staticTemplate(mustMatch(relaySrc, /const CLASSIFY_SYSTEM_PROMPT = `([\s\S]*?)`;/, 'CLASSIFY_SYSTEM_PROMPT'), 'CLASSIFY_SYSTEM_PROMPT');
}
// Reads ONLY the `openrouter` entry, comments stripped. A model held in a constant, or
// missing, throws rather than falling through to a later provider's literal: this value
// is what `--capture` sends and records, so a wrong read would pin itself.
export function extractRelayOpenRouterModel(relaySrc) {
const list = mustMatch(relaySrc, /const CLASSIFY_LLM_PROVIDERS = \[([\s\S]*?)\n\];/, 'CLASSIFY_LLM_PROVIDERS');
const entries = list.split(/(?=\{\s*\n\s*name: ')/).filter((e) => /^\{\s*\n\s*name: 'openrouter',/.test(e));
if (entries.length !== 1) throw new Error(`classify-eval: expected one openrouter classify provider, found ${entries.length}`);
const code = entries[0].replace(/\/\/[^\n]*/g, '');
return mustMatch(code, /\n\s*model: '([^']+)',/, "a quoted model on the relay's openrouter classify provider");
}View on GitHub (pinned to e586b8b4b8)