AccezzCompute API Handbook
Everything your engineers need to run private chat, embeddings, reranking, classification and model jobs on AccezzCompute GPUs, from the first request to production.
What you can build
AccezzCompute runs AI models on GPU computers for you. Your server sends a job over HTTPS with an API key, a checked GPU Worker runs it, and your server collects the result. You never see or manage the GPU machine.
Each workload has a status today. Live means you can use it now. On request means it is built and we switch it on for your account after testing your model. Not yet means do not build against it yet.
| Workload | What it does | Contract | Status |
|---|---|---|---|
| Private chat | Answers a question only from passages you send, and says which passages it used. | private-grounded-chat.v1 | Live |
| Text embedding | Turns up to 128 texts per job into vectors (384 numbers each with the current model). | text-embedding.v1 | Live |
| Text classification | Puts one text into your model's labels, with scores. | text-classification.v1 | Live |
| Reranking | Orders up to 512 passages by how well they answer a query. | text-reranking.v1 | On request |
| Bulk embedding | Embeds a whole file of texts (JSONL, JSON, CSV or Parquet) in one job. | bulk-embedding.v1 | Live |
| Bulk classification | Classifies many texts in one job. | bulk-text-classification.v1 | Live |
| LoRA / QLoRA training | Teaches a small model to answer like your examples, from a JSONL file of prompts and responses. | lora-training.v1, qlora-training.v1 | LoRA live · QLoRA needs a larger GPU |
| Adapter merge, conversion, quantisation | Merges a trained adapter, converts to GGUF, or shrinks a model. | adapter-merge.v1 and others | Not yet |
| Offline evaluation | Scores a candidate model against a baseline on a frozen test set. | offline-evaluation.v1 | On request |
Your data class, data-retention setting and prices are set per account. Read them live from programme capabilities rather than from this page.
First call in 5 minutes
- Get an API key from the AccezzCompute console (API keys → Create key). You see the full key only once. Store it in your server's secret store as
ACCEZZ_API_KEY. - Check what the key can run.
- Send one embedding job and wait for it.
# 1. What can this key run?
curl -s https://www.accezzcompute.com/api/v1/programme/capabilities \
-H "Authorization: Bearer $ACCEZZ_API_KEY"
# 2. Send one embedding job (the idempotency key stops it running twice if you retry)
REQ=$(uuidgen)
curl -s -X POST https://www.accezzcompute.com/api/v1/programme/jobs \
-H "Authorization: Bearer $ACCEZZ_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $REQ" \
-d '{"kind":"embedding","request":{"contractVersion":"text-embedding.v1","requestId":"'$REQ'","model":"text-embedding","items":[{"id":"leaflet-12#p3","text":"Do not eat for six hours before your operation."}],"output":{"normalization":"l2","encoding":"float32"}}}'
# 3. Wait up to 60 s for it to finish, then read the result
curl -s "https://www.accezzcompute.com/api/v1/programme/jobs/JOB_ID?wait=60" -H "Authorization: Bearer $ACCEZZ_API_KEY"
curl -s https://www.accezzcompute.com/api/v1/programme/jobs/JOB_ID/result -H "Authorization: Bearer $ACCEZZ_API_KEY"
// Node 18+ (built-in fetch)
import { randomUUID } from "node:crypto";
const BASE = "https://www.accezzcompute.com";
const headers = { Authorization: `Bearer ${process.env.ACCEZZ_API_KEY}`, "Content-Type": "application/json" };
const caps = await (await fetch(`${BASE}/api/v1/programme/capabilities`, { headers })).json();
console.log(caps.workloads.map(w => `${w.id}: ${w.status}`));
const requestId = randomUUID();
const sent = await fetch(`${BASE}/api/v1/programme/jobs`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": requestId },
body: JSON.stringify({ kind: "embedding", request: {
contractVersion: "text-embedding.v1", requestId, model: "text-embedding",
items: [{ id: "leaflet-12#p3", text: "Do not eat for six hours before your operation." }],
output: { normalization: "l2", encoding: "float32" } } }),
});
let job = await sent.json();
while (!job.lifecycle.terminal) job = await (await fetch(`${BASE}/api/v1/programme/jobs/${job.id}?wait=60`, { headers })).json();
const done = await (await fetch(`${BASE}/api/v1/programme/jobs/${job.id}/result`, { headers })).json();
console.log(done.result.data[0].vector.length); // 384
import os, uuid, requests
BASE = "https://www.accezzcompute.com"
H = {"Authorization": f"Bearer {os.environ['ACCEZZ_API_KEY']}"}
caps = requests.get(f"{BASE}/api/v1/programme/capabilities", headers=H, timeout=30).json()
print([(w["id"], w["status"]) for w in caps["workloads"]])
request_id = str(uuid.uuid4())
job = requests.post(f"{BASE}/api/v1/programme/jobs", headers={**H, "Idempotency-Key": request_id}, json={
"kind": "embedding", "request": {
"contractVersion": "text-embedding.v1", "requestId": request_id, "model": "text-embedding",
"items": [{"id": "leaflet-12#p3", "text": "Do not eat for six hours before your operation."}],
"output": {"normalization": "l2", "encoding": "float32"}}}, timeout=30).json()
while not job["lifecycle"]["terminal"]:
job = requests.get(f"{BASE}/api/v1/programme/jobs/{job['id']}", params={"wait": 60}, headers=H, timeout=90).json()
done = requests.get(f"{BASE}/api/v1/programme/jobs/{job['id']}/result", headers=H, timeout=30).json()
print(len(done["result"]["data"][0]["vector"])) # 384
If status for text-embedding is ready and the last line prints 384, your integration works end to end.
How it works
Jobs
Every call that uses a GPU is a job: you submit it, it runs, and you read its result. Jobs are asynchronous, but one status call can wait up to 60 seconds, so most jobs need only three calls.
Two job families
Private chat uses /api/v1/jobs. All other model workloads (embedding, reranking, classification, bulk, training) use /api/v1/programme/jobs. Both follow the same submit, wait, read pattern.
Models are releases
A model you bring becomes an immutable release. We qualify it on our GPUs for one workload contract, then it is assigned to that workload for your account. Jobs name the workload (for example "model": "text-embedding") and get the assigned release.
Proof of what ran
Every result carries the fingerprints of the model and runner that produced it, and every finished job has an Ed25519-signed attestation you can verify yourself.
Job lifecycle
- queuedsubmitted
- reservedGPU found
- dispatchedsent to a Worker
- runningWorker started
- completedresult verified
From queued or running a job can also end as failed (for example QUEUE_EXPIRED) or cancelled.
Use lifecycle.terminal to know when to stop waiting, and lifecycle.state for the state. completed, failed and cancelled are terminal. A failed or cancelled job has a terminalReason code (programme jobs) or a failure object (chat jobs).
API keys and scopes
Send the key on every request as a bearer token: Authorization: Bearer acz_live_…. Keys look like acz_live_<id>.<secret> or acz_test_<id>.<secret>. We store only a hash; a lost key cannot be recovered, only replaced.
Never put a key in a browser, mobile app or source repository. If a key is exposed (for example pasted into a chat), create a new key, switch your server to it, and revoke the old one in the console.
Scopes
A key carries only the scopes you tick when you create it. Each endpoint below lists the scopes it needs.
| Scope | Allows | Needed for |
|---|---|---|
jobs:read | Read jobs, results, events, capabilities | Every integration |
jobs:write | Submit jobs | Every workload |
jobs:cancel | Cancel jobs | Cancelling chat jobs |
programme:run | Run embedding, reranking, classification, bulk and model workloads | All /api/v1/programme workloads |
inference:run | Legacy chat only (switched off); chat now needs programme:run | — |
models:status | Legacy chat readiness (switched off); use GET /api/v1/programme/capabilities | — |
artifacts:read | List and download your files and chat models | Model management |
artifacts:write | Upload files, register and delete models | Bringing your own model |
usage:read | Read account usage | /api/v1/account/usage |
production:run | Send real (non-synthetic) data to private chat | Only after your account is switched to a production data class |
Workload allowlist and key limits
Each key also has a list of allowed workloads, and three limits set when it is created:
- Requests per minute: default 60. Over the limit you get
429 API_KEY_RATE_LIMITwith aRetry-Afterheader. - Concurrent jobs: default 5 active jobs per key. Over the limit you get
429 API_KEY_CONCURRENCY_LIMIT. - Monthly spend cap (private chat): optional. Over it you get
402 API_KEY_SPEND_LIMIT. Programme workloads use the account budget instead.
Use separate keys for test and live, and for each service that calls us. Test keys (acz_test_) can run programme workloads on synthetic data.
Request rules
JSON and strict schemas
Send Content-Type: application/json. Request schemas are strict: an unknown or missing field is rejected with 400. Send exactly the fields shown for each workload.
Idempotency-Key (required on every submit)
- Every
POSTthat creates a job needs anIdempotency-Keyheader of 8 to 128 characters. Use a UUID; using yourrequestIdis simplest. - Retrying with the same key and the same body returns the original job with
200(programme jobs also say"replay": true). It never runs twice. - The same key with a different body returns
409 IDEMPOTENCY_CONFLICT. - Keys are remembered for 24 hours. After that, reconcile using the
requestIdfilter before sending a new key.
requestId
Every workload request carries your own requestId: 8 to 100 characters from A–Z a–z 0–9 . _ -. A UUID fits. It comes back in the result, and you can find a job by it after a crash: GET /api/v1/programme/jobs?requestId=….
Waiting for a job
GET …/jobs/{id}?wait=60 holds the request open until the job ends or 60 seconds pass, and returns as soon as it ends. Set your HTTP client timeout above the wait (90 seconds works). Values from 0 to 60 are accepted.
Times, money and IDs
- Times are ISO 8601 in UTC, for example
2026-10-08T07:33:38.554Z. - Prices in job objects are in rupees; budgets are in paise (
limitMinor; ₹1 = 100). - Job, release and file IDs are UUIDs. Registered chat models use
mdl_<uuid>.
Checking what you can run
Call this at start-up and before sending real data. It is the single source of truth for your key: which workloads can run now, your data-retention setting, your data class and your budget.
{
"schemaVersion": "programme.v1",
"activationMode": "governed",
"dataRetention": { "mode": "none", "resultHoldMinutes": 60 },
"dataClass": "generated-synthetic",
"budget": { "currency": "INR", "limitMinor": 100000, "reservedMinor": 0, "spentMinor": 0,
"remainingMinor": 100000, "periodStart": "2026-10-07T15:29:48Z", "periodEnd": "2027-01-05T15:30:48Z" },
"customerStorageHosts": [],
"productionReady": true,
"workloads": [
{ "kind": "embedding", "id": "text-embedding", "contractVersion": "text-embedding.v1",
"entitled": true, "keyAllowed": true, "switchedOn": true, "customerSetup": true,
"productionModel": true, "canSubmit": true, "executionReady": true, "status": "ready" },
{ "kind": "reranking", "id": "text-reranking", "status": "workload-paused", "canSubmit": false }
]
}| Field | What to do with it |
|---|---|
workloads[].status | ready means jobs run now. Anything else is the first blocker: credential-policy (key or entitlement), workload-paused (switched off by us), customer-setup-required, production-model-required (no model assigned), not-available (the contract exists but cannot run yet), unavailable or admission-preview. |
dataRetention.mode | none = keep nothing after delivery; standard = job data kept. Refuse to send data if it is not what you expect. |
dataClass | generated-synthetic = synthetic data only. Send real patient data only when this reports a production class. |
budget | The hard spend cap for this key in this period, in paise. null = no budget, so programme jobs are refused. |
customerStorageHosts | Storage hosts you may give us file links from (see section 15). |
Other discovery endpoints
| Endpoint | Returns |
|---|---|
GET /api/v1/capabilities | Scopes, limits and chat workloads for your key |
GET /api/v1/protocol | The integration protocol: scopes, contracts, limits, lifecycle, examples, error contract |
GET /api/v1/openapi.yaml | The full OpenAPI 3.1 description (needs jobs:read). Generate clients from it. |
GET /api/v1/integration-manifest.yaml | The OpenAPI file plus your key's effective scopes, workloads, limits and data handling |
GET /health | Service is up (no key needed) |
The job loop
Every programme workload uses the same five endpoints. Put this helper in your code once and call it for every workload.
| Step | Endpoint | Scopes |
|---|---|---|
| Submit | POST /api/v1/programme/jobs with { "kind", "request" } | jobs:write, programme:run |
| Wait | GET /api/v1/programme/jobs/{id}?wait=60 | jobs:read |
| Read result | GET /api/v1/programme/jobs/{id}/result | jobs:read |
| Cancel | POST /api/v1/programme/jobs/{id}/cancel | jobs:write |
| Find after a crash | GET /api/v1/programme/jobs?requestId=… | jobs:read, programme:run |
| Event history | GET /api/v1/programme/jobs/{id}/events?after=0&limit=100 | jobs:read |
// accezz.js — submit a programme job, wait for it, return its result.
import { randomUUID } from "node:crypto";
export const BASE = "https://www.accezzcompute.com";
export const headers = { Authorization: `Bearer ${process.env.ACCEZZ_API_KEY}`, "Content-Type": "application/json" };
const sleep = ms => new Promise(r => setTimeout(r, ms));
export async function runJob(kind, request) {
// 1. Submit. Retrying with the same Idempotency-Key never runs the job twice.
let job;
for (let attempt = 0; ; attempt++) {
const sent = await fetch(`${BASE}/api/v1/programme/jobs`, {
method: "POST", headers: { ...headers, "Idempotency-Key": request.requestId }, body: JSON.stringify({ kind, request }),
}).catch(() => null);
if (sent?.ok) { job = await sent.json(); break; }
const problem = sent ? await sent.json().catch(() => ({})) : { retryable: true };
if (!problem.retryable || attempt === 4) throw new Error(`${problem.code}: ${problem.detail}`);
await sleep(Number(sent?.headers.get("retry-after")) * 1000 || 2000 * 2 ** attempt);
}
// 2. Wait. Each call waits up to 60 s and returns as soon as the job ends.
while (!job.lifecycle.terminal) {
const check = await fetch(`${BASE}/api/v1/programme/jobs/${job.id}?wait=60`, { headers, signal: AbortSignal.timeout(90_000) }).catch(() => null);
if (check?.ok) job = await check.json(); else await sleep(2000); // brief network hiccup: ask again
}
if (job.status !== "completed") throw new Error(`Job ${job.status}: ${job.terminalReason}`);
// 3. Read the result.
return (await (await fetch(`${BASE}/api/v1/programme/jobs/${job.id}/result`, { headers })).json());
}
# accezz.py — submit a programme job, wait for it, return its result.
import os, time, requests
BASE = "https://www.accezzcompute.com"
H = {"Authorization": f"Bearer {os.environ['ACCEZZ_API_KEY']}"}
def run_job(kind, request):
# 1. Submit. Retrying with the same Idempotency-Key never runs the job twice.
for attempt in range(5):
try:
sent = requests.post(f"{BASE}/api/v1/programme/jobs", json={"kind": kind, "request": request},
headers={**H, "Idempotency-Key": request["requestId"]}, timeout=30)
except requests.RequestException:
time.sleep(2 * 2 ** attempt); continue
if sent.ok:
job = sent.json(); break
problem = sent.json()
if not problem.get("retryable") or attempt == 4:
raise RuntimeError(f"{problem.get('code')}: {problem.get('detail')}")
time.sleep(int(sent.headers.get("Retry-After", 2 * 2 ** attempt)))
# 2. Wait. Each call waits up to 60 s and returns as soon as the job ends.
while not job["lifecycle"]["terminal"]:
try:
check = requests.get(f"{BASE}/api/v1/programme/jobs/{job['id']}", params={"wait": 60}, headers=H, timeout=90)
if check.ok: job = check.json()
except requests.RequestException:
time.sleep(2) # brief network hiccup: ask again
if job["status"] != "completed":
raise RuntimeError(f"Job {job['status']}: {job.get('terminalReason')}")
# 3. Read the result.
return requests.get(f"{BASE}/api/v1/programme/jobs/{job['id']}/result", headers=H, timeout=30).json()
The job object
{
"schemaVersion": "frontend-programme-job.v1",
"id": "0c2a6a30-7893-4161-bce0-ee6cdb569dda",
"type": "text-embedding",
"contractVersion": "text-embedding.v1",
"status": "completed",
"lifecycle": { "state": "completed", "terminal": true, "resultAvailable": true, "cancelRequested": false },
"createdAt": "2026-10-08T07:33:36.401Z", "startedAt": "2026-10-08T07:33:36.9Z", "completedAt": "2026-10-08T07:33:38.554Z",
"commercial": { "currency": "INR", "estimate": 0, "maximumCharge": 0, "charged": 0, "state": "charged" },
"provenance": { "modelReleaseDigest": "91b6b33c…", "runnerDigest": "sha256:ce0bfef9…", "qualificationId": "9584754d-…", "trustDecisionDigest": "…" },
"terminalReason": null
}Programme job states are queued, reserved, dispatched, running, completed, failed and cancelled. A job nobody picks up in 6 hours fails with QUEUE_EXPIRED and is not charged.
Private chat Live
Sends a conversation and up to 64 approved passages to your qualified chat model. The model answers only from those passages and returns which ones it cited. The answer is a draft: your system decides whether to show it. Chat is a programme job like every other workload, so it uses the same job loop, idempotency, errors and signed evidence.
export async function askFromPassages(question, passages, history = []) {
const requestId = randomUUID();
const sent = await fetch(`${BASE}/api/v1/programme/jobs`, {
method: "POST", headers: { ...headers, "Idempotency-Key": requestId },
body: JSON.stringify({ kind: "chat", request: {
contractVersion: "private-grounded-chat.v1",
requestId,
model: CHAT_MODEL_ID, // your qualified chat model release ID
messages: [...history, { role: "user", content: question }],
approvedPassages: passages, // [{ sourceId, sourceVersion, text }]
generation: { maxTokens: 512, temperature: 0 },
cache: { mode: "warm", idleSeconds: 3600 }, // or { mode: "cold", idleSeconds: null }
}}),
});
let job = await sent.json();
if (!sent.ok) throw new Error(`${job.code}: ${job.detail}`);
while (!job.lifecycle.terminal) job = await (await fetch(`${BASE}/api/v1/programme/jobs/${job.id}?wait=60`, { headers })).json();
if (job.lifecycle.state !== "completed") throw new Error(job.terminalReason);
return (await (await fetch(`${BASE}/api/v1/programme/jobs/${job.id}/result`, { headers })).json()).result;
}
def ask_from_passages(question, passages, history=()):
request_id = str(uuid.uuid4())
sent = requests.post(f"{BASE}/api/v1/programme/jobs", headers={**H, "Idempotency-Key": request_id}, json={
"kind": "chat", "request": {
"contractVersion": "private-grounded-chat.v1", "requestId": request_id,
"model": CHAT_MODEL_ID, # your qualified chat model release ID
"messages": [*history, {"role": "user", "content": question}],
"approvedPassages": passages, # [{"sourceId", "sourceVersion", "text"}]
"generation": {"maxTokens": 512, "temperature": 0},
"cache": {"mode": "warm", "idleSeconds": 3600}}}, timeout=30)
job = sent.json()
if not sent.ok: raise RuntimeError(f"{job.get('code')}: {job.get('detail')}")
while not job["lifecycle"]["terminal"]:
job = requests.get(f"{BASE}/api/v1/programme/jobs/{job['id']}", params={"wait": 60}, headers=H, timeout=90).json()
if job["lifecycle"]["state"] != "completed": raise RuntimeError(job.get("terminalReason"))
return requests.get(f"{BASE}/api/v1/programme/jobs/{job['id']}/result", headers=H, timeout=30).json()["result"]
Request fields (request)
| Field | Rule |
|---|---|
contractVersion | Required. private-grounded-chat.v1 |
requestId | Required. 8–120 characters, A–Z a–z 0–9 . _ : - |
model | Required. Your chat model's release ID (it must be qualified for private-grounded-chat.v1) |
messages | Required. 1–64 turns of { role: user|assistant, content (≤16,000) }, ending with a user turn |
approvedPassages | Required. 1–64 items of { sourceId (≤120, unique), sourceVersion, text } |
generation | Required. { maxTokens: 1–2048, temperature: 0–1 }. Use at least 512 for thinking models such as Qwen3, or the answer may be cut short (CHAT_OUTPUT_TRUNCATED). |
cache | Required. { mode: "warm", idleSeconds: 60–604800 } keeps the model loaded between requests; { mode: "cold", idleSeconds: null } unloads it after the job. |
The whole request must be under 512 KB, and passages plus answer must fit the model's context window (4,096 tokens for today's model). Larger requests end with CHAT_CONTEXT_TOO_LARGE.
Result
{
"jobId": "…", "contractVersion": "private-grounded-chat.v1",
"result": {
"requestId": "chat-67abd974-…",
"status": "ok", // ok | insufficient_context | safety_rejection
"answer": "8 am",
"citations": ["clinic-hours"],
"usage": { "inputTokens": 212, "outputTokens": 19 } // example values
},
"executionCache": { "state": "warm" }
}insufficient_context: the passages do not answer the question. Show your fallback, not the draft.safety_rejection: the model refused. Do not show the draft.- Every ID in
citationsis one you sent; the platform checks this before you see the result. - With keep-nothing retention the result can be read for 60 minutes after the job ends, then returns
410 RESULT_DELETED.
The older POST /api/v1/jobs chat (private-gguf-chat.v1), /api/v1/inference and /api/v1/private-chat/health are switched off and return 503 LEGACY_CHAT_COMPATIBILITY_DISABLED. Use the programme chat above.
Text embedding Live
Turns up to 128 texts into vectors in one job. Today's model is BAAI/bge-small-en-v1.5 with CLS pooling: 384 numbers per text, English.
embeddingjobs:write, programme:run{
"kind": "embedding",
"request": {
"contractVersion": "text-embedding.v1",
"requestId": "2f0c7a8e-5d0b-4b8e-9d41-0d6a9f2e1c33",
"model": "text-embedding",
"items": [
{ "id": "leaflet-12#p3", "text": "Do not eat for six hours before your operation." },
{ "id": "leaflet-12#p4", "text": "You may drink water until two hours before." }
],
"output": { "normalization": "l2", "encoding": "float32" }
}
}| Field | Rule |
|---|---|
items | 1–128 items of { id, text }. IDs unique within the job; text 1–32,000 characters. |
model | "text-embedding": the release assigned to this workload for your account. |
output.normalization | l2 (unit-length vectors, for cosine or dot-product search) or none |
output.encoding | float32 (JSON numbers in vector) or base64-f32le (little-endian float32 bytes in vectorBase64, about 3× smaller) |
{
"schemaVersion": "programme-job-result.v1",
"jobId": "…", "type": "text-embedding", "contractVersion": "text-embedding.v1",
"result": {
"contractVersion": "text-embedding.v1",
"requestId": "2f0c7a8e-…",
"modelReleaseDigest": "91b6b33c66bb9fa8…",
"runnerDigest": "sha256:ce0bfef97d24…",
"dimensions": 384,
"encoding": "float32",
"data": [ { "id": "leaflet-12#p3", "vector": [0.0132, -0.0419, …] }, { "id": "leaflet-12#p4", "vector": [ … ] } ]
},
"outputs": []
}Vectors from different models cannot be compared. Save modelReleaseDigest beside each vector. If it ever changes, re-embed your library before mixing old and new vectors.
For Tamil and Hindi, we will qualify intfloat/multilingual-e5-small on request. e5 models expect query: at the start of questions and passage: at the start of documents.
Bulk embedding Live
Embeds a whole file of texts in one job. Use it to index a library; use text embedding for questions and day-to-day additions.
- Put the texts in one file, one record per text with
idandtext. Accepted formats:.jsonl(one JSON object per line),.json(an array, or{ "items": [...] }),.csv(columnsid,text) or.parquet. - Make the file available as a file ID: either upload it (section 14, upload steps) or keep it in your own storage and register a link (section 15).
- Submit the job with that ID as
inputArtifactId.
{
"kind": "bulk-embedding",
"request": {
"contractVersion": "bulk-embedding.v1",
"requestId": "index-2026-10-09-batch-001",
"model": "bulk-embedding",
"inputArtifactId": "6f1d2c3e-…",
"itemCount": 50000,
"output": { "normalization": "l2", "format": "arrow-ipc-f32le" }
}
}The result is { requestId, itemCount } plus one output file, embeddings.arrow (Apache Arrow IPC file, columns id and vector). Download it as described in section 16 and load it into your vector database. itemCount may be 1 to 10,000,000.
Reranking On request
Scores each document against a query with a cross-encoder and returns the best topN in order. Use it after vector search to pick the 3–5 passages you send to chat.
{
"kind": "reranking",
"request": {
"contractVersion": "text-reranking.v1",
"requestId": "rr-6c1e0e77-…",
"model": "text-reranking",
"query": "How long before surgery must I stop eating?",
"documents": [
{ "id": "leaflet-12#p3", "text": "Do not eat for six hours before your operation." },
{ "id": "leaflet-40#p1", "text": "Visiting hours are 4 pm to 7 pm." }
],
"topN": 1
}
}Rules: query 1–32,000 characters; 1–512 documents with unique IDs, each up to 32,000 characters; topN from 1 to the number of documents.
{
"contractVersion": "text-reranking.v1", "requestId": "rr-6c1e0e77-…",
"modelReleaseDigest": "…", "runnerDigest": "sha256:…",
"scoreSemantics": "ranking-score",
"data": [ { "id": "leaflet-12#p3", "rank": 0, "score": 8.91 } ]
}rank starts at 0 (best). Scores order documents within one job; do not compare them across jobs or treat them as probabilities. Ties keep your original order.
Classification Live
Classifies one text into your model's labels. Today's model is a test model with POSITIVE and NEGATIVE; when you bring your own (for example urgent / routine / not-clinical), the same call returns your labels.
{
"kind": "classification",
"request": {
"contractVersion": "text-classification.v1",
"requestId": "cls-0d8a7f5e-…",
"model": "text-classification",
"text": "The nurse explained everything clearly and I felt calm."
}
}Rules: text 1–32,000 characters and at most 128 KiB; requestId 8–100 characters (the general rule in section 5).
{
"contractVersion": "text-classification.v1", "requestId": "cls-0d8a7f5e-…",
"state": "classified", // classified | abstained | rejected
"reason": null, // or low_score | insufficient_margin | out_of_distribution | truncation_prohibited | qualification_mismatch
"labels": [ { "id": "POSITIVE", "score": 0.986 } ],
"truncated": false,
"scoreSemantics": "…", "labelSchemaDigest": "582122c8…", "calibrationDigest": "…",
"modelReleaseDigest": "…", "runnerDigest": "sha256:…"
}abstainedmeans the model is not sure;labelsis then empty andreasonsays why. Route these to a person.- Scores are between 0 and 1. Texts are never silently truncated (
truncatedis alwaysfalse).
Bulk classification Live
The bulk-text-classification.v1 contract (up to 10,000 inline items) is defined but cannot run yet. Submitting it returns 409 WORKLOAD_NOT_AVAILABLE at once, and capabilities reports its status as not-available. Until it runs, send one classification job per text; at 1–1.5 seconds each when the model is loaded, and several at a time, that suits batches of a few thousand texts.
Training, transforms and evaluation
LoRA training Live
Teaches the base model assigned to lora-training (today Qwen2.5-0.5B-Instruct) to answer the way your examples do. The result is a small adapter (about 2 MB) and a new adapter model release in your account. Training runs on the same governed GPUs as every other workload; your examples never leave your account.
Three calls: upload a JSONL file, register it as a dataset, submit the job. In the Compute Console the same steps are the Training studio on the LoRA and QLoRA cards.
1 · Upload your examples
One JSON object per line, each with a non-empty prompt and response (other columns are allowed and ignored). Upload it like any private file (section 14) with contentType: "text/plain", and wait until it shows verified.
{"prompt":"How long before surgery must I stop eating?","response":"Stop eating solid food six hours before your operation."}
{"prompt":"Can I drink water on the morning of surgery?","response":"Yes. Plain water is allowed until two hours before your operation."}2 · Register the dataset
{ "fileId": "<uploaded file id>", "format": "jsonl",
"licenceAttested": true, "consentAttested": true, "retentionDays": 30 }The server reads every line and returns a TrainingDataset: { schemaVersion: "training-dataset.v1", id, filename, format, columns, rowCount, tokenEstimate, dataClass, retentionDays, sha256, sizeBytes, createdAt }. Use its id as datasetArtifactId. GET /api/v1/programme/datasets/{id} reads it back. Registering the same file again returns the same dataset.
400 TRAINING_DATASET_INVALIDnames the first bad line (linein the response): not JSON, not an object, or a missingprompt/response. Limits: 1,000,000 rows, 256 MiB, 64,000 characters per field, 32 columns.400 TRAINING_DATASET_ATTESTATION_REQUIRED: both attestations must betrue.400 TRAINING_DATASET_FORMAT_UNSUPPORTED: onlyjsonltoday.404 FILE_NOT_FOUND,409 ARTIFACT_NOT_CLEAN(the upload has not finished verification).
3 · Submit the training job
{
"kind": "lora-training",
"request": {
"contractVersion": "lora-training.v1",
"requestId": "train-2026-10-09-01",
"model": "lora-training",
"datasetArtifactId": "<dataset id>",
"objective": "causal-language-modeling",
"seed": 7,
"adapter": { "rank": 8, "alpha": 16, "dropout": 0.05, "bias": "none", "targetModules": ["q_proj", "v_proj"] },
"optimization": { "epochs": 2, "maxSteps": 60, "batchSize": 2, "gradientAccumulationSteps": 2,
"learningRate": 0.0002, "warmupSteps": 5, "weightDecay": 0, "checkpointEverySteps": 20 },
"precision": { "computeDtype": "float16", "gradientCheckpointing": true },
"limits": { "maxWallSeconds": 21600, "maxOutputBytes": 67108864, "maxCheckpoints": 10, "maxCostInr": 0 },
"resumeCheckpointId": null
}
}- How long it trains: it stops at whichever comes first,
maxStepsorepochspasses over your data. One step usesbatchSize × gradientAccumulationStepsexamples. - What it learns: only the response is learned; the prompt is context. Examples longer than 512 tokens are shortened (counted in the result), and an example whose prompt alone fills the limit is skipped.
- Reproducible: the same dataset, settings and
seedgive the same adapter, and a run resumed from a checkpoint (resumeCheckpointId) ends with identical weights. - QLoRA (
qlora-training,qlora-training.v1) adds"quantization": { "bits": 4, "quantType": "nf4", "computeDtype": "float16", "doubleQuantization": true }and needs a GPU with bitsandbytes support; on the current 4 GB GPUs use LoRA.
Result
{
"schemaVersion": "training-result.v1", "state": "completed",
"requestId": "train-2026-10-09-01", "contractVersion": "lora-training.v1",
"steps": 12, "epochsCompleted": 2, "resumedFromStep": 0,
"firstLoss": 3.508, "finalLoss": 3.459,
"examples": 24, "truncatedExamples": 0, "skippedExamples": 0, "maxTokensPerExample": 512,
"responseTokensTrained": 850, "checkpointSequences": [1, 2],
"candidateReleaseId": "78b9d45a-…",
"candidateArtifacts": [ { "artifactId": "…", "digest": "…" }, { "artifactId": "…", "digest": "…" } ],
"identityDigest": "…", "assignmentChanged": false
}That run, 24 examples and 12 steps on a 4 GB GTX 1650, took about two minutes from submission: one minute waiting for the next dispatch and one minute training. candidateReleaseId is the new adapter model release in your account (named adapter-<job id>, GET /api/v1/programme/models/{id}); it records the base model it was trained from and is ready for adapter merge or evaluation. Training never changes which model a workload uses (assignmentChanged: false).
Output files: adapter_model.safetensors and adapter_config.json (the trained adapter), plus one pair of checkpoint files per saved checkpoint. Download them as in section 16. A falling loss (firstLoss → finalLoss) shows the model learned; if finalLoss is not finite the job fails with TRAINING_DIVERGED, so lower learningRate.
Other batch workloads
These are defined and appear in capabilities, but are not ready for your use yet. The shapes below let you plan.
| Kind | Request fields | Status |
|---|---|---|
adapter-merge | contractVersion, requestId, sourceReleaseId, adapterReleaseId, targetProfile (safetensors-f16 | safetensors-bf16), limits | Not yet |
model-conversion | contractVersion, requestId, sourceReleaseId, targetProfile (gguf-f16 | gguf-bf16), limits | Not yet |
model-quantisation | contractVersion, requestId, sourceReleaseId, targetProfile (gguf-q8_0 | gguf-q6_k | gguf-q5_k_m | gguf-q4_k_m), importanceMatrixArtifactId (or null), limits | Not yet |
offline-evaluation | contractVersion, requestId, suiteId, candidateReleaseId, baselineReleaseId, limits{maxWallSeconds, maxOutputBytes, maxCostInr}. The suite (your frozen test set and metrics) is registered by AccezzCompute with you. | On request |
Transform limits are { maxWallSeconds (60–172,800), maxScratchBytes, maxOutputBytes, maxCostInr }. Training limits allow up to 7 days of wall time.
Bring your own model
You can run your own models: a GGUF chat model, or safetensors models for embedding, reranking and classification. Every model goes through the same steps, and nothing runs until we have qualified it on our GPUs.
- Upload files
- Register roles
- Package
- Release
- Qualify we run it
- Assign
1. Upload each file
{ "filename": "model.safetensors", "contentType": "application/x-safetensors",
"sizeBytes": 133466304, "sha256": "3c9f3166…" }
→ 201 { "artifact": { "id": "26a81f6e-…", "status": "awaiting_upload", … },
"upload": { "mode": "single", "method": "PUT", "url": "https://…", "expiresInSeconds": 900,
"headers": { "content-type": "application/x-safetensors", "if-none-match": "*", … } } }- Single mode (smaller files;
upload.modetells you which mode applies):PUTthe bytes toupload.urlwith exactly the headers given, within 15 minutes. - Multipart mode (larger files): for each part,
POST /api/requester/artifacts/{id}/partswith{ "partNumber": n },PUTthat slice (sizeupload.partSize) to the returned URL, and keep itsETagresponse header. - Then
POST /api/requester/artifacts/{id}/complete(multipart:{ "parts": [{ "partNumber": 1, "etag": "…" }, …] }in order). The file is checked and scanned; pollGET /api/requester/artifactsuntil it isverifiedand clean. - Allowed types:
application/x-safetensors,application/x-gguf,application/json,application/octet-stream,text/plain,application/zip,application/onnx. Up to 20 GiB per file.
Chat models (GGUF): register and you are done
{ "artifactId": "…", "name": "Clinic chat", "version": "1.0.0" } returns an immutable mdl_… ID. We then run the chat qualification suite; mark the model as your production chat model in the console, and modelId: "production" uses it. List with GET /api/requester/models; delete with DELETE /api/requester/models/{id}.
Programme models: register, package, release
| Step | Endpoint | Body |
|---|---|---|
| 2 · Register each file's role | POST /api/v1/programme/artifacts/{fileId}/register | { "role": "embedding-weights" } — one of generator-weights, embedding-weights, reranker-weights, classifier-weights, adapter, tokenizer, config |
| 3 · Freeze a package | POST /api/v1/programme/model-packages | { "manifest": { schemaVersion: "customer-model-package.v2", name, version, family, architecture, modelKind, format, roles, files[{filename, role, sha256, sizeBytes, mediaType}], allowDynamicCode: false }, "members": [{ artifactId, filename }] } |
| 4 · Create a release | POST /api/v1/programme/models | { packageId, name, version, family, architecture, configuration, licenceAttestation, sourceAttestation } |
| 5 · Ask for qualification | POST /api/v1/programme/models/{id}/qualification-requests | Usually done by us. Becomes qualified only after signed test evidence from our GPU is verified. |
| 6 · Assign to a workload | PUT /api/v1/programme/model-assignments/{workloadId} | { "releaseId": "…", "expectedVersion": 0 } (0 for the first assignment; then the current version, so two people cannot overwrite each other) |
A model that cannot serve its workload is refused when you upload or create the release, not later. Classifiers must be fine-tuned for classification: config.json needs architectures ending in ForSequenceClassification and an id2label map with 2 to 128 labels (a base model such as DistilBertForMaskedLM is refused with MODEL_RELEASE_LABELS_REQUIRED). Embedding models must record dimensions, pooling and normalization; for sentence-transformers packages the pooling is read from 1_Pooling/config.json (CLS for BGE). Rerankers must record their maximum input length.
Supported architectures include BERT, DistilBERT, RoBERTa, XLM-RoBERTa, DeBERTa, MPNet, ModernBERT, Qwen2/3, Llama, Mistral, Gemma, Phi and T5. modelKind is one of causal-llm, embedding, dense-retriever, cross-encoder-reranker, sequence-classifier, adapter; format is safetensors, gguf or onnx. Pickle files, executables and remote code are rejected.
Checking a model
GET /api/v1/programme/modelsandGET /api/v1/programme/models/{id}: the release (name, version, family, architecture, package and release digests, format, licence declaration, label IDs for classifiers, state) and itsqualifications. A release is usable for a workload only when it has a qualification with that exactcontractVersion,state: "qualified"andinvalidatedAt: null.GET /api/v1/programme/model-assignments: which release each workload uses.POST /api/v1/programme/models/{id}/retire: retire an unassigned release.
Files in your own storage
Large inputs can stay in your storage (Azure Blob, S3 or similar). You give us a short-lived read-only link; the GPU Worker reads the file directly and checks every byte against the checksum you declared. AccezzCompute never downloads or keeps the file.
{ "url": "https://apacsdata.blob.core.windows.net/index/batch-001.jsonl?sv=…&sig=…",
"sha256": "a3f1…", "sizeBytes": 48211933, "filename": "batch-001.jsonl",
"role": "job-input",
"expiresAt": "2026-10-09T14:00:00Z" }
→ 201 { "schemaVersion": "external-input.v1", "id": "6f1d2c3e-…", "role": "job-input",
"sha256": "a3f1…", "sizeBytes": 48211933, "storedByAccezzCompute": false, "state": "ready" }- The link's host must be on your account's allowed storage list (shown as
customerStorageHostsin capabilities). Ask us to add your storage host. expiresAtmust be at least 1 minute and at most 24 hours ahead. The link is never returned and is forgotten when it expires.role:job-input(default),datasetorevaluation-input. Files up to 8 GiB.- Use the returned
idwherever a job takes a file ID (for exampleinputArtifactId). If the link has expired when the job runs, the job fails withEXTERNAL_INPUT_UNAVAILABLE; register a fresh link and resubmit. GET /api/v1/programme/external-inputs/{id}shows whether the link is stillreadyorexpired.
Output files (for example embeddings.arrow) are written to AccezzCompute storage and downloaded by you with a short-lived link. Writing outputs straight to your storage is planned.
Results and output files
Returns { schemaVersion: "programme-job-result.v1", jobId, type, contractVersion, result, outputs }. result is the verified workload result; outputs lists output files as { artifactId, filename, contentType, sizeBytes, sha256, download }.
409 RESULT_NOT_READY: the job has not completed.410 RESULT_DELETED: removed under keep-nothing retention. Read results promptly after completion.
Returns a private download link valid for 15 minutes. Download the file, then check its SHA-256 against sha256 from the result before using it.
Results are verified before you see them
Before a result is stored, the control plane checks it against the contract: the right number of vectors with the right dimensions, unit length when you asked for l2, only label IDs your model declares, scores in range, and the expected model and runner fingerprints. A result that fails is rejected and the job fails with RESULT_CONTRACT_INVALID rather than giving you bad data.
Data handling and retention
| Data | Keep-nothing (mode: none) | Standard (mode: standard) |
|---|---|---|
| Inputs (questions, passages, texts) | Removed about 1 minute after the job ends | Kept with the job (upper limit 1 year) |
| Results | Removed 5 minutes after your first read, or after the hold window (default 60 minutes) if never read | Kept with the job (upper limit 1 year) |
| Output files | Deleted after the hold window | Kept with the job |
| Files in your storage | Never copied by AccezzCompute; the Worker reads them for the job only | |
| Metadata (IDs, states, timings, token counts, fingerprints, cost) | Kept: needed for billing, evidence and support | |
- Where: the API and database run in the EU (Amsterdam). GPU Workers run in India.
- On the GPU: each job runs in a sandbox with no network access, in its own temporary folder that is deleted after the job. Model files may be cached on the Worker (model files only, never your data).
- Synthetic first: your account is approved for synthetic data. Real patient data needs a signed agreement and a production data class, which
dataClassin capabilities will report. - Backups: database backups are not yet switched on. When they are, keep-nothing content will be kept out of them.
Signed evidence
For every finished programme job you can get a statement of what produced the result, signed by AccezzCompute with Ed25519. Store it beside the result for audit.
{
"statement": {
"schemaVersion": "programme-evidence-attestation.v1",
"issuer": "AccezzCompute",
"issuedAt": "2026-10-08T11:44:47.906Z",
"job": { "id": "0c2a6a30-7893-4161-bce0-ee6cdb569dda", "workload": "text-embedding",
"contractVersion": "text-embedding.v1", "state": "completed",
"requestId": "4e95cc77-3e4f-480c-b60b-4e93ce50345e",
"completedAt": "2026-10-08T07:33:38.554Z", "terminalReason": null },
"modelReleaseDigest": "91b6b33c66bb9fa85cbba36bdcdf7e57858b7389ac8f7246a2b2cf146068ca1e",
"runnerDigest": "sha256:ce0bfef97d24883248a5b6ad8958601cff2f9197fb5e0cc0fbbb6ab793f5fe77",
"resultDigest": "643948bbdf631a8ec6e5bd85fc56a1a53f9904d7d005e34c18cb2e8db4d3551f",
"resultRedacted": true, "outputs": [], "workerSignatureVerified": true,
"keyId": "ac-ev-145c3c761ce7fdc3"
},
"algorithm": "Ed25519",
"keyId": "ac-ev-145c3c761ce7fdc3",
"signature": "crBUMHcSfcuYFZXjCbvnRlTD1YRLmtgNb4CRU+lvUfYZTVFaCAtXTDn4Y62R9dpaIVq/pGB8BX/zJY40azhmDg=="
}Verify it
- Encode
statementas canonical JSON: object keys sorted at every level, no spaces, UTF-8, strings escaped as JSON.stringify does. - Verify the base64
signatureover those bytes with the public key from/evidence-keyswhosekeyIdmatches. - Check
resultDigest: the SHA-256 (hex) of the canonical JSON of theresultobject you received from/result. Numbers must be written the way JavaScript writes them (for example0.00001becomes1e-5), so verify result digests in Node.js, or format numbers the same way.
import crypto from "node:crypto";
const canonical = v => Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
: v && typeof v === "object" ? `{${Object.keys(v).sort().map(k => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
: JSON.stringify(v);
export async function verifyJob(jobId, storedResult) {
const att = await (await fetch(`${BASE}/api/v1/programme/jobs/${jobId}/attestation`, { headers })).json();
const keys = await (await fetch(`${BASE}/api/v1/programme/evidence-keys`)).json();
const key = keys.keys.find(k => k.keyId === att.keyId);
const signed = crypto.verify(null, Buffer.from(canonical(att.statement)), crypto.createPublicKey(key.publicKeyPem), Buffer.from(att.signature, "base64"));
const digest = crypto.createHash("sha256").update(canonical(storedResult)).digest("hex");
return signed && digest === att.statement.resultDigest;
}
# pip install cryptography requests
import base64, hashlib, json, requests
from cryptography.hazmat.primitives.serialization import load_pem_public_key
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def verify_signature(job_id):
att = requests.get(f"{BASE}/api/v1/programme/jobs/{job_id}/attestation", headers=H, timeout=30).json()
keys = requests.get(f"{BASE}/api/v1/programme/evidence-keys", timeout=30).json()["keys"]
key = next(k for k in keys if k["keyId"] == att["keyId"])
load_pem_public_key(key["publicKeyPem"].encode()).verify(base64.b64decode(att["signature"]), canonical(att["statement"]))
return att # raises InvalidSignature if it does not verify
# The statement has no fractional numbers, so Python's encoding matches. For resultDigest
# over vectors, use the JavaScript routine (number formatting differs).
issuedAt is the time of each request, so every fetch returns a fresh statement and signature for the same job. When the signing key is rotated, the retired public key will stay published so older attestations keep verifying.
GET /api/v1/programme/jobs/{id}/evidence returns the verified workload and commercial evidence record for the job.
Budgets and charges
- Programme workloads are admitted only within the key's active budget (
budgetin capabilities). Each job reserves its maximum charge when it is admitted; when it ends, the reservation is replaced by the real charge or released. - Only completed jobs are charged. Failed, cancelled and expired jobs cost nothing.
- A job whose maximum charge would exceed the remaining budget is refused with
BUDGET_EXCEEDED. This is a hard stop until the budget is raised or the period renews. - Private chat uses the key's optional monthly spend cap (
402 API_KEY_SPEND_LIMIT). - Each programme job shows
commercial: { currency, estimate, maximumCharge, charged, state }.statemoves frompendingorevidence-dependenttochargedornot-charged. GET /api/v1/account/usage(usage:read) gives account totals.
During the pilot every workload is priced at ₹0 per job, so budgets prove the cap works without blocking anything.
Errors and retries
Errors use application/problem+json. Branch on code, retry only when retryable is true, obey Retry-After, and quote requestId to support.
{ "type": "https://www.accezzcompute.com/problems/result-not-ready", "title": "Request conflict", "status": 409,
"detail": "Result is not ready", "code": "RESULT_NOT_READY",
"requestId": "b3456736-3807-447a-b5e9-bd10395aa753", "retryable": false }| Code | Status | Retry | Meaning and what to do |
|---|---|---|---|
INVALID_REQUEST, *_SCHEMA_INVALID, *_INVALID | 400 | No | A field broke a rule. Fix the request. |
CONTRACT_VERSION_UNSUPPORTED | 400 | No | Kind and contractVersion do not match |
IDEMPOTENCY_KEY_INVALID | 400 | No | Missing key, or not 8–128 characters |
REQUEST_ID_INVALID | 400 | No | Malformed requestId filter on the job list |
BUDGET_EXCEEDED | 400 | No | Would exceed the budget; raise it or wait for the next period |
UNAUTHORIZED | 401 | No | Key missing, wrong or revoked |
API_KEY_SPEND_LIMIT | 402 | No | Chat: the key's monthly cap would be exceeded |
API_KEY_SCOPE_REQUIRED | 403 | No | Key lacks a scope; requiredScope names it |
API_KEY_WORKLOAD_REQUIRED, WORKLOAD_NOT_ENTITLED | 403 | No | Workload not allowed for the key or the account |
DATA_HOSTS_NOT_CONFIGURED, DATA_HOST_NOT_ALLOWED | 403 | No | Storage host not on your allowed list |
JOB_NOT_FOUND | 404 | No | No such job for this account |
MODEL_RELEASE_NOT_FOUND | 404 | No | No model release with that ID belongs to your account |
MODEL_RELEASE_UNAVAILABLE | 409 | No | The model is retired or its files are gone |
MODEL_QUALIFICATION_REQUIRED, WORKLOAD_MODEL_UNAVAILABLE | 409 / 400 | No | The model is not qualified for this workload on the current GPUs; request qualification first |
TRAINING_*_OVER_BUDGET | 400 | No | A training setting is above the qualified limit; POST /api/v1/programme/training/preflight lists the limits |
CHAT_CONTEXT_TOO_LARGE | job | No | Passages plus answer do not fit the model's context window; send fewer or shorter passages |
IDEMPOTENCY_CONFLICT | 409 | No | Same key, different body. Use a new key for a new request. |
RESULT_NOT_READY | 409 | Later | Wait for the job to complete |
JOB_NOT_CANCELLABLE | 409 | No | Job already ended |
JOB_NOT_FINISHED | 409 | Later | Attestation is available once the job ends |
WORKLOAD_PAUSED | 409 | Later | Workload switched off by AccezzCompute |
EXTERNAL_INPUT_CONFLICT | 409 | No | Same checksum registered with a different size |
WORKLOAD_NOT_AVAILABLE | 409 | No | The contract exists but cannot run yet |
TRAINING_DATASET_INVALID | 400 | No | A line of the training file is not a JSON object with prompt and response; line says which |
TRAINING_DATASET_ATTESTATION_REQUIRED | 400 | No | Set licenceAttested and consentAttested to true |
TRAINING_DIVERGED | job | No | Training loss stopped being a number; lower learningRate and resubmit |
MODEL_RELEASE_LABELS_REQUIRED | 400 | No | A classifier release has no labels; upload a model fine-tuned for classification |
MODEL_RELEASE_CONFIGURATION_INCOMPLETE | 400 | No | An embedding or reranker release lacks what it needs to run |
IDEMPOTENCY_RECORD_EXPIRED | 410 | No | Key older than 24 hours; reconcile by requestId |
RESULT_DELETED | 410 | No | Removed under keep-nothing retention |
PAYLOAD_TOO_LARGE | 413 | No | A size limit was exceeded |
API_KEY_RATE_LIMIT | 429 | Yes | Too many requests this minute; wait Retry-After seconds |
API_KEY_CONCURRENCY_LIMIT | 429 | Yes | Too many active jobs on this key |
CAPACITY_UNAVAILABLE | 503 | Yes | No GPU can take interactive work right now; resend after Retry-After (15 s) with the same Idempotency-Key |
SERVICE_UNAVAILABLE, EVIDENCE_SIGNING_UNAVAILABLE | 503 | Yes | Temporarily unavailable; back off and retry |
Every error also carries stage (where it happened) and action (what to do next). The complete list of codes, with status, retryable and action, ships as error-catalogue.json in your integration pack.
Why a job ended (terminalReason)
| Code | Meaning | Charged |
|---|---|---|
QUEUE_EXPIRED | No GPU picked the job up in time (6 hours for programme jobs) | No |
REQUESTED | You cancelled it | No |
PROGRAMME_EXECUTION_FAILED | The model runner reported a failure | No |
RESULT_CONTRACT_INVALID | The result did not pass verification and was rejected | No |
PROVENANCE_MISMATCH | The GPU's model or runner fingerprint did not match the scheduled one | No |
EXTERNAL_INPUT_UNAVAILABLE | Your storage link had expired or the file was gone | No |
EXECUTION_RETRIES_EXHAUSTED | The job stopped the GPU on three attempts in a row (six for training); check the input, then contact support | No |
PROGRAMME_RUNNER_DEADLINE | The job ran past its time limit; send a smaller input or a larger limits.maxWallSeconds | No |
Retry policy that works
- Network errors and 5xx on submit: retry with the same Idempotency-Key, backing off 2, 4, 8, 16 seconds.
- The JavaScript SDK does this for you: it retries only responses marked
retryable(and bare gateway 502/503/504), honoursRetry-After, and never repeats a write without an Idempotency-Key. - 429: wait the
Retry-Afterseconds. - While waiting on a job, a failed status call is harmless; ask again after 2 seconds.
- A failed job is final. To try again, submit a new job with a new
requestId.
Speed and reliability
Measured on 14 November 2026 on today's GPU (NVIDIA GTX 1650, 4 GB), end to end from a client in India, including the API round trips.
| Workload | Model already loaded | First job after the model was unloaded |
|---|---|---|
| Private chat (short answer) | 4–6 s | about 90–145 s |
| Text embedding (1–2 texts) | 1.7–2.5 s | about 50–120 s |
| Classification | about 3 s | about 70–95 s |
| Bulk embedding / classification (small file) | — | about 75–105 s |
- Keep a model loaded. Each model has an execution setting per workload. Set
prefer-warmwith an idle time (1 hour to 7 days) throughPATCH /api/v1/programme/models/{id}/execution-settings?workloadId=…&environment=productionor the console, and repeat requests are served in seconds. Chat can also ask per request withcache.mode: "warm".prefer-warmfalls back to a cold run instead of failing. - Today's GPU has 4 GB. It cannot hold the chat model and the encoders at the same time, so switching between chat and embedding or classification may reload a model.
- Capacity today: one GPU Worker. Interactive requests that arrive while it is busy get
503 CAPACITY_UNAVAILABLEwithRetry-After: 15; bulk and training jobs queue instead. - Connection drops: the GPU site's internet sometimes drops. Short drops are invisible; after a longer drop the job is re-queued and finishes once the GPU reconnects, and it never runs twice.
- No uptime commitment yet. With one Worker there is no failover; we will offer an uptime figure once a second Worker is online.
- Design for it: allow up to 3 minutes for a first request, set HTTP timeouts of 90 seconds on
?wait=60calls, show a "preparing" state if a job is still queued after 10 seconds, and keep your own fallback (for example a canned answer) for chat.
Recipe: full RAG pipeline
Your documents and vectors stay in your database. AccezzCompute does only the GPU steps. The model never reads your library; you send it exactly what each step needs.
Indexing, once per document
- Your server sends passages to
embedding(128 per job) orbulk-embedding(a file). - AccezzCompute returns vectors and the
modelReleaseDigest. - Your server stores the vectors in its own database.
Each question
- The patient asks your server a question.
- Your server sends the question to
embeddingand gets its vector. - Your server searches its own vectors for the top 20 passages.
- Your server sends the question and those passages to
reranking(topN 5). - Your server sends the question and the best passages to private chat.
- AccezzCompute returns a draft answer and
sourceIdsUsed; your server checks it and replies.
// Indexing: embed passages in batches of 128 and store each vector with its model fingerprint.
export async function indexPassages(passages /* [{ id, text }] */) {
for (let i = 0; i < passages.length; i += 128) {
const batch = passages.slice(i, i + 128), requestId = randomUUID();
const { result } = await runJob("embedding", { contractVersion: "text-embedding.v1", requestId,
model: "text-embedding", items: batch, output: { normalization: "l2", encoding: "float32" } });
await db.saveVectors(result.data, result.modelReleaseDigest); // your database
}
}
// Answering: embed the question, search your own vectors, rerank, then ask.
export async function answer(question) {
const { result: q } = await runJob("embedding", { contractVersion: "text-embedding.v1", requestId: randomUUID(),
model: "text-embedding", items: [{ id: "q", text: question }], output: { normalization: "l2", encoding: "float32" } });
const candidates = await db.nearest(q.data[0].vector, 20, q.modelReleaseDigest); // [{ id, text, sourceVersion }]
const { result: ranked } = await runJob("reranking", { contractVersion: "text-reranking.v1", requestId: randomUUID(),
model: "text-reranking", query: question, documents: candidates.map(c => ({ id: c.id, text: c.text })), topN: 5 });
const best = ranked.data.map(r => candidates.find(c => c.id === r.id));
return askFromPassages(question, best.map(c => ({ sourceId: c.id, sourceVersion: c.sourceVersion, text: c.text })));
}- Until reranking is switched on for you, skip it and send the top 3–5 vector results to chat.
- Only search vectors made by the same
modelReleaseDigestas the question vector. - Re-index only new or changed documents; an unchanged document never needs to be sent again.
Go-live checklist
Integration
- API keys live only in your server's secret store; separate keys for test and live.
- Every submit sends an Idempotency-Key and a unique
requestId, and your code can find a job byrequestIdafter a crash. - Wait calls use
?wait=60with a 90-second HTTP timeout; status-call failures are retried. - Errors are handled by
code; 429 obeysRetry-After. - Results are read promptly (keep-nothing removes them) and stored with their
modelReleaseDigest. - Chat answers with
insufficient_contextorsafety_rejectionare never shown as answers.
Checks against capabilities before each session
workloads[].statusisreadyfor what you need.dataRetention.modeanddataClassmatch what your approval allows.budgetis not null and has room.
Before real patient data
- Data processing agreement signed;
dataClassreports a production class. - Your key has
production:runand chat requests senddataMode: "production". - Your own evaluation of the chat and classifier models is recorded.
Limits at a glance
| Limit | Value |
|---|---|
| Requests per minute per key (default) | 60 |
| Concurrent active jobs per key (default) | 5 |
| Idempotency-Key length · remembered for | 8–128 characters · 24 hours |
| requestId | 8–100 characters, A–Z a–z 0–9 . _ - |
wait on status calls | 0–60 seconds |
| Embedding: items per job · text length | 128 · 32,000 characters |
| Bulk embedding: items per file | up to 10,000,000 |
| Reranking: documents · text length | 512 · 32,000 characters |
| Classification: text | 32,000 characters and 128 KiB |
| Chat: question · passages · passage text | 8,000 characters · 32 · 24,000 characters |
| Chat: history · maxTokens | 8 turns (4,000 characters each) · 512 |
| Programme job queue timeout | 6 hours, then QUEUE_EXPIRED |
| Chat queue timeout | about 5 minutes |
| Uploaded file size | 20 GiB (large files use multipart upload) |
| Upload and part URLs | 15 minutes |
| Your-storage link (external input) | 1 minute to 24 hours ahead · files up to 8 GiB |
| Output file download link | 15 minutes |
| Keep-nothing: inputs · results | about 1 minute after the job · 5 minutes after first read or 60-minute hold |
Endpoint index
| Method | Path | Purpose | Section |
|---|---|---|---|
| GET | /api/v1/programme/capabilities | What this key can run, retention, data class, budget | 06 |
| POST | /api/v1/programme/jobs | Submit a programme job | 07 |
| GET | /api/v1/programme/jobs | List jobs (limit, beforeCreatedAt, beforeId, requestId) | 07 |
| GET | /api/v1/programme/jobs/{id} | Job status (?wait=) | 07 |
| GET | /api/v1/programme/jobs/{id}/result | Verified result and output list | 16 |
| GET | /api/v1/programme/jobs/{id}/outputs/{artifactId} | 15-minute download link | 16 |
| GET | /api/v1/programme/jobs/{id}/events | Ordered event history | 07 |
| POST | /api/v1/programme/jobs/{id}/cancel | Cancel | 07 |
| GET | /api/v1/programme/jobs/{id}/attestation | Signed evidence | 18 |
| GET | /api/v1/programme/jobs/{id}/evidence | Workload and commercial evidence | 18 |
| GET | /api/v1/programme/evidence-keys | Public signing keys (no auth) | 18 |
| POST | /api/v1/programme/datasets | Register an uploaded JSONL file as training data | 13 |
| GET | /api/v1/programme/datasets/{id} | Read a training dataset | 13 |
| POST | /api/v1/programme/external-inputs | Register a file in your storage | 15 |
| GET | /api/v1/programme/external-inputs/{id} | Is the link still ready? | 15 |
| POST | /api/v1/programme/artifacts/{id}/register | Give an uploaded file a model role | 14 |
| POST | /api/v1/programme/model-packages | Freeze a model package | 14 |
| POST | /api/v1/programme/models | Create a release | 14 |
| GET | /api/v1/programme/models, /models/{id} | Releases and qualifications | 14 |
| POST | /api/v1/programme/models/{id}/compatibility-preview | Which runners and GPU classes fit | 14 |
| POST | /api/v1/programme/models/{id}/qualification-requests | Ask for qualification | 14 |
| POST | /api/v1/programme/models/{id}/retire | Retire an unassigned release | 14 |
| GET | /api/v1/programme/model-assignments | Production assignments | 14 |
| PUT | /api/v1/programme/model-assignments/{workloadId} | Assign a qualified release | 14 |
| POST | /api/v1/jobs | Submit private chat | 08 |
| GET | /api/v1/jobs, /jobs/{id}, /jobs/{id}/result | Chat job list, status (?wait=), result | 08 |
| POST | /api/v1/jobs/{id}/cancel | Cancel chat | 08 |
| POST | /api/v1/inference | Chat as a conversation (accezz.inference.v1) | 08 |
| GET | /api/v1/private-chat/health | Chat model readiness | 08 |
| POST | /api/requester/artifacts | Start a file upload | 14 |
| POST | /api/requester/artifacts/{id}/parts, /complete | Multipart part URL, finish upload | 14 |
| GET | /api/requester/artifacts, /{id}/download | List files, 5-minute download link | 14 |
| POST | /api/requester/models | Register a GGUF chat model | 14 |
| DELETE | /api/requester/models/{id} | Delete a chat model and its file | 14 |
| GET | /api/v1/capabilities, /api/v1/protocol | Key capabilities, integration protocol | 06 |
| GET | /api/v1/openapi.yaml, /api/v1/integration-manifest.yaml | Machine-readable API description | 06 |
| GET | /api/v1/account/usage | Account totals | 19 |
A generated JavaScript client covering every programme operation (@accezzcompute/programme-sdk, no dependencies, Node.js 20+) is available on request.
Glossary A–Z
- Approved passage
- A text you send to private chat with its
sourceIdandsourceVersion; the only material the model may answer from. - Assignment
- Which qualified release a workload uses for your account. Changed with
PUT /model-assignments/{workloadId}. - Attestation
- An Ed25519-signed statement of what produced a job's result.
- Budget
- The hard spend cap for a key in a period, in paise.
- Bulk
- A workload that processes a whole file in one job.
- Capabilities
- The live report of what your key can run, your retention, data class and budget.
- Cold start
- The first job after a model was idle: the GPU loads the model first, adding 30–80 seconds.
- Contract version
- The exact, versioned shape of a workload's request and result, such as
text-embedding.v1. - Data class
- What data your key may send:
generated-synthetictoday; a production class after approval. - Digest
- A SHA-256 fingerprint that identifies a file, model release or result exactly.
- Evidence keys
- The public keys you verify attestations with.
- External input
- A file in your own storage, registered by link and read directly by the GPU Worker.
- GGUF
- The single-file format for chat models.
- Hold window
- How long an unread result is kept under keep-nothing (default 60 minutes).
- Idempotency-Key
- The header that makes a retried submit safe: same key and body, same job.
- Keep-nothing
- Retention mode
none: inputs and results are removed shortly after delivery. - Lifecycle
- The job's state and whether it has ended (
lifecycle.terminal). - modelReleaseDigest
- The fingerprint of the exact model that produced a result. Store it with your vectors.
- Problem
- An error response in
application/problem+jsonwith a stablecode. - Programme workload
- Any non-chat model workload, run through
/api/v1/programme. - Qualification
- Our signed test of a release on our GPUs for one contract. Only qualified releases run.
- Release
- An immutable, versioned model built from a package of files.
- requestId
- Your own ID for a request; returned in the result and searchable.
- resultDigest
- The SHA-256 of a result's canonical JSON, inside the attestation.
- Runner
- The signed, sandboxed program on the GPU Worker that runs models; identified by
runnerDigest. - Scope
- A permission on an API key, such as
programme:run. - terminalReason
- Why a programme job failed or was cancelled.
- Wait
- The
?wait=parameter that holds a status call open until the job ends, up to 60 seconds. - Warm
- A model already loaded on the GPU; jobs then take 1.5–3 seconds.
- Worker
- A checked GPU computer that runs your jobs. Its identity is never exposed to you.