An ordinary non-streaming sdk/complete call returns a map shaped like this;
the callback-based streaming form returns the same shape after accumulation.
Adapters fill in parts, optional usage, provider-data, and sometimes an
authoritative provider-reported cost. The SDK preserves an existing
:response/cost; only when it is absent does central stamping estimate cost
from canonical usage and known pricing. Cache stamping retains the same honest
unknown semantics across providers.
{:response/id "resp_..."
:response/provider :vertex-gemini
:response/model "gemini-2.5-pro"
:response/parts [<Part> ...]
:response/tool-calls [<ToolCallPart> ...] ; optional, only when non-empty
:response/finish-reason :stop ; :stop|:length|:tool-calls|:content-filter|:incomplete|:unknown
:response/usage {<Usage>} ; optional — every counter is optional
:response/cost {<Cost>} ; optional — reported or estimated when attributable
:response/cache {<Cache>} ; stamped by sdk/complete
:response/provider-data {...} ; provider-specific replay state
:response/raw {...}} ; provider response body, verbatim
The Malli schemas live in llm.sdk.schema (Response, Usage, Cost,
Cache).
Canonical parts retain opaque data that a provider requires on a later turn; canonicalization must not reduce that data to display text alone.
:reasoning/signature alongside
:reasoning/text and optional :reasoning/encrypted. During streaming,
:event/index identifies the reasoning block being updated and
:reasoning/signature is retained on that block in the final
:response/parts. The index is stream-local; the accumulated part keeps
the ordinary canonical reasoning shape.:tool-call/provider-data, including opaque
metadata needed to distinguish or replay a custom or provider-native tool
call. Metadata supplied by tool-call start events is merged across updates
and retained in both :response/tool-calls and the corresponding ordered
tool-call part.:citation/source-id or
:citation/provider-data, plus descriptive metadata such as
:citation/title, :citation/snippet, :citation/text-range,
:citation/date, :citation/last-updated, and :citation/source. A
citation must have at least one of :citation/url, :citation/source-id,
or :citation/provider-data.Top-level provider replay state remains in :response/provider-data. Callers
that construct a later assistant message must preserve replay-significant
canonical parts in :message/content or :message/tool-calls and carry
relevant response-wide state in :message/provider-data; otherwise a provider
may be unable to validate or replay the prior response.
Canonical usage is sparse. Token-metered responses may look like:
:response/usage
{:usage/input-tokens 123 ; OPTIONAL — uncached text/general input
:usage/output-tokens 45 ; OPTIONAL
:usage/total-tokens 168 ; OPTIONAL — authoritative when reported
:usage/cached-input-tokens 42 ; OPTIONAL — present iff reported
:usage/cache-write-tokens 8 ; OPTIONAL — same rule
:usage/reasoning-tokens 100 ; OPTIONAL — may overlap output
:usage/citation-tokens 12 ; OPTIONAL
:usage/search-queries 1 ; OPTIONAL — informational, e.g. Perplexity
:usage/image-tokens 20 ; OPTIONAL
:usage/audio-tokens 10 ; OPTIONAL
:usage/request-count 1 ; OPTIONAL
:usage/provider-raw {...}} ; OPTIONAL raw provider usage envelope
A provider may instead report only its billable unit. Cohere rerank uses the
ordinary Usage schema, while transcription responses accept the dedicated
duration form through TranscriptionUsage:
{:usage/search-units 1} ; Cohere rerank search
{:usage/duration-seconds 12.5} ; transcription audio duration
:usage/search-queries is distinct informational activity metadata. It is not
a substitute for Cohere's billable :usage/search-units.
Canonical response usage does not require input or output tokens. Unit-only usage is valid, and the SDK never inserts token counters merely to satisfy a common shape. The presence/absence rule applies to every field: absent means the provider did not report it, whereas an explicit zero means the provider reported zero.
StreamUsage is also sparse because providers can report cumulative counters
in separate events. Each new value is the latest cumulative value for that
key, not an increment to add to the previous event; missing keys retain their
earlier values.
A provider-reported :usage/total-tokens is authoritative. Canonical
:usage/input-tokens excludes tokens already represented by
:usage/cached-input-tokens or :usage/cache-write-tokens, so those cache
counters are not added again. Reasoning tokens may overlap output tokens and
are likewise not blindly added to a derived total.
:response/cost
{:cost/usd 0.0012M ; OR :unknown
:cost/estimated? true ; false for provider-reported cost
:cost/pricing-source "models-dev" ; registry tier or provider report
:cost/source-url "https://..." ; source URL when known
:cost/breakdown {:input-tokens 1000 ; canonical totals when present
:output-tokens 500
:text-input-tokens 800 ; routed counts when distinguishable
:image-input-tokens 200
:audio-input-tokens 0
:text-output-tokens 100
:image-output-tokens 400
:audio-output-tokens 0
:cached-input-tokens 50
:text-cached-input-tokens 0
:image-cached-input-tokens 50
:audio-cached-input-tokens 0
:search-units 1
:duration-seconds 12.5
:input-cost-per-million 2.5
:output-cost-per-million 10.0
:cache-read-cost-per-million 1.25
:image-input-cost-per-million 10.0
:image-output-cost-per-million 40.0
:audio-input-cost-per-million 32.0
:audio-output-cost-per-million 64.0
:image-cache-read-cost-per-million 2.5
:audio-cache-read-cost-per-million 8.0
:rerank-cost-per-search-unit 0.002
:transcription-cost-per-minute 0.006}}
The breakdown is sparse: it contains the usage dimensions and rates used for that result, not every possible field shown above. Existing specialized rates, such as per-image, per-megapixel, per-minute, per-character, request, and cache write rates, remain available where the catalog supplies them.
Precedence and honesty rules:
:response/cost is authoritative. This includes costs
reported by providers such as OpenRouter or Perplexity and streamed costs
carried on a usage event. Central stamping never replaces it with a registry
estimate.:cost/usd :unknown with :cost/reason, never 0 or 0M.:cost/estimated? is true for SDK calculations and false for a provider-
reported amount.:cost/pricing-source identifies the registry tier or provider report that
supplied the numbers. nil means no pricing source was found.Usage normalizers keep cache math non-overlapping: :usage/input-tokens is the
uncached input count, while :usage/cached-input-tokens and
:usage/cache-write-tokens are separate billable lines when reported.
:response/cache
{:cache/status :hit ; :hit | :miss | :unknown
:cache/cached-tokens 200 ; integer when known, otherwise :unknown
:cache/cache-write-tokens 50} ; integer when known, otherwise :unknown
Honesty rule:
:cache/status :hit — provider reported a positive cached-input-token count.:cache/status :miss — provider explicitly reported 0 cached tokens.:cache/status :unknown — provider did not report cache stats at all.:cache/cached-tokens is the keyword :unknown (not 0) when the
status is :unknown. This is the distinction between "provider said
0" and "provider was silent."The usage normalizers (llm.sdk.usage) include :usage/cached-input-tokens
only when the raw provider payload contained a cache field. That
absence is how :unknown propagates upward.
Three rules are applied without exception:
Never substitute 0 for an unknown usage or cache count. If the
provider did not surface a counter, the SDK leaves it absent. For cache
status this becomes :unknown, preserving the distinction from an explicit
zero and therefore :miss.
Never invent token usage for a unit-metered operation. Search units and audio duration stand on their own; absent input/output tokens stay absent. Informational search-query or request counts do not become search units.
Never substitute $0 for an unknown cost. If usage, pricing, or a rate
for a reported billable dimension is missing, cost stays absent or carries
:cost/usd :unknown. Callers aggregating spend do not silently undercount.
These rules are enforced in:
llm.sdk.usage normalizers, which omit fields the provider did not carry.llm.sdk.pricing/canonical-cost, which returns
{:cost/usd :unknown ...} when attributable pricing is incomplete.llm.sdk.pricing/canonical-cache, which returns
{:cache/status :unknown :cache/cached-tokens :unknown} when usage is silent.llm.sdk.pricing/stamp-response-cost-and-cache, which preserve an existing
authoritative :response/cost before considering estimation.The same canonical shapes are available outside the request path:
(sdk/canonical-cost :openai "gpt-4o"
{:usage/input-tokens 1000
:usage/output-tokens 500})
;; => {:cost/usd 0.0075M :cost/estimated? true :cost/pricing-source "models-dev" ...}
(sdk/canonical-cache {:usage/cached-input-tokens 0})
;; => {:cache/status :miss :cache/cached-tokens 0 :cache/cache-write-tokens :unknown}
(sdk/canonical-cache {})
;; => {:cache/status :unknown :cache/cached-tokens :unknown}
With :stream? true and no callback, sdk/complete returns a lazy sequence
of canonical stream events. Realizing that sequence performs the read. A
:stream/error is therefore an event value in this mode; merely consuming
the lazy stream does not turn that event into an exception.
With :stream? true :on-event cb, the SDK consumes the stream, calls cb for
each event, and then accumulates the events into the canonical response
returned by sdk/complete. The same accumulation is available through
llm.sdk.stream/events->response. If an accumulated stream contains a
:stream/error, accumulation throws ExceptionInfo rather than returning a
successful response. Its ex-data contains the classified :error, the
original :stream/error, the :provider, and a :partial-response containing
the canonical state accumulated from the event sequence.
This differs intentionally from the lazy event interface, where callers
observe and decide how to handle the error event themselves.
Indexed :stream/reasoning-delta events update independent reasoning blocks;
an optional :reasoning/signature survives on the resulting reasoning part.
Tool-call start and argument-delta events are reduced into both
:response/tool-calls and an ordered :part/type :tool-call entry in
:response/parts; optional :tool-call/provider-data survives that reduction.
Provider-state events are merged into :response/provider-data. Non-URL
citation events retain their source identifiers and provider metadata in
canonical citation parts.
Stream usage events follow the sparse cumulative rules described above. The
SDK emits exactly one terminal :stream/end, after any trailing usage or
provider metadata, so the final aggregate includes those trailers. There are
no separate cost or cache event types. Provider-reported cost may
accompany a usage event and remains authoritative on the aggregate response.
When no such cost exists, cost/cache stamping runs only after the complete
usage envelope is available.
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 |