OpenAI-compatible inference
Every project has an OpenAI-compatible API, so any OpenAI client or SDK can call its models by changing the base URL and the key. The model you call can be a hosted model of the project, a finished training job, or a catalog base model.
Endpoints
GET /api/projects/{owner_slug}/{project_slug}/openai/v1/models
POST /api/projects/{owner_slug}/{project_slug}/openai/v1/chat/completions
POST /api/projects/{owner_slug}/{project_slug}/openai/v1/completions
POST /api/projects/{owner_slug}/{project_slug}/openai/v1/embeddings
Point an OpenAI SDK at the project's base URL:
from openai import OpenAI
client = OpenAI(
base_url="https://reopenly.com/api/projects/<owner>/<project>/openai/v1",
api_key="<your ReOpenly API key>",
)
GET /models returns the hosted models you can call:
{
"object": "list",
"data": [
{
"id": "support-assistant",
"object": "model",
"created": 1764547200,
"owned_by": "reopenly"
}
]
}
Use a returned id as model. The list does not include base models or
training jobs: read GET /api/v1/base-models for the base catalog and address a
base model as <catalog-model-id>, and a finished job of a project you can
access as training-job#<job-id>.
Chat completions
curl "https://reopenly.com/api/projects/<owner>/<project>/openai/v1/chat/completions" \
-H "Authorization: Bearer $REOPENLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<catalog-model-id>",
"messages": [
{"role": "user", "content": "Give me one sentence about datasets."}
],
"max_tokens": 64,
"temperature": 0.2,
"stream": false
}'
The request supports model, messages, max_tokens, temperature,
top_p, reasoning_effort, function tools, stream, and
stream_options.include_usage. The response is an OpenAI chat completion with
choices, a finish reason, and usage:
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1764547200,
"model": "<catalog-model-id>",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "…"},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 18,
"completion_tokens": 12,
"total_tokens": 30,
"reasoning_tokens": 0,
"cached_tokens": 0
}
}
Token counts come from the model when it reports them. A count it does not
report is 0; do not read that as an estimate.
Defaults for omitted fields
| Field | Applied when omitted |
|---|---|
max_tokens | 1024 |
temperature | 1.0 |
top_p | 1.0 |
stream | false |
reasoning_effort | nothing: the model's own default |
GET /api/v1/base-models publishes the same table as an inference block
(defaults and capabilities), so a client can read it rather than hard-code
it. The playground starts from that block, so a prompt typed there and the same
prompt sent through this API are the same request.
reasoning_effort: "none" explicitly disables reasoning, while omitting the
field asks the model for whatever it does by default: they are different
requests. The explicit values are none, low and high; the playground's
"model default" sends no field at all.
Streaming
Set stream: true to receive text/event-stream frames. A successful stream
ends with a finish chunk followed by:
data: [DONE]
Ask for the standard final usage chunk when you need it:
{
"model": "support-assistant",
"messages": [{"role": "user", "content": "Summarize this."}],
"stream": true,
"stream_options": {"include_usage": true}
}
The usage frame has an empty choices array and a usage object. Not every
frame carries a role or content: tool-call deltas and the finish frame have
other shapes.
A request that fails before the stream starts gets the normal JSON error
response. If generation fails after 200 OK and the stream headers were sent,
the last frame is a JSON error envelope and the stream ends without [DONE].
See Errors and retries.
Legacy completions
POST /completions accepts a string or a list of strings in prompt and
returns one OpenAI text_completion. A list is joined with spaces and treated
as one prompt; it does not produce one choice per prompt, so send separate
requests when you need that. It supports model, prompt, max_tokens,
temperature, top_p and reasoning_effort, and does not stream.
Embeddings
POST /embeddings serves an embedding project's hosted embedding models.
curl "https://reopenly.com/api/projects/<owner>/<project>/openai/v1/embeddings" \
-H "Authorization: Bearer $REOPENLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "citation-embedding",
"input": ["A short passage to embed.", "A second passage."],
"encoding_format": "float",
"input_type": "document"
}'
The request takes a non-empty string or list of strings, float or base64
encoding (base64, which the official OpenAI Python SDK requests by default,
returns little-endian float32 bytes, base64-encoded), the model's vector size as
dimensions, and the optional input_type query or document. A trained
model with a query_instruction applies it to queries only.
Limits. Each input may be up to 512 tokens, each request up to two inputs,
and a request up to 512 tokens in total. A model you trained with
max_seq_length 1024 or 2048 accepts inputs up to that length and raises the
request token budget to the same limit. The two-input ceiling stays: one full
length input fills the budget, while two shorter inputs can fit together. A
model trained at 512 or less keeps 512. The input limit is separate from the
output size: the 0.6B base returns a 1,024-dimensional vector whatever the
input length. A request over these limits answers
400 invalid_request with error.param="input" and no Retry-After: split or
shorten it, because retrying it unchanged will not help.
A value of the wrong type or shape answers 422 with detail (unknown fields
are ignored); a well-formed but unsupported dimensions answers
400 invalid_request with error.param="dimensions". An inactive or unknown
model answers 404, and a project without embeddings answers 409.
By default an input over the limit is refused with 400, as OpenAI does. Set
truncate: true to cut each over-limit input to the limit instead: the end of
the text is removed, exactly as the models were trained, and the item is marked
"truncated": true. A truncated input fills the whole input budget, so send it
in a request of its own. The truncated field is present only when an input was
shortened; an omitted field means false. From the OpenAI Python SDK:
response = client.embeddings.create(
model="citation-embedding",
input=["A long document. " * 600],
extra_body={"truncate": True},
)
print(getattr(response.data[0], "truncated", False))
The response keeps input order:
{
"object": "list",
"data": [
{"object": "embedding", "embedding": [0.01, -0.02], "index": 0}
],
"model": "citation-embedding",
"usage": {"prompt_tokens": 12, "total_tokens": 12}
}
Timeouts and capacity. A model that has been idle can take a moment to
answer its first request, so set a client timeout of at least 60 seconds.
If the model cannot answer within that time, or more requests arrive at once
than it can take, the response is 503 capacity_unavailable with
Retry-After: wait that long and retry. Every response is checked before it is
returned (vector count, size, order and usage), and a request is charged at most
once, even when it is retried internally.
Embedding calls are never saved into a dataset. ReOpenly does not keep the request text or the returned vectors as training data: a vector cannot say which passage answered a query, so it would be half a training pair at best. To build training pairs from real searches, report them through Search feedback. To try a model interactively or rank a dataset with it, see Playground and Search.
Context limits
A chat request must fit the model's context: max_context_tokens from
GET /api/v1/base-models (4,096 for Qwen3.5 9B), counting both the prompt and
max_tokens. The check happens before the model runs, and a request that does
not fit answers invalid_request. Read the limit from the catalog rather than
from this page, and leave room for max_tokens.
Billing and retries
A request is charged only after the model has run it, and a failed request is
not charged. A successful request on a paid route is billed at least 100 milliseconds
at its frozen rate (0.000050 credits when the rate is 0.03 credits per GPU
minute). Retry only when the response is a retryable model_warming error with
Retry-After. An ambiguous_transport response may mean the request was
served, so a blind retry can duplicate work and billing.
The playground keeps the original conversation, performs at most three automatic retries
after the original request, and then leaves a manual Retry. It never retries
an error that arrives mid-stream. It stores the project in the page URL
(/app/playground?project=owner/project) and calls the same project API shown
above; there is no inference route outside a project.
Inference and fine-tuning are charged in 100 ms units, rounded up from measured active time. Credit amounts use six decimal places. A training run that never starts remains free.
Hosted models also lists the base models for every project. No fine-tuning is required: use ReOpenly/Qwen3.5-9B for text, or ReOpenly/Qwen3-Embedding-0.6B / ReOpenly/Qwen3-Embedding-4B for embeddings. Base calls require the project owner and an available serving route. Text calls respect inference auto-save; embedding vectors require explicit retrieval feedback to become training examples.