---
title: XOR Provider Guide
description: An open-weights decision provider that also reads an image and audio and extracts typed values — calibrated judgments from Juspay's XOR, at any deployment or LiteLLM proxy route you configure
keywords: xor, decide, decision model, open weights, images, audio, value extraction, litellm, calibrated confidence, system one
---
# XOR Provider Guide
**A provider of `decide`** — the same typed `boolean` / `choice` /
`score` answers as [TypeSafe's Jev](typesafe.md) and [Laya](laya.md), plus
`value` extraction and a `choice` with an `unknown` option, from Juspay's
open-weights model, which also reads an image and an audio clip. It emits no
text at all.
---
## Overview
XOR is Juspay's Apache-2.0 decision model, post-trained from Qwen3.6-35B-A3B.
It is asked for by one of two aliases: `xor-latest`, the text model, and
`xor-omni`, the multimodal one, which also reads an image and audio. You send
one `state` plus named, typed questions, and optionally an image or a clip. XOR
answers every question in a single batched pass and returns a typed answer for
each; the result's `model` is the id that served the request (for example
`xor-text-1`), not the alias you asked for. NeuroLink calls whatever base URL
you configure — a deployment of the model, or a LiteLLM proxy with a route to
one. There is no built-in endpoint. The weights and setup instructions are at
[huggingface.co/juspay/xor](https://huggingface.co/juspay/xor).
### Key Facts
| | |
| --------------------- | --------------------------------------------------------------------------------------------- |
| Inference type | `decide` only |
| Model | `xor-latest` by default, `xor-omni` for media and `value`; `XOR_MODEL` sets your proxy's name |
| Question types | `boolean`, `choice` (optionally with `unknown`), `score` and `value` |
| Media | one image and one audio clip (first 30 seconds heard); no video; body at most 25 MB |
| Input window | 120,000 estimated tokens of state on `xor-latest`, 28,000 on `xor-omni` |
| Questions per request | 128 (`tryDecide()` splits more); a `choice` takes 2 to 64 options, a `score` 2 to 10 levels |
| Endpoint | `/v1/decisions`; the base URL is required, with no default |
| Precedence | used automatically only when TypeSafe and Laya are not configured |
---
## Quick Start
### 1. Get an endpoint and a key
Run a deployment of XOR (see
[huggingface.co/juspay/xor](https://huggingface.co/juspay/xor)), or use a
LiteLLM proxy with a route to one. NeuroLink posts to `/v1/decisions`.
The base URL is the origin of the deployment or of the proxy route, and a
trailing `/v1` is accepted. It must be `https://`; `http://` is accepted only for
`localhost`, `127.0.0.1` and `[::1]`, because every request carries the key and the
state. A deployment reachable only over plain http inside a network needs an
`https://` front, or a port-forward to `localhost`. On a LiteLLM proxy the key's team must be allowed
the model you ask for (`xor-latest` by default, `xor-omni` for media); otherwise the proxy answers 403
`team_model_access_denied`. A proxy may serve XOR under a name of its own: set
`XOR_MODEL` to that name.
### 2. Configure
Both the base URL and the key are required. Set them in the environment:
```bash
export XOR_BASE_URL=https://your-proxy.example.com # NeuroLink calls /v1/decisions
export XOR_API_KEY=sk-... # the key your endpoint accepts
export XOR_MODEL=xor-latest # optional; the default
```
or in the config passed to the SDK, exactly as for any other provider. Values
passed per call override the constructor's, which override the environment:
```typescript
const neurolink = new NeuroLink({
credentials: {
xor: {
baseURL: "https://your-proxy.example.com",
apiKey: process.env.MY_XOR_KEY,
},
},
});
```
### 3. Use it
```typescript
import { NeuroLink, readDecisionChoice } from "@juspay/neurolink";
const neurolink = new NeuroLink();
const result = await neurolink.decide({
provider: "xor",
state: "We were billed twice for March. Please refund it today.",
questions: {
team: {
type: "choice",
instructions: "Which team should handle this?",
criteria: {
billing: "Payments and refunds",
technical: "Bugs",
sales: "Pricing",
},
},
urgent: { type: "boolean", instructions: "Is this urgent?" },
},
});
const team = readDecisionChoice(result.answers, "team");
```
### From the CLI
The CLI reads the same environment variables:
```bash
neurolink decide "We were billed twice for March." --provider xor \
--questions '{"urgent":{"type":"boolean","instructions":"Is this urgent?"}}'
```
---
## Extracting values
A `value` question pulls one typed field out of the state. `dtype` is `string`
(the default), `int` or `float`, and an optional `pattern` is a regular
expression the extracted text must match. A field the state does not hold comes
back as `value: null` with `unknown: true`:
```typescript
import { NeuroLink, readDecisionValue } from "@juspay/neurolink";
const result = await new NeuroLink().decide({
provider: "xor",
model: "xor-omni",
state: "Invoice INV-2024-0892 for 18,275.50 rupees, due on 15 September.",
questions: {
amount: { type: "value", dtype: "float", instructions: "Invoice amount?" },
ref: {
type: "value",
instructions: "Invoice reference?",
pattern: "INV-[0-9]{4}-[0-9]{4}",
},
phone: { type: "value", instructions: "Customer phone number?" },
},
});
readDecisionValue(result.answers, "amount"); // { value: 18275.5, confidence: 0.99, unknown: false }
readDecisionValue(result.answers, "phone"); // { value: null, confidence: 0.82, unknown: true }
```
`xor-omni` answers `value`. When nobody chose a model — the call names none and
`XOR_MODEL` is unset — a request with a `value` question goes to `xor-omni`
directly and is checked against its 28,000-token window, since `xor-latest`
cannot answer it (a LiteLLM gateway would hand it to `xor-omni` behind your back,
past a window NeuroLink never checked). A model you name is always the one sent:
asked of `xor-latest` explicitly, a gateway with that fallback still answers it,
and a deployment without one refuses it with a 422 that arrives as
`invalid_request`. Every other decision provider refuses a `value` question
before any request.
A `choice` with `unknown: true` adds an option named `unknown`, picked when none
of the criteria fits; the answer then carries `unknown: true`. The 64-option cap
counts it, and no option may itself be named `unknown`.
---
## Images, audio and video
XOR reads **one image** and **one audio clip** alongside the `state`, through the
`xor-omni` model. A request carrying either goes there by itself when nobody
chose a model, as a `value` question does; name it (`model: "xor-omni"`, or
`--model xor-omni` on the CLI) if `XOR_MODEL` points elsewhere. It does **not**
read video: the gateway drops a
video and the model answers without it (a solid red clip was answered exactly as
garbage bytes were), so NeuroLink refuses one before any request rather than
return a blind answer. TypeSafe, Laya, [SubQ](subq.md) and
[ScaleDown](scaledown.md) do not read media, and [Perplexity](perplexity-decider.md),
[Cloudflare Clef](cloudflare-clef.md) and [OpenAI Decisions](openai-decisions.md)
read images only.
```typescript
import { readFile } from "node:fs/promises";
import { NeuroLink, readDecisionChoice } from "@juspay/neurolink";
const neurolink = new NeuroLink();
// One image: a file path, a Buffer or a data: URL
const photo = await neurolink.decide({
provider: "xor",
model: "xor-omni",
state: "A product photo from a listing.",
images: [await readFile("./front.jpg")],
questions: {
color: {
type: "choice",
instructions: "What color is the product?",
criteria: { red: "Mostly red", blue: "Mostly blue" },
},
},
});
const color = readDecisionChoice(photo.answers, "color");
console.log(photo.mediaBytes); // encoded size of the media sent
// An audio clip; the state may be empty
const call = await neurolink.decide({
provider: "xor",
model: "xor-omni",
state: "",
audio: "./call.wav",
questions: {
language: {
type: "choice",
instructions: "Which language is spoken?",
criteria: { english: "English", hindi: "Hindi" },
},
},
});
```
From the CLI, pass `--image ` or `--audio `; with `--audio` the state
may be left out:
```bash
neurolink decide --provider xor --model xor-omni --audio ./call.wav \
--questions '{"language":{"type":"choice","instructions":"Which language is spoken?","criteria":{"english":"English","hindi":"Hindi"}}}'
```
The rules:
- **One image and one audio clip** per request, in `images` and `audio`; a
second image is refused before any request.
- **Each is a Buffer, a local file path or a `data:` URL.** An `http(s)` URL is
refused: NeuroLink does not fetch media for you.
- **The type comes from the bytes, not the file extension**: PNG, JPEG, WebP
and GIF images, and WAV, FLAC, Ogg (Vorbis or Opus) and MP3 audio, each of
which was heard with speech in testing (English identified at 0.9999). An image
goes to XOR as a `data:` URL and a clip as `{ data, format }`. An image costs
about 600 input tokens whatever its resolution.
- **The model hears the first 30 seconds of a clip**, at about 25 input tokens a
second, and drops the rest without a word. A WAV, FLAC or Ogg (Opus or Vorbis)
clip that runs longer is refused before any request, its length read from the
file; an MP3's length is not read, so only its first 30 seconds count.
**AAC/M4A is refused**: it was
measured decoding to nothing while the request was still answered. A WAV
without a complete `fmt` and `data` chunk is refused as well.
- **The whole request body may be at most 25 MB** (the server's own cap is
24 MiB). The limit applies to the encoded body, so the base64 form counts. A
file over the limit is refused from its size, before it is read.
- **What can be checked locally is refused before any request**, as a
non-retryable `invalid_request`: a missing file, a directory, an empty Buffer
or file, bytes that are not a recognised image or clip, a remote URL or any
other URL scheme, more than one image, a video, a clip that is too long, or a
body over 25 MB. An image or clip with the right signature that the model
cannot decode is sent, and the server's refusal (a 502 wrapping the backend's 400) comes back as `invalid_request`, not retried.
- **The result carries `mediaBytes`**, the encoded size of the media sent. The
`decide` spans carry `decision.images.count`, `decision.audio.count` and
`decision.media.bytes`, and
never any base64.
---
## When NeuroLink uses it
Every built-in consumer of `decide` — model routing, relevance-driven
compaction, tool routing and RAG planning — asks for the default decision
provider. That is the first one that is configured, in the environment or in the
`credentials` passed to the SDK, in the order TypeSafe, Laya, XOR,
[SubQ](subq.md), [Perplexity](perplexity-decider.md), [Cloudflare Clef](cloudflare-clef.md),
[OpenAI Decisions](openai-decisions.md), then [ScaleDown](scaledown.md). TypeSafe counts with either of its keys,
`TYPESAFE_API_KEY` or `AI_GATEWAY_API_KEY`. Laya and XOR each count only with
both their key and their base URL. SubQ counts with its key alone. Perplexity
counts with its key alone, which is shared with Perplexity's text provider. Cloudflare Clef counts only with both
`CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID`, the same two variables the
Workers AI text provider reads. OpenAI Decisions counts with its key alone,
`OPENAI_API_KEY`, which the OpenAI chat provider reads too. ScaleDown counts with
its key alone, `SCALEDOWN_API_KEY`, which no other provider reads. A caller can
always name XOR with `provider: "xor"`. So:
- **A TypeSafe key, or Laya's key and base URL, plus XOR's key and base URL:**
built-in features use TypeSafe or Laya, in that order; XOR runs only where a
caller asks for `provider: "xor"`.
- **XOR's key and base URL, plus `SUBQ_API_KEY`:** built-in features use XOR;
SubQ runs only where a caller asks for `provider: "subq"`.
- **XOR's key and base URL, plus `SCALEDOWN_API_KEY`:** built-in features use XOR;
ScaleDown runs only where a caller asks for `provider: "scaledown"`.
- **XOR's key and base URL, plus a Perplexity key:** built-in features use XOR;
Perplexity runs only where a caller asks for
`provider: "perplexity-decider"`.
- **XOR's key and base URL, plus `CLOUDFLARE_API_KEY` and
`CLOUDFLARE_ACCOUNT_ID`:** built-in features use XOR; Cloudflare Clef runs
only where a caller asks for `provider: "cloudflare-clef"`.
- **Only XOR's key and base URL:** built-in features use XOR.
- **An XOR key with no base URL:** XOR is not configured. Built-in features
ignore it, and `provider: "xor"` fails with `XOR requires a base URL`.
- **None of TypeSafe, Laya, XOR, SubQ, Perplexity, Cloudflare Clef, OpenAI
Decisions or ScaleDown:** everything behaves exactly as it did without a decision model.
---
## Limits
The figures below were measured through a LiteLLM gateway in October 2026.
**Each model has its own window.** `xor-latest` read a needle at the end of
120,076 input tokens and refused 200,076 as longer than its 131,072-token
context; `xor-omni` refuses past 32,768. NeuroLink allows 120,000 estimated
tokens of state on `xor-latest` and 28,000 on `xor-omni` (and on any other model
name), leaving room for the questions, an image and a clip, and refuses more
before any network call, with `max_tokens_exceeded`. The size is an estimate,
not XOR's tokenizer: about four characters per token for ASCII text, and one
token per character for other scripts. It over-counts English prose, and
under-counts dense JSON and digits, so a dense state estimated under the limit
can still be longer than the server's context.
A state near the limit can therefore still be refused by the server as too long.
A 413, or a 5xx other than 503 whose message says the context was too long,
arrives as `max_tokens_exceeded` and is not retried; every other reply is
classified as in the Errors table below. Built-in consumers treat a refusal as
"carry on as before".
**A proxy may cap parallel requests.** On that route the key allowed 5 at a
time. After a burst of rejected requests (403 and 422 replies), every request
was answered 429 for about half an hour. That order was observed; the rejected
requests were not confirmed as the cause. NeuroLink retries a 429 once.
**128 questions per request.** The gateway refuses more, so `decide()` refuses
a longer map before any request, and `tryDecide()` (what every built-in consumer
uses) splits it into batches of 128 and joins the answers. A `choice` takes 2 to
64 options, counting `unknown` (64 plus `unknown` crashed the server with a 500),
and a `score` 2 to 10 levels (the gateway refuses 11); past either cap the
request is refused before any request, as `invalid_request`. Score levels must
be strings, objects or lists: numbers are refused by the gateway. A level that is
not a string comes back in the answer's `legend` as its JSON text.
**The timeout grows with the state.** 5 seconds, plus 150 ms for every full
1,000 estimated tokens of state, because a 120,000-token state took about 18
seconds to answer. Override it per call with `timeoutMs`, or with `--timeout` on
the CLI.
---
## Errors
| Reply | Kind | Retried |
| ------------------------------------------------------------ | --------------------------------------------------------- | --------- |
| 401 | `authentication` — the provider instance stops retrying | no |
| 403 / 402 | `invalid_request` — fixable; the instance is not disabled | no |
| 413, or a 5xx other than 503 saying the context was exceeded | `max_tokens_exceeded` | no |
| a 5xx wrapping the backend's 4xx (`backend 400: …`) | `invalid_request` — an image or clip it could not open | no |
| any other 4xx (for example 422, 404) | `invalid_request` | no |
| 429 | `rate_limit` | yes, once |
| 503 | `overloaded` | yes, once |
| other 5xx | `server` | yes, once |
| state over the window (local) | `max_tokens_exceeded`, with no network call | no |
| unusable media, or a question it cannot answer (local) | `invalid_request`, with no network call | no |
| no base URL configured (local) | `invalid_request`, with no network call | no |
A 403 or 402 is deliberately not `authentication`. On a LiteLLM proxy a 403
means the key's team does not allow the model you asked for
(`team_model_access_denied`), and
a 402 means the team has no budget. An admin can fix either, so the provider
instance keeps working once it is fixed and is not disabled. A 401 does disable
the instance: it stops sending requests after the first rejection, because a bad
key does not fix itself.
"Unusable media" covers a missing file, a directory, an empty Buffer or file,
something that is not an image or a recognised clip, a remote URL, more than one
image, any video, a clip over 30 seconds or in AAC/M4A, and a request body over
25 MB. "A question it cannot answer" is a choice or score past its cap, or a
choice naming an option `unknown` beside `unknown: true`.
A proxy's error text can echo the key and the request. Before a message reaches
a log or an error, the provider removes the configured key, anything shaped like
a LiteLLM key, long hex runs, embedded `data:` URLs, the request content pydantic
quotes back (`input_value=…`) and LiteLLM's table of fallback model groups.
---
## Troubleshooting
- **`XOR requires a base URL`** — set `XOR_BASE_URL`, or pass
`credentials.xor.baseURL`. There is no default endpoint.
- **`The XOR base URL must not carry credentials…`** — the base URL has a user
name, a password, a query string or a fragment. Set it to the origin only, for
example `https://your-proxy.example.com`, and give the key through
`XOR_API_KEY` or `credentials.xor.apiKey`.
- **`The XOR base URL must start with https:// or http://`** — the value has no
scheme (for example `xor.internal:8080`) or uses another scheme. Add
`https://`, or `http://` for a server on this machine.
- **`The XOR base URL must use https:// unless it points at this machine`** — the
value is an `http://` URL whose host is not `localhost`, `127.0.0.1` or `[::1]`.
A plain-http request would send the key and the state unencrypted. Put an
`https://` front on the deployment, or reach it through a port-forward to
`localhost`.
- **`XOR answered with a redirect`** — the endpoint replied 301, 302, 307 or 308.
The provider does not follow it and does not retry. Set the base URL to the
address the redirect leads to.
- **`The XOR base URL is not a valid absolute URL`** — the value cannot be
parsed as a URL. Set it to an absolute origin.
- **`XOR requires an API key`** — set `XOR_API_KEY`, or pass
`credentials.xor.apiKey`.
- **A 401** — the endpoint rejected the key. The provider instance stops
retrying after a rejection, so fix the key and construct a new one.
- **A 403 with `team_model_access_denied`** — the LiteLLM proxy's team for this
key does not allow the model you asked for (`XOR_MODEL`, or `xor-latest` by
default; `xor-omni` for media). Add the model to the team; the instance works again without a
restart.
- **`max_tokens_exceeded`** — the state is over the model's window (120,000
estimated tokens on `xor-latest`, 28,000 on `xor-omni`), the request has more
than 128 questions, or the state, the questions and the media together are
more than the model's context. Shorten the state, or use `tryDecide()` to split
the questions.
- **`XOR cannot answer question "…"`** — a choice with more than 64 options
(counting `unknown`), a score with more than 10 levels, or a choice naming an
option `unknown` beside `unknown: true`.
- **`XOR does not accept video`** — see [Images, audio and video](#images-audio-and-video).
Send frames as images instead, one per request.
- **Images seem to be ignored** — a deployment started without
`OPENJEV_IMAGES=1` answers 200 and silently ignores images, and NeuroLink
cannot detect that. Send a red image and a blue image and check that the two
answers differ.
- **Built-in routing never uses XOR** — a TypeSafe key, or Laya's key and base
URL, is also set and takes precedence, or XOR has no base URL.
---
## See also
- [TypeSafe (Jev) Provider Guide](typesafe.md)
- [Laya Provider Guide](laya.md)
- [Perplexity Decisions Provider Guide](perplexity-decider.md)
- [Cloudflare Clef Provider Guide](cloudflare-clef.md)
- [SubQ Provider Guide](subq.md)
- [OpenAI Decisions Provider Guide](openai-decisions.md)
- [ScaleDown Provider Guide](scaledown.md)
- [The `decide` inference type](../../features/decide-inference-type.md)
- [XOR on Hugging Face](https://huggingface.co/juspay/xor)