A Clojure client for Jev, TypeSafe's
System One model, for Babashka and the JVM from the same source. You send a
state and a set of typed questions about it, and get back structured answers
with probabilities.
It covers the two endpoints in the
TypeSafe OpenAPI spec:
POST /v1/systemone and GET /v1/models.
cljev is at 0.x: the API may change between minor versions until 1.0. It is not affiliated with TypeSafe.
Each release is on Clojars and is also a git tag (see
RELEASING.md). The first release, 0.1.0, has not been made
yet; until it is, depend on a commit with :git/sha.
;; deps.edn or bb.edn, from Clojars
{:deps {io.github.joshrotenberg/cljev {:mvn/version "0.1.0"}}}
;; or from the tag; :git/sha is the tag's commit
{:deps {io.github.joshrotenberg/cljev {:git/tag "v0.1.0" :git/sha "..."}}}
git ls-remote https://github.com/joshrotenberg/cljev.git refs/tags/<tag>
gives the sha.
The only runtime dependencies are babashka.http-client and cheshire, both
of which Babashka bundles.
Set TYPESAFE_API_KEY, then:
(require '[cljev.core :as jev])
(def client (jev/client))
(jev/ask client
"I was charged twice for my subscription this month. Please refund it."
{:billing (jev/noul "Is this message about billing?")
:tone (jev/choice "What is the tone of the message?"
[:angry :calm :excited])
:urgency (jev/score "How urgent is this?"
["Can wait"
"Needs attention this week"
"Needs attention today"])})
;; => {:model "jev-1.14.0"
;; :answers {:billing {:type :noul :noul 0.98}
;; :tone {:type :choice
;; :choice :calm
;; :confidence 0.7
;; :probabilities {:angry 0.15 :calm 0.8 :excited 0.05}}
;; :urgency {:type :score
;; :score 1.7
;; :confidence 0.55
;; :legend {0 "Can wait"
;; 1 "Needs attention this week"
;; 2 "Needs attention today"}
;; :probabilities {0 0.1 1 0.1 2 0.8}}}
;; :usage {:input-tokens 120 :output-tokens 12}}
state is a string, a map, or a sequential collection. Answers are keyed by
the question names you gave, and choice answers use your option names, so
keywords come back as keywords and strings as strings. Score levels are keyed
by their integer position.
For a single question, ask-one returns the answer itself, with the model,
usage, and request id in its metadata:
(jev/ask-one client "The package arrived crushed." (jev/noul "Is this a complaint?"))
;; => {:type :noul :noul 0.97}
| Constructor | Question | Answer fields |
| - | - | - |
| (noul instructions) or (noul instructions {:true .. :false ..}) | Yes or no | :noul, the probability of yes |
| (choice instructions options) | One of 1 to 255 options | :choice, :confidence, :probabilities |
| (score instructions levels) | A rating on 1 to 10 ordered levels | :score, :confidence, :legend, :probabilities |
| (box instructions) | Where something is in an image | :box with :top :bottom :left :right, :confidence |
Instructions, option descriptions, and level descriptions can be strings, maps, or vectors.
The constructors return plain maps, and ask accepts hand-written maps in the
same shape as the HTTP API ({:type :noul :instructions "..."}).
choice takes its options in three forms:
(jev/choice "Tone?" [:angry :calm]) ; names
(jev/choice "Tone?" [[:angry "Hostile or upset"] [:calm "Neutral"]]) ; [name description] pairs
(jev/choice "Tone?" {:angry "Hostile or upset" :calm "Neutral"}) ; a map
Options are sent in the order given. TypeSafe documents that the order can affect the answer and that Jev can lean toward the first option. A collection keeps its order for any number of options. A Clojure map literal only keeps its order up to 8 entries, so use pairs for a larger set with descriptions.
ask-many asks the same questions about each of a collection of states, one
request per state, several at a time. Results come back in input order. A
failed request does not stop the others; its result is {:error e}.
(jev/ask-many client
["Where is my refund?" "I love this product."]
{:question (jev/noul "Is this text a question?")}
{:concurrency 4}) ; default 8
;; => [{:model .. :answers {:question {:type :noul :noul 0.99}} :usage ..}
;; {:model .. :answers {:question {:type :noul :noul 0.01}} :usage ..}]
ask, ask-one, and ask-many take an options map as a last argument:
(jev/ask client state questions
{:model "jev-1.14.0" ; default is the client's model, jev-latest
:long-context? true ; TypeSafe-Danger-Zone-Long-Context: true
:timeout-ms 30000
:headers {"x-request-id" "abc"}
:extra-body {:some_new_field 1}})
jev-latest is an alias that moves when a release ships. Pass a versioned id
to pin the model, per request or as :model on the client.
:extra-body adds top-level fields to the request, for API features this
client has no option for.
image takes a path, a file, or a byte array with a format (:png, :jpg,
:jpeg). Put the image wherever a value goes in the state or in a question's
instructions or criteria. Requests with images or box questions need
:images? true, which sends the TypeSafe-Danger-Zone-Images: true header.
(jev/ask client
{:photo (jev/image "cat.png")}
{:cat? (jev/noul "Is there a cat in the photo?")
:where (jev/box "the cat")}
{:images? true})
To use an image inside a string, write its :ref into the string and pass
the image in :assets:
(let [before (jev/image "before.jpg")
after (jev/image "after.jpg")]
(jev/ask client
(str "Before: " (:ref before) " After: " (:ref after))
{:changed (jev/noul "Did anything change between the two photos?")}
{:assets [before after] :images? true}))
:assets also takes a map of your own reference strings to images, or to raw
asset maps in the API's shape.
Image bytes are uploaded as multipart/form-data. Pass :upload :base64 to
put them in the JSON body instead, which makes each image about a third
larger. An image used in several places is uploaded once.
(jev/models client)
;; => [{:name "jev-latest" :description "..." :release-date "2026-10-22T02:00:00+00:00"} ...]
Results of ask and models carry the HTTP exchange in their metadata:
(meta (jev/ask client state questions))
;; => {:request-id "req_01a1..."
;; :response {:status 200 :headers {...} :body {...}}}
:body is the JSON as received, with string keys, including any fields this
client does not know about. Answers of a type the client does not know are
returned with their fields as they came.
(jev/client {:api-key "..." ; default: TYPESAFE_API_KEY
:base-url "..." ; default: TYPESAFE_BASE_URL, then https://api.typesafe.ai
:model "jev-1.14.0" ; default: TYPESAFE_DEFAULT_MODEL, then jev-latest
:timeout-ms 10000
:compression :gzip
:retry {:max-retries 2
:backoff-initial-ms 500
:backoff-max-ms 5000
:backoff-jitter 0.25
:max-retry-after-ms 60000}
:headers {"x-team" "support"}})
The values shown for :timeout-ms, :compression, and :retry are the
defaults.
:compression nil for a server that does not accept compressed requests.
Multipart requests are not compressed.Responses with status 408, 429, or 5xx (including 529) and connection
failures are retried with exponential backoff. retry-after and
retry-after-ms response headers set the wait when present. A reply that asks
for a wait longer than :max-retry-after-ms is thrown without retrying.
Failures are thrown as ex-info:
| :type in ex-data | When | Other keys |
| - | - | - |
| :cljev.core/api-error | Non-2xx reply after retries | :status, :detail, :body, :headers, :request-id |
| :cljev.core/connection-error | Server unreachable or timed out after retries | :attempts, with the cause attached |
| :cljev.core/invalid-question | Bad choice options or score levels | |
| :cljev.core/invalid-asset | Unsupported image format, or a bad :assets value | |
| :cljev.core/invalid-request | ask called with no questions | |
| :cljev.core/invalid-option | Unknown :upload or :compression value | |
| :cljev.core/missing-api-key | No API key given or in the environment | |
| :cljev.core/invalid-api-key | API key contains whitespace or non-ASCII characters | |
examples/ has example programs, each a namespace with a -main. Run them
from the project root with TYPESAFE_API_KEY set:
| Command | What it does |
| - | - |
| bb -m cljev.example.models | Lists the models the account can use. A quick check that the key works. |
| bb -m cljev.example.triage [MESSAGE] | Asks a noul, a choice, and a score about a support message in one request. |
| bb -m cljev.example.classify LABEL LABEL ... < lines | Labels each line of stdin, one request per line, several at a time. Marks low-confidence labels. |
| bb -m cljev.example.grep DESCRIPTION < lines | Prints the lines of stdin that fit a description, using one yes/no question per line in a single request. |
git log --format=%s -20 | bb -m cljev.example.classify feature fix docs chore
bb -m cljev.example.grep "mentions a deadline" < notes.txt
On the JVM, clojure -M:dev -m cljev.example.triage runs the same programs.
ask-many, future, or virtual threads to fan out requestsAGENTS.md has the layout, the checks, and the conventions.
bb test # tests under Babashka
bb jvm-test # the same tests on the JVM
bb lint # clj-kondo, no em dashes, conventional commits
bb fmt:check # cljfmt check; bb fmt fixes
bb check # all four
bb test and bb jvm-test use a stubbed request function and a local
http-kit server, and never call the TypeSafe API.
bb test:live calls the API. It runs only with CLJEV_LIVE_TESTS=1 and
TYPESAFE_API_KEY set, its requests are billed, and its assertions depend on
the model's answers. It is not part of bb check or CI.
MIT
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |