Jevaro accepts TypeSafe System One questions and returns an Arrow IPC stream.
Send the states as JSON, or as an Arrow IPC stream. States
and questions can include images.
The local default endpoint is POST http://127.0.0.1:8000/v1/systemone.
Send JSON with Content-Type: application/json:
{
"model": "jev-latest",
"states": ["Please refund the shoes.", "Where is my parcel?"],
"questions": {
"refund": {
"type": "noul",
"instructions": "Is a refund being requested?"
}
}
}| Field | Rule |
|---|---|
state |
One string, object, or array |
states |
A nonempty array of states; no configured element-count limit |
questions |
A nonempty map of string IDs to question definitions |
model |
Nonempty string; defaults to jev-latest |
rows_per_call |
Integer from 1 to 256; defaults to 1. Above 1, packs states into fewer upstream calls |
assets |
Map of references to images, as TypeSafe accepts them |
box_format |
typesafe (default) or geoarrow: how Box answers store their edges |
Supply exactly one of state and states. state: ["a", "b"] is one
evaluation; states: ["a", "b"] is two. An individual state cannot be a
number, boolean, or null. Unknown top-level fields are rejected.
Questions use the TypeSafe definitions:
type |
criteria |
|---|---|
choice |
Map of 1–255 labels to descriptions; a description may be null |
score |
Ordered array of 2–10 level descriptions |
noul |
Optional object describing true and false outcomes |
box |
None; a Box question has only instructions |
Instructions and descriptions can use strings, objects, or arrays. The proxy accepts omitted or null instructions, as the SDK question helpers do. Every state uses the same questions and model.
To send states as Arrow, POST multipart/form-data with two parts:
| Part | Content-Type | Contents |
|---|---|---|
request |
application/json |
questions, model, and optional state_column, rows_per_call, assets, and box_format |
states |
application/vnd.apache.arrow.stream |
An Arrow IPC stream with one row per state |
The request part takes the same fields as a JSON request, plus
state_column. With a states part, it rejects state and states. A part
may omit its Content-Type; states may also use application/octet-stream.
Any other part must be an image that assets
names. The response is the same Arrow stream as for a JSON request.
Each row is one state. These examples use a table of two support tickets:
id |
subject |
body |
|---|---|---|
| 101 | Shoes | Please refund the shoes. |
| 102 | Parcel | Where is my parcel? |
Without state_column, each state is an object of the row's columns, as
PyArrow's table.to_pylist() would produce. The two states are:
{"id": 101, "subject": "Shoes", "body": "Please refund the shoes."}
{"id": 102, "subject": "Parcel", "body": "Where is my parcel?"}Instructions can refer to a column by name, as in
"Does the customer's `body` ask for a refund?".
With "state_column": "body", that column's values are the states. The other
columns are not sent:
"Please refund the shoes."
"Where is my parcel?"To send some columns but not others, put them in a struct column and name it.
If ticket is a struct column with subject and body fields,
"state_column": "ticket" sends these states, and id stays out of them:
{"subject": "Shoes", "body": "Please refund the shoes."}
{"subject": "Parcel", "body": "Where is my parcel?"}A state column can hold strings, images, structs, lists, maps, or
arrow.json values. Each arrow.json value is parsed, so states can differ
in shape from row to row. A state column cannot contain nulls. A binary
value is an image. Answer rows follow input
rows, so you can join answers to the input by position.
Write the table as an Arrow IPC stream. With PyArrow:
import pyarrow as pa
tickets = pa.table({
"id": [101, 102],
"subject": ["Shoes", "Parcel"],
"body": ["Please refund the shoes.", "Where is my parcel?"],
})
with pa.ipc.new_stream("tickets.arrows", tickets.schema) as writer:
writer.write_table(tickets)Save the request fields as request.json. This one sends each body as a
state:
{
"questions": {
"refund": {"type": "noul", "instructions": "Is a refund being requested?"}
},
"state_column": "body"
}Send both parts:
curl --fail-with-body --no-buffer http://127.0.0.1:8000/v1/systemone \
-F 'request=<request.json;type=application/json' \
-F 'states=@tickets.arrows;type=application/vnd.apache.arrow.stream' \
--output http-results.arrowsTo send whole rows instead, remove state_column from request.json and
refer to the columns in the instructions.
| Arrow type | JSON value |
|---|---|
| String, large string, string view | String |
| Boolean, integer, float | Boolean or number; NaN and infinities are rejected |
| Date, time, timestamp, decimal | String from Arrow's cast, such as 2024-01-02 03:04:05.123456 or 12.50 |
| Struct | Object |
| List, large list, fixed-size list | Array |
| Map with string keys | Object; duplicate keys are rejected |
| Dictionary | Its decoded value |
arrow.json extension |
The parsed JSON value |
| Binary, large binary, binary view | An image, by its reference |
| Null | null |
A null inside a row becomes null. Other types, including fixed-size binary,
list views, durations, and unions, are rejected. Send the IPC stream format; the IPC file
format is rejected.
The server checks the API key before reading the body. It then reads the whole upload and converts every row once, so a bad row causes a 422 before streaming starts. The upload stays in memory as Arrow data. Rows are converted again, 1,024 at a time, or 32 at a time for a table with binary values, as upstream calls start.
TypeSafe calls images and Box questions
dreamer-ready: fit for prototypes, not yet
for production. To use them, send the TypeSafe-Danger-Zone-Images: true
header. Jevaro sends it to TypeSafe with each upstream call, and rejects a
request with images or Box questions that lacks it.
A Box question finds one thing in an image. It has only instructions:
{"mug": {"type": "box", "instructions": "the coffee mug in `photo`"}}Box needs Jev 1.14: send "model": "jev-preview" or "jev-1.14.0".
jev-latest still names Jev 1.13, and TypeSafe rejects a Box question for it.
Each answer row holds the box's four edges, as fractions of the image's height
(top, bottom) and width (left, right), and a confidence that may be
null:
{"box": {"top": 0.25, "bottom": 0.82, "left": 0.44, "right": 0.84}, "confidence": 0.92}With "box_format": "geoarrow", the edges are a geoarrow.box instead:
{"box": {"xmin": 0.44, "ymin": 0.25, "xmax": 0.84, "ymax": 0.82}, "confidence": 0.92}See the Arrow schema for both layouts.
assets maps each image's reference to the image, as in
TypeSafe's API.
Write a reference wherever its image belongs: in a state, in instructions,
or in criteria. This request finds the mug in two photos and compares each
with a picture of a particular mug:
{
"model": "jev-preview",
"states": [{"id": 1, "photo": "<kitchen>"}, {"id": 2, "photo": "<office>"}],
"assets": {
"<kitchen>": {"type": "image", "format": "jpg", "data_base64": "/9j/4AAQSkZJRg..."},
"<office>": {"type": "image", "format": "png", "data_base64": "iVBORw0KGgo..."},
"<my-mug>": {"type": "image", "format": "jpg", "data_base64": "/9j/4AAQSkZJRg..."}
},
"questions": {
"mug": {"type": "box", "instructions": "the coffee mug in `photo`"},
"mine": {"type": "noul", "instructions": "Is the mug in `photo` this one: <my-mug>?"}
}
}Each upstream call sends the images that its state and the questions refer to:
| Row | Upstream state |
Images sent |
|---|---|---|
| 0 | {"id": 1, "photo": "<kitchen>"} |
<kitchen>, <my-mug> |
| 1 | {"id": 2, "photo": "<office>"} |
<office>, <my-mug> |
Save the request as request.json and send it:
curl --fail-with-body --no-buffer http://127.0.0.1:8000/v1/systemone \
-H 'Content-Type: application/json' \
-H 'TypeSafe-Danger-Zone-Images: true' \
--data @request.json --output boxes.arrowsAn asset has "type": "image" or no type, and either:
data_base64: the file, base64-encoded with the standard alphabet and=padding, without whitespace or adata:prefix, andformat:png,jpg, orjpeg; orpart: in a multipart request, the name of the file part that holds the image.
Base64 makes each image about a third larger. To send image files as they are,
POST multipart/form-data: the JSON request in a request part, and each
image in its own part, named by its asset's part. These are the same
requests as above:
curl --fail-with-body --no-buffer http://127.0.0.1:8000/v1/systemone \
-H 'TypeSafe-Danger-Zone-Images: true' \
-F 'request={
"model": "jev-preview",
"states": [{"id": 1, "photo": "<kitchen>"}, {"id": 2, "photo": "<office>"}],
"assets": {"<kitchen>": {"part": "kitchen"}, "<office>": {"part": "office"}, "<my-mug>": {"part": "my-mug"}},
"questions": {
"mug": {"type": "box", "instructions": "the coffee mug in `photo`"},
"mine": {"type": "noul", "instructions": "Is the mug in `photo` this one: <my-mug>?"}
}
};type=application/json' \
-F 'kitchen=@kitchen.jpg;type=image/jpeg' \
-F 'office=@office.png;type=image/png' \
-F 'my-mug=@my-mug.jpg;type=image/jpeg' \
--output boxes.arrowsAn image part has Content-Type image/png, image/jpeg, image/jpg, or
application/octet-stream, or none. Each part belongs to exactly one asset.
The request part holds state or states, or the request can send
Arrow states in a states part.
In Arrow states, each binary value is an image file. Jevaro writes a new reference in its place and sends the image with that row's upstream call. Make a table of two photos:
from pathlib import Path
import pyarrow as pa
photos = pa.table({
"id": [1, 2],
"photo": [Path("kitchen.jpg").read_bytes(), Path("office.png").read_bytes()],
})
with pa.ipc.new_stream("photos.arrows", photos.schema) as writer:
writer.write_table(photos)Save the request fields as request.json:
{
"model": "jev-preview",
"questions": {"mug": {"type": "box", "instructions": "the coffee mug in `photo`"}}
}Send both parts:
curl --fail-with-body --no-buffer http://127.0.0.1:8000/v1/systemone \
-H 'TypeSafe-Danger-Zone-Images: true' \
-F 'request=<request.json;type=application/json' \
-F 'states=@photos.arrows;type=application/vnd.apache.arrow.stream' \
--output boxes.arrowsThe two states are:
{"id": 1, "photo": "<image:5d1c2a7e0b9f4c3d8e6a000000000000>"}
{"id": 2, "photo": "<image:5d1c2a7e0b9f4c3d8e6a000000000001>"}Each upstream call sends one image, under the reference in its state. Every
request uses a new random prefix. With "state_column": "photo", the state is
the image alone, so write instructions such as "the coffee mug".
Binary values work anywhere in a row. A list of binary values becomes an array
of references, and a binary field of a struct or a binary map value becomes a
reference in its object. A null stays null. Map keys can't be binary, since
JSON object keys are strings. A binary column with an Arrow extension type,
such as geoarrow.wkb, is rejected rather than read as images. A request can
also name images for its questions in assets and send them as file parts
beside the states part.
Images that the questions refer to go with every upstream call. Images that a
state refers to go only with that state's call. Jevaro finds references the
way TypeSafe does: a string that is exactly a reference, then references
inside longer strings, from left to right. Where several start at the same
place, such as img and img2, Jevaro matches the longest. TypeSafe matches
the one that assets lists first, so Jevaro lists each call's assets longest
first. Object keys, such as Choice options, are never references. Use
references that can't appear in your text by accident, such as <image:...>
or a UUID.
Every asset must be referenced somewhere, as TypeSafe requires. Jevaro doesn't compress the bodies of upstream calls with images, since image files are already compressed.
Jevaro returns 422 before any upstream call for:
- Images or Box questions without the
TypeSafe-Danger-Zone-Images: trueheader - Images or Box questions with
rows_per_callabove 1; packing would send each row's images again with every question - An asset that no state, instructions, or criteria refer to
- An asset without
partordata_base64,data_base64withoutformat, invalid base64, or an empty image - A part that an asset names but the request lacks, a part that two assets name, a part that no asset names, or an image part with another Content-Type
- A reference that is empty, only whitespace, or over 1,024 bytes, or the references of one call totaling over 65,536 bytes
- An empty binary value, or binary values under an Arrow extension type
Jevaro passes image files to TypeSafe unchanged. TypeSafe checks their contents: PNG or JPEG, at least 256 and at most 1,568 pixels on each side, and its other limits, which may change. An image that TypeSafe rejects ends the stream at its row, so prepare images before sending them.
By default, Jevaro makes one upstream call per state. With rows_per_call
above 1, each call evaluates up to that many states. Each state goes into its
own copy of every question's instructions, and the call's state is empty.
TypeSafe evaluates the questions in a call independently, so states can't
affect each other's answers.
This request:
{
"states": ["Please refund the shoes.", "Where is my parcel?"],
"questions": {"refund": {"type": "noul", "instructions": "Is a refund being requested?"}},
"rows_per_call": 2
}makes this single upstream call instead of two:
{
"model": "jev-latest",
"state": "",
"questions": {
"0.0": {"type": "noul", "instructions": {"state": "Please refund the shoes.", "question": "Is a refund being requested?"}},
"1.0": {"type": "noul", "instructions": {"state": "Where is my parcel?", "question": "Is a refund being requested?"}}
}
}The response is the same Arrow stream: one row per state, in input order.
- Object instructions keep their fields.
stateis added beside them. - Paths are rewritten. A backticked path that names part of the state is
rewritten in instructions and criteria. With the state
{"ticket": {"body": "Please refund the shoes."}}, the instruction"Does `ticket.body` ask for a refund?"becomes"Does `state.ticket.body` ask for a refund?". Choice options and the schema's Score legends keep their original text. - A call can hold fewer states than
rows_per_call. Each call is kept to about 48,000 estimated tokens. If TypeSafe still rejects one as too large, Jevaro splits it in two.
Throughput. One live test used three questions about short support messages (about 180 characters each):
| Setting | States per second |
|---|---|
| One state per call | 1,040 |
rows_per_call: 64, 100,000 states |
2,000 |
rows_per_call: 64, 10,000 states |
9,600 |
The sustained rate is set by TypeSafe's input token rate limit for the account. A short job runs faster while TypeSafe's burst allowance lasts. When TypeSafe returns 429, Jevaro lowers the number of calls in flight.
Tradeoffs.
-
Answers change slightly. Compared with one state per call, the differences were 2 to 4 times the variation between two unpacked runs.
- About 1.5% of Choice answers differed.
- A Score with levels 0 to 2 averaged about 0.03 lower.
The changes are consistent: packed answers don't depend on the batch size, the other states in a call, or a state's position. Don't mix packed and unpacked answers, and recheck any thresholds tuned on unpacked answers.
-
Cost depends on state length and question count. Every question carries a copy of its state.
- Short states with few questions use fewer input tokens, because calls share a fixed overhead of about 260 tokens: 28% fewer in the test above.
- Long states with many questions use more: 1.8 times as many for structured tickets with 6 questions. Under a token rate limit, they're also slower than one state per call once the burst allowance is spent.
Conflicts. Before any upstream call, Jevaro rejects three kinds of request with 422:
-
Images or Box questions. Packing would send each row's images again with every question, so send them one state per call.
-
A
statefield in instructions. For example, a question's instructions are{"state": "CA", "question": "Is this on the west coast?"}. Rename the field. -
A name shared by the instructions and the state. A question refers in backticks to a field of its instructions object, and a state has a top-level field with the same name. Take these instructions with the state
{"policy": "60-day returns"}:{"policy": "30-day returns", "question": "Is the request covered by `policy`?"}Once the state moves into the instructions,
`policy`would name only the instructions field. Rename that field.
The error names the question and, for a shared name, the row.
TypeSafe turns on experimental features with TypeSafe-Danger-Zone-*
request headers. Jevaro sends every such header that it receives, unchanged,
with each upstream call for that request. Apart from the
API key, it sends no other request headers upstream.
| Header | At TypeSafe |
|---|---|
TypeSafe-Danger-Zone-Images: true |
Turns on images and Box questions |
TypeSafe-Danger-Zone-Long-Context: true |
Raises the context length to 262k tokens per request and per question |
This request evaluates two long contracts with long context on:
curl --fail-with-body --no-buffer http://127.0.0.1:8000/v1/systemone \
-H 'Content-Type: application/json' \
-H 'TypeSafe-Danger-Zone-Long-Context: true' \
--data '{"states":["<first contract>","<second contract>"],"questions":{"termination":{"type":"noul","instructions":"Can either party end the agreement early?"}}}' \
--output contracts.arrowsTypeSafe says long context is slower and less accurate, so send the header
only with requests that need it. With rows_per_call, Jevaro still keeps each
packed call to about 48,000 estimated tokens.
Send Authorization: Bearer <TYPESAFE_API_KEY>, or let the server use its
TYPESAFE_API_KEY environment variable. A supplied header takes precedence.
The server forwards the selected key to TypeSafe.
See server configuration for shared-server access and environment settings.
A valid request starts a response with:
| Header | Value |
|---|---|
Content-Type |
application/vnd.apache.arrow.stream |
X-Jevaro-Row-Count |
Number of input states |
X-Accel-Buffering |
no |
There is no Content-Length. HTTP/1.1 uses chunked transfer.
After validating the whole request, Jevaro sends the schema and a zero-row batch before starting upstream calls. Each later batch holds the next row plus any later rows that have already finished. Columns follow the question order in the request; rows follow state order, even if upstream calls finish out of order. A slow earlier call delays later rows. The schema holds the shared answer metadata.
The full request is held in memory. Pending calls and results are bounded by the concurrency setting. Closing the HTTP connection cancels pending work.
| When | Result |
|---|---|
| Invalid request | HTTP 422 with a JSON detail field |
| Missing key or malformed bearer header | HTTP 401 with a JSON detail field |
| Upstream failure after streaming starts | Aborted HTTP body; status remains 200 |
An invalid upstream API key is detected after the stream starts. It therefore causes an aborted stream, rather than a new HTTP 401 response from Jevaro.
The server uses TypeSafe's default SDK retries, including 429 and 529 backoff, and lowers the number of calls in flight while TypeSafe returns 429 or 529. If retries fail, it stops the stream without null rows or JSON error records. The successful Arrow end marker is sent only on normal completion.
Both SDKs propagate read failures and reject a completed stream with the wrong row count. Treat the result as complete only after reading to the end without an error. A partial file may contain valid rows from before a failure.
With the server's API key set:
curl --fail-with-body --no-buffer http://127.0.0.1:8000/v1/systemone \
-H 'Content-Type: application/json' \
--data '{"states":["Please refund the shoes.","Where is my parcel?"],"questions":{"refund":{"type":"noul","instructions":"Is a refund being requested?"}}}' \
--output http-results.arrowsOn success, read the file with pyarrow.ipc.open_stream. If curl fails, the
output may be a JSON error or an incomplete stream.