On this page

Detector

A detector is one commercial AI-detection API wrapped behind a uniform interface (src/detectors.rs): given a text, it returns an AI probability in [0, 1]. Four are built in, and the set is closed — an unknown name is refused with unknown detector "<name>"; available detectors: gptzero, originality, sapling, winston.

Shape

Name Endpoint (default) Auth header Request body Revision
gptzero https://api.gptzero.me/v2/predict/text x-api-key {"document": text} v2
winston https://api.gowinston.ai/v2/ai-content-detection Authorization: Bearer {"text": text} v2-human-score
originality https://api.originality.ai/api/v3/scan x-oai-api-key {"content": text, "aiModelVersion": "1"} v3
sapling https://api.sapling.ai/api/v1/aidetect Authorization: Bearer {"text": text, "sent_scores": false} 20251027

Each detector is enabled by its *_API_KEY variable and its endpoint is overridable with the matching *_API_URL variable (configuration). Every detector result is:

{ "detector": "sapling", "ai_probability": 0.7835, "cached": false }

Probability extraction

Responses are parsed against an ordered list of JSON-pointer paths per provider (e.g. GPTZero tries /documents/0/completely_generated_prob, then /documents/0/ai_probability, /documents/0/probability/ai, /score). A raw value in (1, 100] is treated as a percentage and divided by 100. Winston reports a human score, so Skryba stores 1 - score — every detector's ai_probability means the same thing. A response with no recognized value in [0, 1] fails with <name> response: <body> caused by response has no recognized probability in [0, 1].

Lifecycle of one score

  1. Empty or whitespace-only text is refused: <name> cannot score empty text.
  2. The cache is consulted; a hit returns cached: true and no request is sent.
  3. The per-detector rate limiter spaces requests at least --detector-interval-ms apart (default 250 ms).
  4. The request is sent (45-second timeout). 429 and 5xx responses are retried up to 4 times, honoring Retry-After when present, otherwise exponential backoff (1, 2, 4, 8 s; each delay capped at 30 s). Connection errors back off the same way.
  5. Other non-2xx statuses fail immediately: <name> returned <status>: <body> (body bounded to 500 characters).
  6. The extracted probability is written to the cache, and the result returns cached: false.

The ensemble

Commands never talk to one detector; they hold a DetectorEnsemble built from the configured names (see credential boundary for inference rules). The ensemble scores a text with every detector concurrently and fails as a whole if any member fails — a partial ensemble would silently change what ai_probability means. The ensemble mean is the number the pipeline optimizes; see score.

Invariants

  • A detector is only constructed when its API key is present and non-empty (<KEY_VARIABLE> is required for <name>).
  • ai_probability is always in [0, 1] and always means "probability the text is AI-generated", regardless of provider convention.
  • Identical text never bills twice against the same detector revision and endpoint (cache).
  • At least one detector must be configured; the ensemble refuses to be empty.

Maintained as part of the website-owned Skryba documentation corpus.