Tencent/WeKnora · warning

decode suggestion JSON: %w

Error message

decode suggestion JSON: %w

What it means

parseGeneratedSuggestions extracts a JSON envelope from raw LLM output and unmarshals it. If the extracted substring is not valid JSON (malformed model output), json.Unmarshal fails and the error is wrapped as 'decode suggestion JSON'. This is a model-output quality problem, not a caller bug.

Source

Thrown at internal/application/service/message_suggestion.go:815

}

type generatedSuggestionEnvelope struct {
	Questions []struct {
		Text     string `json:"text"`
		Category string `json:"category"`
	} `json:"questions"`
}

func parseGeneratedSuggestions(content string, allowedCategories []string, limit int) (types.SuggestionItems, error) {
	content = strings.TrimSpace(suggestionThinkBlock.ReplaceAllString(content, ""))
	start := strings.Index(content, "{")
	end := strings.LastIndex(content, "}")
	if start < 0 || end < start {
		return nil, errors.New("model returned invalid suggestion JSON")
	}
	var envelope generatedSuggestionEnvelope
	if err := json.Unmarshal([]byte(content[start:end+1]), &envelope); err != nil {
		return nil, fmt.Errorf("decode suggestion JSON: %w", err)
	}
	allowed := make(map[string]struct{}, len(allowedCategories))
	for _, category := range allowedCategories {
		allowed[category] = struct{}{}
	}
	seen := make(map[string]struct{})
	items := make(types.SuggestionItems, 0, limit)
	for _, question := range envelope.Questions {
		text := strings.TrimSpace(question.Text)
		if text == "" || len([]rune(text)) > 200 {
			continue
		}
		key := normalizeSuggestionText(text)
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		category := question.Category

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Strengthen the prompt: require strict JSON, no fences, no commentary, and set a sufficient max_tokens.
  2. Use structured output / JSON mode if the model provider supports it, or lower temperature.
  3. Log the raw content on failure to see what the model actually returned, then widen the extraction or repair (e.g. strip fences) before unmarshaling.
  4. Retry the generation once on decode failure; treat repeated failures as model regression.

Example fix

// before
if err := json.Unmarshal([]byte(content[start:end+1]), &envelope); err != nil {
    return nil, fmt.Errorf("decode suggestion JSON: %w", err)
}
// after
candidate := strings.ReplaceAll(content[start:end+1], "```", "")
if err := json.Unmarshal([]byte(candidate), &envelope); err != nil {
    return nil, fmt.Errorf("decode suggestion JSON: %w (content: %.200s)", err, content)
}
Defensive patterns

Strategy: validation

Validate before calling

start := strings.Index(content, "{")
end := strings.LastIndex(content, "}")
if start < 0 || end <= start { return fmt.Errorf("no JSON in model output") }
json.Valid([]byte(content[start : end+1])) // cheap pre-check

Type guard

func looksLikeSuggestionJSON(content string) bool {
    s := strings.Index(content, "{"); e := strings.LastIndex(content, "}")
    return s >= 0 && e > s && json.Valid([]byte(content[s:e+1]))
}

Try / catch

suggestions, err := parseGeneratedSuggestions(content, allowed)
if err != nil && strings.Contains(err.Error(), "decode suggestion JSON") {
    log.Warnf("bad model output: %.200s", content)
    suggestions, err = retryGeneration(ctx) // one retry
}

Prevention

When it happens

Trigger: generateWithModel receiving model content where the text between the first '{' and last '}' is not valid JSON — truncated response, prose mixed with JSON, markdown fences inside the braces, or non-JSON hallucination.

Common situations: Model hitting max_tokens and truncating the JSON; model wrapping JSON in ```json fences; small/weak models producing comments or trailing commas; temperature too high for structured output.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/dfb1de9e25468aa7. Report an issue: GitHub.