One key, any OpenAI client.
LLMJob speaks the OpenAI chat-completions protocol. Point any SDK you already use at our base URL, pass your key as a bearer token, and your requests run on the GPUs in your own cluster — or on our shared hosted models while you get your nodes online.
Quickstart
Create a key on the dashboard, then make a request. The base
URL is https://llmjob-production.up.railway.app/v1.
curl https://llmjob-production.up.railway.app/v1/chat/completions \
-H "Authorization: Bearer $LLMJOB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "role": "user", "content": "Explain PPLNS in two sentences." }
]
}'
import os
from openai import OpenAI
client = OpenAI(
base_url="https://llmjob-production.up.railway.app/v1",
api_key=os.environ["LLMJOB_API_KEY"],
)
resp = client.chat.completions.create(
model="qwen/qwen3.8-27b",
messages=[
{"role": "user", "content": "Explain PPLNS in two sentences."}
],
)
print(resp.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://llmjob-production.up.railway.app/v1",
apiKey: process.env.LLMJOB_API_KEY,
});
const resp = await client.chat.completions.create({
model: "qwen/qwen3.8-27b",
messages: [
{ role: "user", content: "Explain PPLNS in two sentences." },
],
});
console.log(resp.choices[0].message.content);
The response is an ordinary OpenAI chat.completion object:
{
"id": "chatcmpl-8f1c…",
"object": "chat.completion",
"created": 1775001600,
"model": "qwen/qwen3.8-27b",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "PPLNS pays…" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 12, "completion_tokens": 48, "total_tokens": 60 }
}
Your API key
Keys are created on the dashboard under API. They look
like lj-live-… and are shown once, at creation — we only ever
store a hash, so a lost key has to be revoked and replaced rather than recovered.
Send it as a bearer token on every request:
Authorization: Bearer lj-live-7c2f4a91b8e3d05f6a1c9e2b4d7f8031
Most OpenAI SDKs take this as the api_key argument and set the
header for you — there is nothing LLMJob-specific to install.
Every request is logged against the key that made it: model, node, tokens in and out, speed, and finish reason all show up under Logs on the dashboard.
Models
Two kinds of model sit behind the same endpoint, chosen by the model
field in your request.
| Model | Runs on | Available to |
|---|---|---|
| qwen/qwen3.8-27b | LLMJob's hosted backend | public keys |
| Gemma-4-E4B-it-Q4_K_M | The node network — a GPU running LLMJob Earn | any key |
The Qwen model is the one the free Chat page serves. It is available through the API so you can build against a working endpoint today, before your own nodes are up. It draws on a shared free allowance — see Limits.
The split is a hardware one. Gemma-4-E4B is small enough to load on the range of consumer GPUs that actually run LLMJob Earn, which is what makes it the network model. A 27B model is not, so Qwen is hosted rather than mined — a node serving it would have nothing left to earn with.
Anything else falls through to the node network. Omit model
entirely, or send gpt-4, or send whatever string your framework
hard-codes: the request becomes an inference job and the next available node in your cluster
runs it against the model it has loaded. The model field of the
response always names what actually ran, never what you asked for.
Public & private keys
Every key carries a routing setting, toggled on the dashboard:
- Public — requests may be served by any node on the network, and may use the hosted models above. This is the default.
- Private — requests only ever run on nodes in your account. Nothing leaves your hardware, which also means the hosted models are off-limits: asking for one returns 403 rather than quietly sending your prompt elsewhere.
Flip a key to private when the prompts matter more than the throughput, and keep a separate public key for hosted-model work.
Chat completions
POST /v1/chat/completions
Request body
| Field | Type | Description |
|---|---|---|
| messagesrequired | array | The conversation, as {role, content} objects. Roles are system, user and assistant; anything else is treated as a user turn. content is either a string or OpenAI's array of text and image_url parts — send an image as a data: URL to a node running a vision model. A conversation over the prompt budget is trimmed from the oldest turn, so the question you just asked always survives. |
| model | string | A hosted model id, or anything else to use the node network. Defaults to the network. |
| stream | boolean | true streams the answer back as server-sent events. Defaults to false. |
| max_tokens | integer | Completion ceiling, clamped to the server limit. See Limits. |
| temperature | number | Passed through to the model. |
Response
A standard chat.completion. Two details are worth knowing:
- finish_reason is "length" when the completion budget ran out. On a reasoning model that is the difference between a short answer and a truncated one.
- message.reasoning_content appears when the model produced a chain of thought. It explains an empty content — the budget was spent thinking. Clients that don't know the field ignore it.
Headers
| Header | Direction | Description |
|---|---|---|
| X-LLMJob-Node | request | Pin the request to one node id, to check that it serves and how fast. Fails fast with 404 if that node is offline or unknown, and with 400 if you also asked for a hosted model (no node runs those). Most OpenAI SDKs let you add default headers. |
| X-LLMJob-Served-By | response | Which node served the request — or openrouter for a hosted model. Non-streaming responses only: a stream sends its headers before any node has the job. |
Streaming
Set "stream": true to receive
chat.completion.chunk events over SSE, terminated by
data: [DONE] — the same shape and the same terminator whichever
backend served you.
curl -N https://llmjob-production.up.railway.app/v1/chat/completions \
-H "Authorization: Bearer $LLMJOB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"stream": true,
"messages": [{ "role": "user", "content": "Count to five." }]
}'
data: {"id":"chatcmpl-8f1c…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-8f1c…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"One"},"finish_reason":null}]}
data: {"id":"chatcmpl-8f1c…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
stream = client.chat.completions.create(
model="qwen/qwen3.8-27b",
messages=[{"role": "user", "content": "Count to five."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
Listing models
GET /v1/models returns the models your key can reach, in OpenAI's
list shape. A private key sees only the network model, because that is all it may use.
curl https://llmjob-production.up.railway.app/v1/models \
-H "Authorization: Bearer $LLMJOB_API_KEY"
{
"object": "list",
"data": [
{ "id": "qwen/qwen3.8-27b", "object": "model", "owned_by": "llmjob-hosted" },
{ "id": "Gemma-4-E4B-it-Q4_K_M", "object": "model", "owned_by": "llmjob-network" }
]
}
Treat it as a guide rather than a whitelist: an unlisted model id is not an error, it just goes to the node network.
Limits
| Limit | Network models | Hosted models |
|---|---|---|
| Prompt size | 24,000 characters | 24,000 characters |
| Images per request | 4, up to 4 MB each | — |
| Request body | 20 MB | 20 MB |
| Completion (max_tokens) | 6,400 | 2,048 |
| Request timeout | 280 seconds | Upstream |
| Cost | Your own hardware | Shared free allowance |
Prompts over the character budget are trimmed rather than rejected, and an oversized
max_tokens is clamped to the ceiling — your request still runs.
The hosted models draw on one shared, capped allowance across everybody's API traffic and
the free web chat. Once it is spent they return 402 until it is
topped up; the node network is unaffected, so a request with no
model keeps working.
Errors
Errors use the standard OpenAI envelope, so any SDK will parse them:
{
"error": {
"message": "Node rig-02 took the job but produced no output within 280s.",
"type": "timeout_error",
"code": null,
"job_id": "job_4b1e…",
"served_by": "rig-02",
"job_status": "assigned"
}
}
Failures from the node network carry three extra fields —
job_id, served_by and
job_status — so a timeout or a node error is attributable instead of
anonymous. Strict clients ignore them; yours can log them.
| Status | Type | What happened |
|---|---|---|
| 400 | invalid_request_error | No usable messages, or a hosted model pinned to a node. |
| 401 | — | Missing, malformed or revoked API key. |
| 402 | quota_exhausted | The hosted models' free allowance is spent. Use the node network. |
| 403 | permission_error | A private key asked for a hosted model. Switch the key to public, or drop the model field. |
| 404 | target_node_error | The node named in X-LLMJob-Node is offline or unknown. |
| 502 | node_error / upstream_error | The node failed the job, or the hosted backend returned an error. |
| 503 | not_configured | Hosted models aren't configured on this deployment. |
| 504 | timeout_error | No node finished the job in time. The message says whether the fleet was empty, a node went quiet, or generation was still running. |
A 504 with no served_by means nothing
picked the job up: check that a node is online and serving on your
dashboard. Still stuck? Ask in
Discord.