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