AI types
The AI types are the values an app passes to and from a model: chat
messages and their roles, vectors and embeddings, and token counts.
AI covers the work you do with them: the ai capability and
its operations ai.generate, ai.stream, ai.embed and ai.embed_all,
with their failures (The ai capability);
prompts as checked files; vectors stored in pgvector and the
nearest-neighbour query; budgets; personal data sent to a model; and the
fake that tests answer from.
The models themselves come from provider packages (External services). Jadpo renders the prompt from a checked file and decodes the answer into a type, as it renders an email and hands it to a sender. Training a model is left out: a package or an outside service trains it, and your app calls the result. This section describes the types themselves.
Messages
ChatRoleis a standard enum of a message's four roles,system,user,assistantandtool. It is written and shown as its variant's name in every locale, as every provider's API and a transcript write arole.ChatMessageis itsroleand itscontent, any text: the shape OpenAI's, Anthropic's and Gemini's APIs share,{"role": "user", "content": "..."}. A message's name, a tool call's id, and content parts (images, tool calls and results as blocks) aren't part of it, and a member for any of them is refused; AI leaves tool calling and content parts out. It is stored as atextcolumn each, and shown after its role:user: Which soups are vegan?.- A message is personal data,
health, whatever field holds it (Personal data). What a person writes to a model, or is written back, is free text that can hold anything, special category details included. So it takes the sensitive category that fits such text: it is never cached, and every read is recorded.
Vectors
An embedding is a list of numbers a model makes from a text, so that
texts with similar meanings have nearby vectors. A Vector holds one.
Its items are single-precision floats, as pgvector's vector column
stores them and as models make embeddings, so storing one loses nothing.
Each item is held as the decimal that is the nearest float to the number
it was built or read from, rounded half to even as IEEE 754 rounds, in
its shortest form.
- Building one from
0.123456789keeps0.12345679, asscale:keeps a decimal to its places, and a vector never breaksformat. - A JSON number is rounded once, from its text as written to the nearest
float. So a float a client wrote out as a double
(
-0.006929283495992422) is that float again (-0.0069292835), and a vector written and read back is the vector it was. - A decimal holds 38 places, so an item below
1e-30is held as 0. That is far below a float's precision beside an embedding's other items. An item of1e38or more is refused, asmax(orminbelow 0) at its place in the array. - JSON has no NaN or infinity, so neither can be read.
A vector is a list. To code it is a List<Decimal>, so list.sum
and the other list operations take it. It has 1 to 16,000 items, the
most pgvector allows in a vector column. It is built from a list,
Vector([0.5, -0.25]). It is sent as a JSON array of numbers, never as
decimals in text as other Decimals are, because every provider sends
one that way. It is shown in the locale's digits in brackets, joined by
; , as a line's points are: [0.5; -0.25; 1]. == compares its items
in order. It isn't ordered.
dimensions: fixes a vector type's length:
type ArticleVector = Vector { dimensions: 1536 }, a whole number from 1
to 16,000. It goes only on a type built on Vector; elsewhere it is
type.constraint_base.
- A vector of another length breaks
dimensions, a rule named for its setting, asfromandtoare. It is checked first, so where it decides the length it settlesmin_lengthandmax_lengthtoo. - It is proved for a list written out with that many items, or a value of
a type with the same
dimensions:. JSON,json.readand OpenAPI (minItemsandmaxItems) check it, and the API record writes it. - A plain
Vectorhas any length, so anEmbeddingcan hold any model's.min_lengthandmax_lengthnarrow a vector type as they narrow a list. - A type built on a vector type repeats its
dimensions:or leaves it out. One outside its lengths, or a different one, istype.constraint_contradiction, as a differentscale:is.
A vector type also holds its items to what its distance: and its index
need: not all zeros when compared by cosine, breaking distance, and
within ±65,504 when indexed at half precision, breaking min and max
(Storing a vector and
The vector index).
No text form. A vector, an embedding, a usage and a message have no
text form: text.from and text.parse on one are type.mismatch, as on
a PostalAddress. A vector's JSON array is its one form, and it is also
pgvector's text.
Embeddings and token counts
Embeddingis itsmodel, aModelId, and itsvector, a plainVector. It is sent as named fields, its model asModelId's text, and shown as its model, then its vector.TokenCountis a whole number from 0 to JavaScript's largest safe integer, asUnitQuantity's count is, so a period's totals fit too. It is sent as a JSON number, stored in abigint, and shown in the locale's digits with no unit, since "1 tokens" needs a plural form CLDR doesn't name. It is ordered.TokenUsageis a call'sinput,outputandcachedtokens, the lastnonewhere a provider doesn't say.cachedcounts the input tokens the provider's cache served, withininput, as OpenAI and Gemini count them, so it is at or belowinput: its rulecached_within_input. A provider package that counts them apart, such as Anthropic's, adds them in.- A usage's total is
input + output, worked out where it is needed and never kept, so it can't disagree. Atotalmember is refused, as any unknown field is. A usage is sent as named fields,cachedasnullwhen it isnone, stored as abigintcolumn each, and shown by labels, the same in every locale, as projected coordinates are:input 1,200, output 350, cached 800.
Vectors, embeddings and counts aren't personal data on their own. Mark an embedding of a person's text with that text's category.
Stored and matched
Stored: a vector is one pgvector vector(n) column (Storage::Vector),
and an embedding is three columns, its model's two and its vector
(Storing a vector). A count is one bigint
column. A usage and a message are stored as a column per part.
Shapes: none of these has a distinctive shape for
type.standard_exists. A { role, content } or an { input, output } is
too common to flag.
Here a help article's embedding is three numbers, where a real model's are 1,536 or more. A list of the wrong length is refused by name:
type ModelReply = Text {}type EmbeddingUnreadable = Failure { kind: FailureKind.invalid_value}/// A help article's embedding, as the support site's model makes it.type ArticleVector = Vector { dimensions: 3}/// A help article's embedding from the model's reply: JSON that isn't one/// at all, or a vector of another length.function article_vector(model_reply: ModelReply) fails EmbeddingUnreadable -> ArticleVector { return attempt json.read(model_reply, as: ArticleVector) { malformed: EmbeddingUnreadable mismatch: EmbeddingUnreadable }}
test "an embedding has its model's length" { var article_meaning: ArticleVector = attempt article_vector("[0.5,-0.25,1]") assert article_meaning == ArticleVector([0.5, -0.25, 1.0]) assert article_vector("[0.5,-0.25]") fails EmbeddingUnreadable assert article_vector("[0.5,-0.25,1e39]") fails EmbeddingUnreadable}
What is built: AI says which parts of AI work are built.
Markdown for agents: /docs/ai-types.md.Built from Jadpo main at a707efb.