can1357/oh-my-pi · error

auto-thinking: unparseable local classification: ${JSON.stri

Error message

auto-thinking: unparseable local classification: ${JSON.stringify(text)}

What it means

The local auto-thinking classifier asks a tiny local model to label a prompt with a difficulty bucket (e.g. low/medium/high/max). classifyLocal parses the model's output with parseDifficultyBucket; if the returned text cannot be mapped to an Effort bucket, it throws this error naming the offending text.

Source

Thrown at packages/coding-agent/src/auto-thinking/classifier.ts:176

async function classifyLocal(input: string, modelKey: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
	if (!isTinyMemoryLocalModelKey(modelKey)) {
		throw new Error(`auto-thinking: unsupported local classifier model: ${modelKey}`);
	}
	const maxTokens = isTinyMemoryReasoningModelKey(modelKey)
		? Math.max(LOCAL_ANSWER_MAX_TOKENS, LOCAL_REASONING_MAX_TOKENS)
		: LOCAL_ANSWER_MAX_TOKENS;
	const builtPrompt = prompt.render(difficultyLocalPrompt, { prompt: input });
	const text = await tinyModelClient.complete(modelKey, builtPrompt, {
		maxTokens,
		signal: deps.signal,
	});
	if (!text) {
		throw new Error("auto-thinking: local classification returned no output");
	}
	const effort = parseDifficultyBucket(text);
	if (!effort) {
		throw new Error(`auto-thinking: unparseable local classification: ${JSON.stringify(text)}`);
	}
	return effort;
}

/**
 * Map an online level keyword to an {@link Effort}; earliest match wins.
 *
 * `max` is only offered to the classifier when the target model exposes that
 * tier, but it is always parsed: an unsupported `max` is snapped back down by
 * {@link clampAutoThinkingEffort} rather than failing the turn.
 */
export function parseDifficultyLevel(text: string): Effort | undefined {
	const lower = text.toLowerCase();
	const candidates: Array<[number, Effort]> = [];
	// `xhigh` must be probed as its own token: `\bhigh\b` cannot match the "high"
	// inside "xhigh" (no word boundary between `x` and `h`), so the two never collide.
	const xhigh = lower.search(/x[\s_-]?high/);
	if (xhigh >= 0) candidates.push([xhigh, Effort.XHigh]);

View on GitHub (pinned to 9690622007)

Solutions

  1. Inspect the JSON-embedded text in the message to see what the model actually returned
  2. Retry the classification — local tiny models can produce flaky output
  3. Verify the configured classifier model is the intended tiny local model and that reasoning output is not polluting the answer
  4. If it reproduces consistently, check parseDifficultyBucket's accepted keywords against the local prompt's answer format
Defensive patterns

Strategy: retry

Validate before calling

const text = await tinyModelClient.complete(modelKey, builtPrompt, { maxTokens, signal });
if (!text || !parseDifficultyBucket(text)) {
  // fall back / retry before surfacing the error
}

Try / catch

try {
  const effort = await effort(input);
} catch (err) {
  if (String((err as Error).message).includes("unparseable local classification")) {
    effort = DEFAULT_EFFORT; // or retry once
  } else throw err;
}

Prevention

When it happens

Trigger: classifyLocal is called (via effort) with a local tiny-model key, tinyModelClient.complete returns non-empty text, but parseDifficultyBucket finds no valid bucket keyword in it — e.g. the model answers with reasoning, a full sentence, or a different label format.

Common situations: Local model outputs verbose prose instead of the expected single keyword; a reasoning-enabled tiny model burns tokens on thinking and answers oddly; the prompt was changed or the model is misconfigured so its reply format drifts.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/3e2c05b1cef04f81. Report an issue: GitHub.