Classify images with Jev: image preprocessing, multipart requests, and reading the results

Translated from the original Traditional Chinese version.

Summary

Since Jev 1.13 and 1.14 opened up image input, you can shrink a photo to the size range the docs require, upload it as multipart, and ask one question, whether it is a photo of a cat, to get the probability that it is. Your code then applies a threshold to answer yes or no. All 10 camera photos were judged correctly on both versions, and the photo showing only a cat’s back got the lowest probability.

Table of contents

Since Jev 1.13 and 1.14 opened up image input, the decision model can judge what a picture contains. You shrink the photo to the size range the docs require, upload it as multipart, and ask one question, “Is this a photo of a cat?” Jev returns the probability that the photo is a cat photo, and your code applies a threshold to answer yes or no.

This post walks through the whole flow with 10 photos I took with a camera, 6 of them with a cat and 4 without. The example data comes from real API calls with jev-1.14.0 and jev-1.13.0 on 2026-10-18, one request per photo. The numbers illustrate the flow and do not represent accuracy. The basic use of question types and thresholds is in the earlier Jev API integration notes, so this post covers only what image input adds.

The question I asked is in Traditional Chinese: 是否為貓咪的照片? means “Is this a photo of a cat?” The probabilities below come from the runs with that Chinese question, and I did not rerun them with an English question. The code blocks keep the Chinese question as I sent it and add the English meaning in a comment or in the text next to it.

Request structure for image input

According to the official Images documentation as of 2026-10-18, image input is still experimental. The request format, limits, and accuracy can change between versions, and the docs currently recommend it only for prototypes and experiments. A request that contains an image has three more items than a text request:

Item Content
Header Every request that contains an image needs TypeSafe-Danger-Zone-Images: true. Without it the API returns 400
Assets The top-level assets lists each image keyed by Reference. In a multipart request, part names the Part that holds the image
Reference A string written in state, instructions, or criteria. The model reads the matching image at that position. The docs recommend generating a new UUID for every image in every request, so it cannot accidentally match ordinary text

You can send an image either as base64 inside the JSON or as the raw file with multipart/form-data. The docs recommend multipart first, because base64 makes each image about one third larger, and the added size counts toward the 32 MiB limit of each request.

The first Part of a multipart request is always named request and holds the JSON body. The image goes in another Part. In this example the image is in a Part named photo with the Content-Type image/jpeg, and the value of state is the Reference of this image. This is the content of request:

1{
2  "model": "jev-1.14.0",
3  "state": "3f6c2a90-8d4e-4b1a-9c7e-2e5d1b0a7f43",
4  "assets": {
5    "3f6c2a90-8d4e-4b1a-9c7e-2e5d1b0a7f43": { "type": "image", "part": "photo" }
6  },
7  "questions": {
8    "cat": { "type": "noul", "instructions": "是否為貓咪的照片?" }
9  }
10}

The instructions string is the Traditional Chinese question “Is this a photo of a cat?”

After sending the cat photo DSC05249, the response has the same format as for a text request, and the tokens used by the image are included in usage.input_tokens:

1{
2  "model": "jev-1.14.0",
3  "answers": { "cat": { "type": "noul", "noul": 0.96 } },
4  "usage": { "input_tokens": 4564, "output_tokens": 20 }
5}

Image preprocessing

The API accepts only static PNG or JPEG images and does not resize them. An image outside the allowed size makes the whole request fail. The official error table says 413, but when I sent a 1569x800 image, the API returned 400 with the message Invalid image dimensions., so your code should treat both status codes as an image that does not meet the spec. The official limits and how I handled my photos are in the table:

Limit Official spec How I handled my photos
Format Static PNG or JPEG Converted the camera’s raw ARW files to JPEG first
Width and height 280 to 1568 pixels per side The original is 6024x4024. Scaling the long side down to 1568 gives 1568x1047, and 1047x1568 for portrait photos
Total pixels 78,400 to 2,458,624 1568x1047 is about 1.64 million pixels
File size 16 MiB per image About 240 to 430 KB for the resized JPEG

The API strips metadata such as EXIF and does not apply the orientation information in it. A portrait photo that relies only on the EXIF flag for its orientation would reach the model lying on its side. So the preprocessing first rotates the pixels upright according to EXIF, then scales proportionally so the long side is 1568 pixels, and outputs a JPEG. Proportional scaling does not handle images that are too small. An image with any side below 280 pixels, such as a panorama with an extreme aspect ratio, needs to be enlarged or padded separately.

Results

These are the 10 test photos:

Thumbnails of the 10 test photos: 6 show cats, and the other 4 show a dead tree under a blue sky, a stone art installation on a coast, a phone and a memory card adapter on a desk, and a wooden Braille model in an art museum
This example asks “Is this a photo of a cat?” The noul value is the probability that the photo is a cat photo, and my code answers yes when noul is 0.5 or higher and no otherwise. The 1.14 and 1.13 columns in the table are the noul returned by each version:

Photo Content Correct answer 1.14 1.13
DSC05249 Calico cat lying on a bed Yes 0.96 0.99
DSC05257 Orange-and-white cat lying on its side by the bed Yes 0.95 0.99
DSC05393 Dead tree under a blue sky No 0.01 0.05
DSC05491 Stone art installation on a coast No 0.01 0.00
DSC07460 Close-up of an orange-and-white cat’s face Yes 0.96 0.99
DSC07466 Only the back of an orange cat Yes 0.78 0.64
DSC07469 Tabby-and-white cat lying on the floor Yes 0.96 0.99
DSC07480 Side view of a tabby-and-white cat’s face Yes 0.96 0.99
DSC07701 Phone and memory card adapter No 0.02 0.00
DSC07829 Wooden Braille model No 0.02 0.00

Both versions gave the correct answer for every photo. The 5 photos in which the cat is clearly in view all scored 0.95 or higher, the 4 photos without a cat all scored 0.05 or lower, and DSC07466, which shows only the back, scored 0.78 and 0.64. If you use the official starting point from the Jev API integration notes instead, which treats 0.7 and above as yes and 0.3 and below as no, DSC07466 counts as yes with 0.78 on 1.14, and its 0.64 on 1.13 falls in the middle band and goes to a person for review. When I sent the same set of photos to 1.14 a second time, every probability differed by 0.02 or less.

The official model changelog says Jev 1.14 improved multilingual support, and this example used a Chinese question. Each photo request costs about 4,565 tokens, which is about 0.0002 USD at the input price of 0.042 USD per million tokens on the Models page.

Backend call flow

The example is written in Python and uses Pillow to process images, and the flow is the same in other languages. The API key, model version, and endpoint are set up the same way as in the Jev API integration notes.

Create a function prepare_image to process the image. It takes an image path and returns the JPEG content, rotated upright and scaled down:

1def prepare_image(path):
2    with Image.open(path) as original:
3        image = ImageOps.exif_transpose(original)  # rotate pixels upright according to EXIF
4        image.thumbnail((1568, 1568))  # scale down proportionally, long side at most 1568
5        buffer = io.BytesIO()
6        image.convert("RGB").save(buffer, "JPEG", quality=90)  # quality is an example value
7    return buffer.getvalue()

Then create a function ask_is_cat to send the request. It generates a new UUID for the image as its Reference, then builds a multipart request from the request content above and the image returned by prepare_image:

1def ask_is_cat(client, path):
2    ref = str(uuid.uuid4())
3    request = {
4        "model": MODEL,
5        "state": ref,
6        "assets": {ref: {"type": "image", "part": "photo"}},
7        # The question is in Traditional Chinese, as tested: "Is this a photo of a cat?"
8        "questions": {"cat": {"type": "noul", "instructions": "是否為貓咪的照片?"}},
9    }
10    files = {
11        "request": (None, json.dumps(request, ensure_ascii=False), "application/json"),
12        "photo": (path.name, prepare_image(path), "image/jpeg"),
13    }
14    response = client.post(API_URL, files=files)
15    response.raise_for_status()
16    return response.json()

For retries on 429 or 529, you can reuse the approach of ask_jev from the earlier post. When the backend service starts, create one shared HTTP client and add the image-input header next to the Authorization header. In the example below, photos is a list of photo paths. It calls ask_is_cat once per photo and answers yes or no from noul:

1with httpx.Client(
2    headers={"Authorization": f"Bearer {API_KEY}", "TypeSafe-Danger-Zone-Images": "true"},
3    timeout=httpx.Timeout(60, connect=10),  # example values
4) as client:
5    for path in photos:
6        noul = ask_is_cat(client, path)["answers"]["cat"]["noul"]
7        print(path.stem, "yes" if noul >= 0.5 else "no")

Summary

Classifying images with Jev takes one Noul question, and the extra work compared with a text request is the image preprocessing and the multipart request format. All 10 photos were judged correctly on both versions, and DSC07466, which shows only the back, got the lowest probability. When you need to be more careful, you can send the middle band to a person for review.

PreviousWhetstone: Matt Pocock Skills rewritten for Claude CodeAINextJev API integration notes: request structure, backend call flow, and reading the resultsAI
Ted Liou

Ted Liou

Software engineer writing about Unity and AI development. Available for consulting.