Skip to main content

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​

FieldApplied when omitted
max_tokens1024
temperature1.0
top_p1.0
streamfalse
reasoning_effortnothing: 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.