Pangram AI-text-detection API client, I/O-free
OCaml 86%
5%
Standard ML 4%
Perl 2%
Shell 2%
Dune 1%

README.md

pangram #

An OCaml client for the Pangram Labs AI-text-detection API, and a pangram command that scores files.

pangram describes the calls of the Pangram inference API (models, tasks and bulk jobs) as values and decodes their answers, with no I/O. pangram-eio performs them over Eio: it submits a text, polls within one deadline and with an exponential backoff, and pages through bulk results. The pangram command prints each file's prediction and the fractions of its text judged AI-written, AI-assisted and human-written, as a table or as JSON.

The codecs follow the API reference for Pangram 4 (version "4.0"). Decoders ignore members they do not name.

Security #

Sending a text to Pangram hands it to a third party. The pangram command refuses a file that lives in a git repository whose remote belongs to a private owner, including a local clone of one, unless --allow-private is given. Standard input is judged by the directory the command runs in. A public dashboard link, which publishes the text, is requested only with --dashboard-link.

The API key is never printed: Pangram.Api_key.pp shows <redacted>, and -v names where the key came from, not the key. A key file that its group or others may read is refused, with the chmod that fixes it.

Install #

opam install pangram-eio

Usage #

Library #

Describing a request needs no network:

# let r = Pangram.submit ~model:"pangram-4" "Some text to score." ;;
val r : Pangram.Task_id.t Pangram.Request.t = <abstr>
# Pangram.Request.path r ;;
- : string = "/task"
# Pangram.Request.body r ;;
- : string option =
Some
 "{\"text\":\"Some text to score.\",\"model\":\"pangram-4\",\"public_dashboard_link\":false}"

Decoding an answer is a result:

# match Pangram.Task.of_string {|{"stage":"STAGE_PREPROCESSING"}|} with
  | Ok (Pangram.Task.Pending s) -> Pangram.Stage.to_string s
  | _ -> "other" ;;
- : string = "STAGE_PREPROCESSING"

Scoring a text with the Eio client:

let score text =
  match Pangram.Api_key.of_string (Sys.getenv "PANGRAM_API_KEY") with
  | Error e -> prerr_endline e
  | Ok key -> (
      Eio_main.run @@ fun env ->
      Eio.Switch.run @@ fun sw ->
      let client =
        Pangram_eio.v ~sw ~clock:(Eio.Stdenv.clock env) (Eio.Stdenv.net env) key
      in
      match Pangram_eio.score client text with
      | Ok s -> Printf.printf "%s (AI %.2f)\n" s.prediction_short s.fraction_ai
      | Error e -> prerr_endline (Pangram.Error.to_string e))

Pangram_eio.bulk submits many texts as bulk jobs, which Pangram bills 20% lower, and maps the results back to the caller's item ids.

Command line #

The key is read from --key-file PATH, then from PANGRAM_API_KEY, then from $XDG_CONFIG_HOME/pangram/token (by default ~/.config/pangram/token), which must have mode 0600.

$ pangram score README.md docs/*.md
estimated cost: USD 0.40 for 3 input(s) by the realtime API
╭──────────────────┬────────────┬──────┬─────────────┬───────╮
│ File             │ Prediction │   AI │ AI-assisted │ Human │
...
$ pangram score --json --max-cost 1.00 README.md
$ git log -1 --format=%B | pangram score
$ pangram score --bulk docs/*.md
$ pangram bulk results blk_123

The estimated cost is printed before anything is sent; --max-cost USD refuses a run whose estimate is above it. More than ten inputs, or --bulk, go through the Bulk API as one job per 1000 billable units; the job id is printed at submission so that pangram bulk status ID and pangram bulk results ID can resume an interrupted run. pangram models lists the model selectors the key may use.

Pangram splits a long text into windows itself, so each file is sent whole.

Tests #

The codec tests decode the example responses of the API reference, copied verbatim. The client is tested against a local fake of the API. Interop traces of the live API are not recorded: run PANGRAM_API_KEY=... REGEN=1 dune build @traces with a key to record them; a run spends a few cents of credit.

License #

ISC. See LICENSE.md.