# Make your model OpenDXP compatible

> A ten-minute guide: package a System One model so any engine runs it without your code, prove it with a conformance file, and publish it to earn the OpenDXP compatible badge.

Source: https://systemonemodels.tech/docs/opendxp-compatible

Your model already answers the System One request: a state, typed questions, a
calibrated probability for every option. Packaged for
[OpenDXP](https://systemonemodels.tech/docs/opendxp), it also runs without your code: on a laptop with
`opendxp run`, behind an HTTP API with `opendxp serve`, as a tool for AI
agents with `opendxp mcp`, and in the System One Engine behind the live
playgrounds here. This guide takes about ten minutes to read; packaging a model
takes an afternoon. Publish the package with your model, and once it passes its
check, your model and your profile show **OpenDXP compatible**.

## What you will have

- A folder, `odxp/`, with an `odxp.json` manifest naming every file and its
  SHA-256: the weights, the tokenizer, the input layout as data, the
  calibration, and a conformance file.
- A conformance file written by **your own code**: what your model answers on a
  fixed set of requests.
- A check report from the reference runtime. When it passes, your model is
  **OpenDXP compatible**, and its page here says so.

## 1. Pick the profile

| Your model | Profile | Weights | Input described in |
| --- | --- | --- | --- |
| A bidirectional encoder that scores a marker token per option (Laya, Julia 1) | `encoder-markers` | ONNX | `template.json` |
| A language model that reads each option letter's probability at an answer slot (Decider) | `causal-letters` | GGUF | `prompt.json` |
| An instruction-tuned model asked in its chat template, option by option in rotation (AnyJev) | `causal-letters`, typed layouts (0.2) | GGUF | `prompt.json` |

If your model fits none of these, it can still be listed on System One Models
and run with its own code. Tell us how it reads its answer: that is what the
next version of the standard needs to know.

## 2. Export the weights

**ONNX (encoder-markers).** The graph takes `input_ids`, `attention_mask`,
`marker_positions`, `marker_mask` and `question_type`, returns
`option_logits`, and names its dynamic axes `batch`, `tokens` and
`options`; opset 17 or later. `opendxp.export.onnx_graph.export_graph` does
this for any torch module with that forward, and points the graph at your
`model.safetensors` as external data instead of copying it: the package adds
a few megabytes, and its weights hash is the one you published.

**GGUF (causal-letters).** Your GGUF file, unchanged. If you publish only
safetensors, convert them at the precision your own code runs, with llama.cpp's
`convert_hf_to_gguf.py` (F16 for BF16 weights). A quantised file such as
Q8_0 is a *derived* package: it is checked against the answers of the original
weights, and on a small model it usually changes a few decisions.

## 3. Describe the input as data

This is the heart of it. Nothing in a package is executed: no Python, no Jinja.
Templates are strings with placeholders such as `{instructions}`,
`{state}`, `{label}` and `{text}`, filled in one pass, so text from a
request is never expanded again.

- **`template.json`** (encoder-markers): the special tokens, how the head and
  each option are written, the token budgets, and what happens when a state is
  too long.
- **`prompt.json`** (causal-letters): the context and question pieces, the
  option labels (each a single token), the layouts by option count, how score
  questions are asked, and the llama.cpp settings that change the numbers
  (flash attention, the cache type). A typed prompt adds the chat template, as
  the text it renders to with `{system}` and `{user}` in place, and the
  rotation rule.

A noul question, written the way AnyJev asks it:

```json
"noul": {"layouts": [{"max_options": 2, "labels": ["No", "Yes"],
                      "head": "Question: {instructions}{legend}\nAnswer ",
                      "option": "{label}", "separator": " or ", "tail": "."}],
         "labels": "attached", "rotate": "cyclic", "listing": ["true", "false"]}
```

Test the description the way the reference tests do: build a few hundred random
requests, and require your template to give **the same token ids** as your own
prompt builder, row for row.

## 4. Declare the calibration

`calibration.json` holds the temperature your code applies, per question type
and optionally per number of options. Write the value your code *applies*: if
it clamps a trained temperature into a range, write the clamped value, and keep
the raw one under `source`.

## 5. Record your model's own answers

A native adapter is a small function that loads your model with your own
package and answers a System One request. With it, the conformance generator
runs the standard request set (52 requests, 91 questions, 11 languages, 2 to 20
options, a 6,687-character state) through your code on a CPU:

```bash
opendxp conformance generate odxp/ --native path/to/checkpoint --runtime yourmodel
```

The file records every probability, and every request your model refuses: an
engine must refuse it too.

## 6. Check it

```bash
opendxp validate odxp/     # schemas, parsers, coverage, hashes
opendxp check odxp/        # replays conformance.jsonl through the reference runtime
```

The check passes when every question gets the same decision and every
probability is within **0.01** of your own code's. When it does not, the cause
is almost always one of four things:

- **Tokenization at a boundary.** Encoding two pieces separately gives
  different ids from encoding them as one string. Describe which one your code
  does.
- **Backend settings.** On llama.cpp, switching flash attention off moved
  Decider's probabilities by up to 0.014; AnyJev passes only with an f32 cache.
  Declare the settings your own engine uses.
- **Calibration.** A clamp, or a temperature per option count, you forgot to
  declare.
- **Quantization.** A smaller weights file is a derived package; check it, but
  do not expect it to pass.

## 7. Publish it

Put the package in an `odxp/` folder next to your `systemone.yaml` and push
the version:

```bash
systemone push ./my-model --repo your-org/your-model --dry-run
systemone push ./my-model --repo your-org/your-model
```

The engine finds the package by its `odxp.json` and checks it against its
conformance file by itself (packages up to 2.5 GB; ask us for larger ones).
When it passes, **OpenDXP compatible** appears on the model's page and on your
profile or your organization's. For a live playground as well, open the
model's **Playground** tab and ask for one. Converters already exist for Laya, Julia 1, Decider and AnyJev
(`opendxp export`); for anything else, write to ceo@systemonemodels.tech.

## The badge

There is one OpenDXP badge, and it cannot be asked for: it is earned by
publishing. When a model you publish into your namespace (your username or your
organization) carries a package that passes its check, **OpenDXP compatible**
appears on the model's page, on its card in every list, and on your profile or
your organization's. If a later version's package fails its check, the model
shows **OpenDXP package** instead, with what failed.
