Liking cljdoc? Tell your friends :D

clojure-llm-sdk

CI Clojars Project

A Clojure SDK for LLM providers: one canonical API for chat, embeddings, moderation, rerank, image generation, audio transcription, and text-to-speech.

This is a provider SDK, not an agent framework or proxy server. It owns provider wire-format differences so your application does not have to. It does not include credential pools, budget routing, plugin loading, vector stores, MCP clients, observability sinks, or secret managers.

Installation

Add the library coordinate to deps.edn:

{:deps {net.clojars.deadmeme5441/clojure-llm-sdk {:mvn/version "0.6.0"}}}

Or with Leiningen / project.clj:

[net.clojars.deadmeme5441/clojure-llm-sdk "0.6.0"]

Upgrading to 0.6.0: review the migration notes for provider, streaming, registry, and model-resolution changes. Upgrading from 0.4.x also requires the 0.5.0 migration.

Then require the public namespace:

(require '[llm.sdk :as sdk])

Quick Start

(sdk/complete
  :openai
  {:request/model "gpt-4o-mini"
   :request/messages [{:message/role :user
                       :message/content "Reply with the single word: ok"}]})

Responses use the same canonical shape across providers:

{:response/provider :openai
 :response/model "gpt-4o-mini"
 :response/parts [{:part/type :text
                   :text "ok"}]
 :response/finish-reason :stop
 :response/usage {...}       ; optional; reported totals remain authoritative
 :response/cost {...}        ; optional; provider-reported cost wins
 :response/cache {...}       ; hit/miss/unknown when stamped
 :response/provider-data {...}
 :response/raw {...}}

Streaming uses the same request shape:

(sdk/complete
  :openai
  {:request/model "gpt-4o-mini"
   :request/messages [{:message/role :user
                       :message/content "Count to three"}]}
  :stream? true
  :on-event (fn [event]
              (when (= :stream/content-delta (:event/type event))
                (print (:event/delta event)))))

Without :on-event, streaming returns an llm.sdk.StreamHandle: a Seqable, Closeable, reducible stream owner. Use with-open when consuming it as a sequence:

(with-open [events (sdk/complete
                     :openai
                     {:request/model "gpt-4o-mini"
                      :request/messages
                      [{:message/role :user
                        :message/content "Count to three"}]}
                     :stream? true)]
  (doseq [event events]
    (when (= :stream/content-delta (:event/type event))
      (print (:event/delta event)))))

reduce also closes the stream, including on early reduced termination. With :on-event, the callback runs incrementally and sdk/complete returns the accumulated canonical response after the stream ends.

The handle is single-pass and single-consumer: seq and reduce advance the same cursor, and consumed events are released rather than retained for replay. Retain any events your application needs later; do not consume one handle concurrently.

Documentation

Library docs:

  • Getting started - install, configure credentials, make chat requests, stream responses, and run tool-call turns.
  • API reference - public functions and request shapes for each modality.
  • Providers - supported provider ids, modalities, credentials, and provider-specific notes.
  • Provider configuration - credentials, Kimi Code, Vertex, Azure deployments, custom aliases, and live model listing.
  • Canonical response contract - response parts, provider replay state, usage, cost, cache, and streaming semantics.
  • Context caching - provider-native cache markers and honest cache attribution.
  • Model registry and pricing - model metadata lookup, live refresh, overrides, and cost estimation.
  • Architecture - provider SDK boundaries and extension points.
  • Python LiteLLM design survey - library-level ideas borrowed from the Python project and intentionally excluded proxy concerns.
  • Provider parity ledger - constructor-backed coverage against Python LiteLLM plus the scoped comparison with the separate unravel-team/litellm-clj library.

Project docs:

Supported Surface

The public API exposes these canonical surfaces:

ModalityPublic functionProviders
Chatsdk/completeOpenAI, Anthropic, Anthropic on Vertex, Gemini, Vertex Gemini, Z.AI, OpenRouter, Codex, DeepSeek, Kimi, Kimi Code, Mistral, Groq, Cerebras, Together, xAI, HuggingFace Router, Perplexity, Bedrock, Ollama, and aggregator aliases
Embeddingssdk/embedOpenAI, Gemini Native, OpenRouter, Azure OpenAI deployment profiles, Cohere, Voyage, Mistral, Together, Jina, Nebius, and Ollama
Moderationsdk/moderateOpenAI
Reranksdk/rerankCohere, Voyage, Jina
Image generationsdk/generate-imageOpenAI, OpenRouter, Vertex Gemini image generation (:vertex-imagen), and Bedrock image models
Audio transcriptionsdk/transcribeOpenAI Whisper, Groq Whisper
Text-to-speechsdk/speakOpenAI TTS, ElevenLabs

See Providers for the full provider matrix and credential list. These rows describe implemented constructors, not a promise that every catalog model is currently served or enabled for an account or region.

Offline model and pricing lookup merges bundled LiteLLM and models.dev snapshots. Entries retain source provenance and, when available, revision, freshness, availability, all contributing sources, and the pricing source. Missing usage or rates remain unknown rather than becoming zero or borrowing a rate from another modality. Live listing is available only for providers with a supported model-list endpoint and reports that endpoint's account-visible view; a failed refresh retains the previous live slice and marks it stale.

OpenAI, OpenRouter, and Bedrock image calls require an explicit :image/model; none of these providers selects a billable image model implicitly.

Credentials

Credentials are read from environment variables. The SDK does not load .env files itself.

export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-api03-...
export GEMINI_API_KEY=AIza...
export ZAI_API_KEY=...
export KIMI_API_KEY=...

Use .env.example as a non-secret template for local live smoke tests.

Some provider names are intentionally distinct:

  • :kimi uses Moonshot's public API and reads MOONSHOT_API_KEY.
  • :kimi-code uses Kimi Code's coding endpoint and reads KIMI_API_KEY.
  • :vertex-gemini uses Google Application Default Credentials or GOOGLE_OAUTH_ACCESS_TOKEN.
  • :zai uses Z.AI's OpenAI-compatible GLM endpoint and reads ZAI_API_KEY.
  • :vertex-anthropic serves Claude models through Google Vertex AI using the same GCP credentials as :vertex-gemini, not an ANTHROPIC_API_KEY.
  • :codex-backend uses ChatGPT OAuth over HTTP/SSE (default) or Responses WebSocket V2 (:config {:transport :websocket}). Managed Codex CLI file credentials refresh automatically; caller-managed :auth-token and :account-id bypass file storage. See OAuth configuration and live verification. :openai and API-key :codex remain separate.

Applications that manage secrets outside environment variables can pass per-call runtime config:

(sdk/complete :openai request
              :config {:api-key "sk-..."
                       :base-url "https://api.openai.com/v1"
                       :timeout-ms 60000})

Design Guarantees

  • Canonical response parts preserve text, images, tool calls, citations, reasoning, safety data, and provider-native state.
  • Provider-specific replay data is not flattened away. Thinking signatures, encrypted reasoning items, Gemini thought signatures, citations, and tool-call ids stay available for later turns.
  • Unreported usage remains absent. Unattributable cost stays absent or :unknown, never zero; reported token totals and costs remain authoritative.
  • All modalities share the same pooled default HTTP client and accept an injected :http-client plus connect and request timeouts.
  • Default tests are offline-only. Live provider calls are opt-in.
  • Custom OpenAI-compatible providers can be registered without writing a new transport; complete profiles are validated and operation capabilities are derived from their constructors.

Examples

Embeddings:

(sdk/embed
  :openai
  {:embed/model "text-embedding-3-small"
   :embed/inputs ["clojure" "lisp" "java"]})

Native Gemini embeddings use the existing :gemini-native profile:

(sdk/embed
  :gemini-native
  {:embed/model "gemini-embedding-001"
   :embed/inputs ["clojure" "lisp" "java"]
   :embed/dimensions 256
   :embed/provider-options {:task-type :retrieval-document}})

Rerank:

(sdk/rerank
  :cohere
  {:rerank/model "rerank-english-v3.0"
   :rerank/query "lisp dialect on the JVM"
   :rerank/documents ["Python" "Clojure" "JavaScript"]
   :rerank/top-n 3})

Fallbacks:

(sdk/with-fallbacks
  [[:openai "gpt-4o"]
   [:anthropic "claude-haiku-4-5"]
   [:groq "llama-3.1-8b-instant"]]
  {:request/messages [{:message/role :user
                       :message/content "Reply: ok"}]})

Custom OpenAI-compatible alias:

(require '[llm.sdk.providers.openai.chat :as openai-chat])

(openai-chat/register-alias!
  {:id :my-private-llm
   :base-url "https://llm.example.com/v1"
   :env-var-names ["MY_PRIVATE_LLM_KEY"]
   :capabilities #{:chat :streaming :tools}})

Provider-family namespaces such as llm.sdk.providers.openai.chat, llm.sdk.providers.anthropic.chat, llm.sdk.providers.gemini.embeddings, llm.sdk.providers.zai.chat, and llm.sdk.providers.openrouter.chat are the sole provider implementations. The former flat namespaces have been removed.

Validation

The public CI workflow runs the offline checks on JDK 17 and 21:

clj-kondo --lint src test
clojure -M:test
python3 -m unittest discover -s test -p '*_test.py'
clojure -T:build jar

Release publishing uses one build task and one RELEASE_VERSION for the jar, POM, and deployment:

RELEASE_VERSION=x.y.z clojure -T:build deploy

Live tests are explicit:

cp .env.example .env
set -a; source .env; set +a
clojure -M:live-test

License

MIT

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close