Skip to content

AI Classification

FenLiu classifies each post's text and first image using an external AI model (any OpenAI-compatible API). The result is a recommendation shown in the review interface — it does not auto-reject posts by default.

How it works

Two checks run on each newly-fetched post, right after it is saved:

  1. Text check: the status text and alt text are checked against the text policy. By default this flags promotional content where a cat is only the excuse (fundraisers, books, war victims, and similar). Flagged posts skip the image check.
  2. Vision check: the post's first image is classified against the vision policy. By default the image must be a real photograph of a cat (not a comic, painting, mural, meme, screenshot, AI-generated image, statue, toy, etc.).

The verdicts are stored in topic-neutral fields: text_flagged (the text stage recommends rejection) and vision_accepted (the vision stage passes the image). What "flagged" or "accepted" means is defined entirely by the prompts — see below.

If a check cannot be completed (e.g. the image can't be downloaded or the API call fails), the post is left as AI: pending and reviewed manually. Posts with no still image skip the vision check.

For safety, image URLs are fetched with server-side request forgery (SSRF) protection: only https:// URLs on public hosts are downloaded, redirects are followed manually with every hop re-validated (up to five), and anything else is treated as a failed download — the post stays AI: pending for manual review.

The combined verdict is stored as ai_would_reject and shown as a badge on each review card:

Badge Meaning
AI: pending Not yet classified
AI: OK Passed both checks
AI: reject — <category or reason> Failed a check — the vision category (e.g. comic) for vision-stage rejects, the text stage's reason (truncated) for text-stage rejects

Review filters

The review page has two AI filters:

  • Show only AI-would-reject — list only posts the AI recommends rejecting.
  • Hide not-yet-classified — hide posts the AI hasn't processed yet.

Settings

  • ai_auto_reject (default off): when enabled, posts the AI would reject are auto-rejected. Leave off until you trust the classifier.

Prompts

The text and vision policies are editable on the Settings page — one textarea per stage. Out of the box both describe the default cat-curation topic, so a fresh FenLiu instance behaves exactly as before. To curate a different topic (birds, books, pottery, …), rewrite both prompts to describe your topic and what should be flagged or accepted.

Two things to know:

  • The machine-readable part of the prompt (the JSON output contract) is appended automatically by the code and is not part of the editable text.
  • Prompt changes apply to future fetches only — existing posts keep their verdicts.

Configuration

Variable Default Description
AI_API_BASE_URL https://api.moonshot.ai/v1 OpenAI-compatible API base URL
AI_PROVIDER moonshot AI back end: moonshot (Moonshot direct) or openrouter (OpenRouter); an unrecognised value fails at startup
AI_API_KEY (none — required) API key
AI_TEXT_MODEL kimi-k2.6 Model for the text check
AI_VISION_MODEL kimi-k2.6 Model for the vision check

FenLiu works against either of these back ends, selected by AI_PROVIDER:

AI_PROVIDER Base URL for AI_API_BASE_URL Model for AI_TEXT_MODEL / AI_VISION_MODEL
moonshot https://api.moonshot.ai/v1 kimi-k2.6
openrouter https://openrouter.ai/api/v1 moonshotai/kimi-k2.6

The setting only selects the reasoning-disable request parameter. The second and third columns are the values to set in AI_API_BASE_URL and AI_TEXT_MODEL/AI_VISION_MODEL when switching back ends — AI_PROVIDER alone does not change them. Switching also requires an API key issued by that provider in AI_API_KEY: a Moonshot key will not authenticate against OpenRouter, and vice versa.

The reasoning-disable request parameter follows the back end: Moonshot's thinking parameter for the Moonshot API, OpenRouter's unified reasoning parameter (per OpenRouter's reasoning documentation) for OpenRouter. Reply handling is independent of the setting — parsing reads whichever reasoning field the reply actually carries, and the malformed-reply diagnostics log the serving provider name that OpenRouter reports.