Developer handbook

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.

Base URL https://www.accezzcompute.comAPI v1 (OpenAPI 1.3.0)As of 9 October 2026
01

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.

WorkloadWhat it doesContractStatus
Private chatAnswers a question only from passages you send, and says which passages it used.private-grounded-chat.v1Live
Text embeddingTurns up to 128 texts per job into vectors (384 numbers each with the current model).text-embedding.v1Live
Text classificationPuts one text into your model's labels, with scores.text-classification.v1Live
RerankingOrders up to 512 passages by how well they answer a query.text-reranking.v1On request
Bulk embeddingEmbeds a whole file of texts (JSONL, JSON, CSV or Parquet) in one job.bulk-embedding.v1Live
Bulk classificationClassifies many texts in one job.bulk-text-classification.v1Live
LoRA / QLoRA trainingTeaches a small model to answer like your examples, from a JSONL file of prompts and responses.lora-training.v1, qlora-training.v1LoRA live · QLoRA needs a larger GPU
Adapter merge, conversion, quantisationMerges a trained adapter, converts to GGUF, or shrinks a model.adapter-merge.v1 and othersNot yet
Offline evaluationScores a candidate model against a baseline on a frozen test set.offline-evaluation.v1On request
Your account

Your data class, data-retention setting and prices are set per account. Read them live from programme capabilities rather than from this page.

02

First call in 5 minutes

  1. 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.
  2. Check what the key can run.
  3. 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"

If status for text-embedding is ready and the last line prints 384, your integration works end to end.

03

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

  1. queuedsubmitted
  2. reservedGPU found
  3. dispatchedsent to a Worker
  4. runningWorker started
  5. 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).

04

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.

Server side only

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.

ScopeAllowsNeeded for
jobs:readRead jobs, results, events, capabilitiesEvery integration
jobs:writeSubmit jobsEvery workload
jobs:cancelCancel jobsCancelling chat jobs
programme:runRun embedding, reranking, classification, bulk and model workloadsAll /api/v1/programme workloads
inference:runLegacy chat only (switched off); chat now needs programme:run—
models:statusLegacy chat readiness (switched off); use GET /api/v1/programme/capabilities—
artifacts:readList and download your files and chat modelsModel management
artifacts:writeUpload files, register and delete modelsBringing your own model
usage:readRead account usage/api/v1/account/usage
production:runSend real (non-synthetic) data to private chatOnly 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_LIMIT with a Retry-After header.
  • 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.

05

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 POST that creates a job needs an Idempotency-Key header of 8 to 128 characters. Use a UUID; using your requestId is 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 requestId filter 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>.
06

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.

GET/api/v1/programme/capabilitiesjobs:read
Response (shortened)
{
  "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 }
  ]
}
FieldWhat to do with it
workloads[].statusready 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.modenone = keep nothing after delivery; standard = job data kept. Refuse to send data if it is not what you expect.
dataClassgenerated-synthetic = synthetic data only. Send real patient data only when this reports a production class.
budgetThe hard spend cap for this key in this period, in paise. null = no budget, so programme jobs are refused.
customerStorageHostsStorage hosts you may give us file links from (see section 15).

Other discovery endpoints

EndpointReturns
GET /api/v1/capabilitiesScopes, limits and chat workloads for your key
GET /api/v1/protocolThe integration protocol: scopes, contracts, limits, lifecycle, examples, error contract
GET /api/v1/openapi.yamlThe full OpenAPI 3.1 description (needs jobs:read). Generate clients from it.
GET /api/v1/integration-manifest.yamlThe OpenAPI file plus your key's effective scopes, workloads, limits and data handling
GET /healthService is up (no key needed)
07

The job loop

Every programme workload uses the same five endpoints. Put this helper in your code once and call it for every workload.

StepEndpointScopes
SubmitPOST /api/v1/programme/jobs with { "kind", "request" }jobs:write, programme:run
WaitGET /api/v1/programme/jobs/{id}?wait=60jobs:read
Read resultGET /api/v1/programme/jobs/{id}/resultjobs:read
CancelPOST /api/v1/programme/jobs/{id}/canceljobs:write
Find after a crashGET /api/v1/programme/jobs?requestId=…jobs:read, programme:run
Event historyGET /api/v1/programme/jobs/{id}/events?after=0&limit=100jobs: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());
}

The job object

GET /api/v1/programme/jobs/{id}
{
  "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.

08

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.

POST/api/v1/programme/jobsjobs:write, programme:run
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;
}

Request fields (request)

FieldRule
contractVersionRequired. private-grounded-chat.v1
requestIdRequired. 8–120 characters, A–Z a–z 0–9 . _ : -
modelRequired. Your chat model's release ID (it must be qualified for private-grounded-chat.v1)
messagesRequired. 1–64 turns of { role: user|assistant, content (≤16,000) }, ending with a user turn
approvedPassagesRequired. 1–64 items of { sourceId (≤120, unique), sourceVersion, text }
generationRequired. { 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).
cacheRequired. { 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

GET /api/v1/programme/jobs/{id}/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 citations is 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.

09

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.

POST/api/v1/programme/jobs · kind embeddingjobs:write, programme:run
request
{
  "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" }
  }
}
FieldRule
items1–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.normalizationl2 (unit-length vectors, for cosine or dot-product search) or none
output.encodingfloat32 (JSON numbers in vector) or base64-f32le (little-endian float32 bytes in vectorBase64, about 3× smaller)
result
{
  "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": []
}
Store the model fingerprint with every vector

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.

10

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.

  1. Put the texts in one file, one record per text with id and text. Accepted formats: .jsonl (one JSON object per line), .json (an array, or { "items": [...] }), .csv (columns id,text) or .parquet.
  2. 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).
  3. Submit the job with that ID as inputArtifactId.
request
{
  "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.

11

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.

request
{
  "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.

result.result
{
  "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.

12

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.

request
{
  "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).

result.result
{
  "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:…"
}
  • abstained means the model is not sure; labels is then empty and reason says why. Route these to a person.
  • Scores are between 0 and 1. Texts are never silently truncated (truncated is always false).

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.

13

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.

examples.jsonl
{"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

POST/api/v1/programme/datasetsartifacts:write
request
{ "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_INVALID names the first bad line (line in the response): not JSON, not an object, or a missing prompt/response. Limits: 1,000,000 rows, 256 MiB, 64,000 characters per field, 32 columns.
  • 400 TRAINING_DATASET_ATTESTATION_REQUIRED: both attestations must be true. 400 TRAINING_DATASET_FORMAT_UNSUPPORTED: only jsonl today.
  • 404 FILE_NOT_FOUND, 409 ARTIFACT_NOT_CLEAN (the upload has not finished verification).

3 · Submit the training job

POST /api/v1/programme/jobs
{
  "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, maxSteps or epochs passes over your data. One step uses batchSize × gradientAccumulationSteps examples.
  • 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 seed give 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

result.result (live run, 9 October 2026)
{
  "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.

KindRequest fieldsStatus
adapter-mergecontractVersion, requestId, sourceReleaseId, adapterReleaseId, targetProfile (safetensors-f16 | safetensors-bf16), limitsNot yet
model-conversioncontractVersion, requestId, sourceReleaseId, targetProfile (gguf-f16 | gguf-bf16), limitsNot yet
model-quantisationcontractVersion, requestId, sourceReleaseId, targetProfile (gguf-q8_0 | gguf-q6_k | gguf-q5_k_m | gguf-q4_k_m), importanceMatrixArtifactId (or null), limitsNot yet
offline-evaluationcontractVersion, 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.

14

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.

  1. Upload files
  2. Register roles
  3. Package
  4. Release
  5. Qualify we run it
  6. Assign

1. Upload each file

POST/api/requester/artifactsartifacts:write
request → response
{ "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.mode tells you which mode applies): PUT the bytes to upload.url with exactly the headers given, within 15 minutes.
  • Multipart mode (larger files): for each part, POST /api/requester/artifacts/{id}/parts with { "partNumber": n }, PUT that slice (size upload.partSize) to the returned URL, and keep its ETag response header.
  • Then POST /api/requester/artifacts/{id}/complete (multipart: { "parts": [{ "partNumber": 1, "etag": "…" }, …] } in order). The file is checked and scanned; poll GET /api/requester/artifacts until it is verified and 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

POST/api/requester/modelsartifacts:write

{ "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

StepEndpointBody
2 · Register each file's rolePOST /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 packagePOST /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 releasePOST /api/v1/programme/models{ packageId, name, version, family, architecture, configuration, licenceAttestation, sourceAttestation }
5 · Ask for qualificationPOST /api/v1/programme/models/{id}/qualification-requestsUsually done by us. Becomes qualified only after signed test evidence from our GPU is verified.
6 · Assign to a workloadPUT /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)
What each model type must contain

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/models and GET /api/v1/programme/models/{id}: the release (name, version, family, architecture, package and release digests, format, licence declaration, label IDs for classifiers, state) and its qualifications. A release is usable for a workload only when it has a qualification with that exact contractVersion, state: "qualified" and invalidatedAt: null.
  • GET /api/v1/programme/model-assignments: which release each workload uses.
  • POST /api/v1/programme/models/{id}/retire: retire an unassigned release.
15

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.

POST/api/v1/programme/external-inputsjobs:write, programme:run
request → response
{ "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 customerStorageHosts in capabilities). Ask us to add your storage host.
  • expiresAt must 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), dataset or evaluation-input. Files up to 8 GiB.
  • Use the returned id wherever a job takes a file ID (for example inputArtifactId). If the link has expired when the job runs, the job fails with EXTERNAL_INPUT_UNAVAILABLE; register a fresh link and resubmit.
  • GET /api/v1/programme/external-inputs/{id} shows whether the link is still ready or expired.
Where results go

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.

16

Results and output files

GET/api/v1/programme/jobs/{id}/resultjobs:read

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.
GET/api/v1/programme/jobs/{id}/outputs/{artifactId}jobs:read

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.

17

Data handling and retention

DataKeep-nothing (mode: none)Standard (mode: standard)
Inputs (questions, passages, texts)Removed about 1 minute after the job endsKept with the job (upper limit 1 year)
ResultsRemoved 5 minutes after your first read, or after the hold window (default 60 minutes) if never readKept with the job (upper limit 1 year)
Output filesDeleted after the hold windowKept with the job
Files in your storageNever 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 dataClass in capabilities will report.
  • Backups: database backups are not yet switched on. When they are, keep-nothing content will be kept out of them.
18

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.

GET/api/v1/programme/jobs/{id}/attestationjobs:read
GET/api/v1/programme/evidence-keysno key needed
attestation (real, 8 Oct 2026)
{
  "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

  1. Encode statement as canonical JSON: object keys sorted at every level, no spaces, UTF-8, strings escaped as JSON.stringify does.
  2. Verify the base64 signature over those bytes with the public key from /evidence-keys whose keyId matches.
  3. Check resultDigest: the SHA-256 (hex) of the canonical JSON of the result object you received from /result. Numbers must be written the way JavaScript writes them (for example 0.00001 becomes 1e-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;
}

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.

19

Budgets and charges

  • Programme workloads are admitted only within the key's active budget (budget in 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 }. state moves from pending or evidence-dependent to charged or not-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.

20

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.

example
{ "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 }
CodeStatusRetryMeaning and what to do
INVALID_REQUEST, *_SCHEMA_INVALID, *_INVALID400NoA field broke a rule. Fix the request.
CONTRACT_VERSION_UNSUPPORTED400NoKind and contractVersion do not match
IDEMPOTENCY_KEY_INVALID400NoMissing key, or not 8–128 characters
REQUEST_ID_INVALID400NoMalformed requestId filter on the job list
BUDGET_EXCEEDED400NoWould exceed the budget; raise it or wait for the next period
UNAUTHORIZED401NoKey missing, wrong or revoked
API_KEY_SPEND_LIMIT402NoChat: the key's monthly cap would be exceeded
API_KEY_SCOPE_REQUIRED403NoKey lacks a scope; requiredScope names it
API_KEY_WORKLOAD_REQUIRED, WORKLOAD_NOT_ENTITLED403NoWorkload not allowed for the key or the account
DATA_HOSTS_NOT_CONFIGURED, DATA_HOST_NOT_ALLOWED403NoStorage host not on your allowed list
JOB_NOT_FOUND404NoNo such job for this account
MODEL_RELEASE_NOT_FOUND404NoNo model release with that ID belongs to your account
MODEL_RELEASE_UNAVAILABLE409NoThe model is retired or its files are gone
MODEL_QUALIFICATION_REQUIRED, WORKLOAD_MODEL_UNAVAILABLE409 / 400NoThe model is not qualified for this workload on the current GPUs; request qualification first
TRAINING_*_OVER_BUDGET400NoA training setting is above the qualified limit; POST /api/v1/programme/training/preflight lists the limits
CHAT_CONTEXT_TOO_LARGEjobNoPassages plus answer do not fit the model's context window; send fewer or shorter passages
IDEMPOTENCY_CONFLICT409NoSame key, different body. Use a new key for a new request.
RESULT_NOT_READY409LaterWait for the job to complete
JOB_NOT_CANCELLABLE409NoJob already ended
JOB_NOT_FINISHED409LaterAttestation is available once the job ends
WORKLOAD_PAUSED409LaterWorkload switched off by AccezzCompute
EXTERNAL_INPUT_CONFLICT409NoSame checksum registered with a different size
WORKLOAD_NOT_AVAILABLE409NoThe contract exists but cannot run yet
TRAINING_DATASET_INVALID400NoA line of the training file is not a JSON object with prompt and response; line says which
TRAINING_DATASET_ATTESTATION_REQUIRED400NoSet licenceAttested and consentAttested to true
TRAINING_DIVERGEDjobNoTraining loss stopped being a number; lower learningRate and resubmit
MODEL_RELEASE_LABELS_REQUIRED400NoA classifier release has no labels; upload a model fine-tuned for classification
MODEL_RELEASE_CONFIGURATION_INCOMPLETE400NoAn embedding or reranker release lacks what it needs to run
IDEMPOTENCY_RECORD_EXPIRED410NoKey older than 24 hours; reconcile by requestId
RESULT_DELETED410NoRemoved under keep-nothing retention
PAYLOAD_TOO_LARGE413NoA size limit was exceeded
API_KEY_RATE_LIMIT429YesToo many requests this minute; wait Retry-After seconds
API_KEY_CONCURRENCY_LIMIT429YesToo many active jobs on this key
CAPACITY_UNAVAILABLE503YesNo GPU can take interactive work right now; resend after Retry-After (15 s) with the same Idempotency-Key
SERVICE_UNAVAILABLE, EVIDENCE_SIGNING_UNAVAILABLE503YesTemporarily 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)

CodeMeaningCharged
QUEUE_EXPIREDNo GPU picked the job up in time (6 hours for programme jobs)No
REQUESTEDYou cancelled itNo
PROGRAMME_EXECUTION_FAILEDThe model runner reported a failureNo
RESULT_CONTRACT_INVALIDThe result did not pass verification and was rejectedNo
PROVENANCE_MISMATCHThe GPU's model or runner fingerprint did not match the scheduled oneNo
EXTERNAL_INPUT_UNAVAILABLEYour storage link had expired or the file was goneNo
EXECUTION_RETRIES_EXHAUSTEDThe job stopped the GPU on three attempts in a row (six for training); check the input, then contact supportNo
PROGRAMME_RUNNER_DEADLINEThe job ran past its time limit; send a smaller input or a larger limits.maxWallSecondsNo

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), honours Retry-After, and never repeats a write without an Idempotency-Key.
  • 429: wait the Retry-After seconds.
  • 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.
21

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.

WorkloadModel already loadedFirst job after the model was unloaded
Private chat (short answer)4–6 sabout 90–145 s
Text embedding (1–2 texts)1.7–2.5 sabout 50–120 s
Classificationabout 3 sabout 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-warm with an idle time (1 hour to 7 days) through PATCH /api/v1/programme/models/{id}/execution-settings?workloadId=…&environment=production or the console, and repeat requests are served in seconds. Chat can also ask per request with cache.mode: "warm". prefer-warm falls 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_UNAVAILABLE with Retry-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=60 calls, 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.
22

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

  1. Your server sends passages to embedding (128 per job) or bulk-embedding (a file).
  2. AccezzCompute returns vectors and the modelReleaseDigest.
  3. Your server stores the vectors in its own database.

Each question

  1. The patient asks your server a question.
  2. Your server sends the question to embedding and gets its vector.
  3. Your server searches its own vectors for the top 20 passages.
  4. Your server sends the question and those passages to reranking (topN 5).
  5. Your server sends the question and the best passages to private chat.
  6. AccezzCompute returns a draft answer and sourceIdsUsed; your server checks it and replies.
JavaScript, using runJob and askFromPassages from sections 7 and 8
// 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 modelReleaseDigest as the question vector.
  • Re-index only new or changed documents; an unchanged document never needs to be sent again.
23

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 by requestId after a crash.
  • Wait calls use ?wait=60 with a 90-second HTTP timeout; status-call failures are retried.
  • Errors are handled by code; 429 obeys Retry-After.
  • Results are read promptly (keep-nothing removes them) and stored with their modelReleaseDigest.
  • Chat answers with insufficient_context or safety_rejection are never shown as answers.

Checks against capabilities before each session

  • workloads[].status is ready for what you need.
  • dataRetention.mode and dataClass match what your approval allows.
  • budget is not null and has room.

Before real patient data

  • Data processing agreement signed; dataClass reports a production class.
  • Your key has production:run and chat requests send dataMode: "production".
  • Your own evaluation of the chat and classifier models is recorded.
24

Limits at a glance

LimitValue
Requests per minute per key (default)60
Concurrent active jobs per key (default)5
Idempotency-Key length · remembered for8–128 characters · 24 hours
requestId8–100 characters, A–Z a–z 0–9 . _ -
wait on status calls0–60 seconds
Embedding: items per job · text length128 · 32,000 characters
Bulk embedding: items per fileup to 10,000,000
Reranking: documents · text length512 · 32,000 characters
Classification: text32,000 characters and 128 KiB
Chat: question · passages · passage text8,000 characters · 32 · 24,000 characters
Chat: history · maxTokens8 turns (4,000 characters each) · 512
Programme job queue timeout6 hours, then QUEUE_EXPIRED
Chat queue timeoutabout 5 minutes
Uploaded file size20 GiB (large files use multipart upload)
Upload and part URLs15 minutes
Your-storage link (external input)1 minute to 24 hours ahead · files up to 8 GiB
Output file download link15 minutes
Keep-nothing: inputs · resultsabout 1 minute after the job · 5 minutes after first read or 60-minute hold
25

Endpoint index

MethodPathPurposeSection
GET/api/v1/programme/capabilitiesWhat this key can run, retention, data class, budget06
POST/api/v1/programme/jobsSubmit a programme job07
GET/api/v1/programme/jobsList jobs (limit, beforeCreatedAt, beforeId, requestId)07
GET/api/v1/programme/jobs/{id}Job status (?wait=)07
GET/api/v1/programme/jobs/{id}/resultVerified result and output list16
GET/api/v1/programme/jobs/{id}/outputs/{artifactId}15-minute download link16
GET/api/v1/programme/jobs/{id}/eventsOrdered event history07
POST/api/v1/programme/jobs/{id}/cancelCancel07
GET/api/v1/programme/jobs/{id}/attestationSigned evidence18
GET/api/v1/programme/jobs/{id}/evidenceWorkload and commercial evidence18
GET/api/v1/programme/evidence-keysPublic signing keys (no auth)18
POST/api/v1/programme/datasetsRegister an uploaded JSONL file as training data13
GET/api/v1/programme/datasets/{id}Read a training dataset13
POST/api/v1/programme/external-inputsRegister a file in your storage15
GET/api/v1/programme/external-inputs/{id}Is the link still ready?15
POST/api/v1/programme/artifacts/{id}/registerGive an uploaded file a model role14
POST/api/v1/programme/model-packagesFreeze a model package14
POST/api/v1/programme/modelsCreate a release14
GET/api/v1/programme/models, /models/{id}Releases and qualifications14
POST/api/v1/programme/models/{id}/compatibility-previewWhich runners and GPU classes fit14
POST/api/v1/programme/models/{id}/qualification-requestsAsk for qualification14
POST/api/v1/programme/models/{id}/retireRetire an unassigned release14
GET/api/v1/programme/model-assignmentsProduction assignments14
PUT/api/v1/programme/model-assignments/{workloadId}Assign a qualified release14
POST/api/v1/jobsSubmit private chat08
GET/api/v1/jobs, /jobs/{id}, /jobs/{id}/resultChat job list, status (?wait=), result08
POST/api/v1/jobs/{id}/cancelCancel chat08
POST/api/v1/inferenceChat as a conversation (accezz.inference.v1)08
GET/api/v1/private-chat/healthChat model readiness08
POST/api/requester/artifactsStart a file upload14
POST/api/requester/artifacts/{id}/parts, /completeMultipart part URL, finish upload14
GET/api/requester/artifacts, /{id}/downloadList files, 5-minute download link14
POST/api/requester/modelsRegister a GGUF chat model14
DELETE/api/requester/models/{id}Delete a chat model and its file14
GET/api/v1/capabilities, /api/v1/protocolKey capabilities, integration protocol06
GET/api/v1/openapi.yaml, /api/v1/integration-manifest.yamlMachine-readable API description06
GET/api/v1/account/usageAccount totals19

A generated JavaScript client covering every programme operation (@accezzcompute/programme-sdk, no dependencies, Node.js 20+) is available on request.

26

Glossary A–Z

A
Approved passage
A text you send to private chat with its sourceId and sourceVersion; 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.
B
Budget
The hard spend cap for a key in a period, in paise.
Bulk
A workload that processes a whole file in one job.
C
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.
D
Data class
What data your key may send: generated-synthetic today; a production class after approval.
Digest
A SHA-256 fingerprint that identifies a file, model release or result exactly.
E
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.
G
GGUF
The single-file format for chat models.
H
Hold window
How long an unread result is kept under keep-nothing (default 60 minutes).
I
Idempotency-Key
The header that makes a retried submit safe: same key and body, same job.
K
Keep-nothing
Retention mode none: inputs and results are removed shortly after delivery.
L
Lifecycle
The job's state and whether it has ended (lifecycle.terminal).
M
modelReleaseDigest
The fingerprint of the exact model that produced a result. Store it with your vectors.
P
Problem
An error response in application/problem+json with a stable code.
Programme workload
Any non-chat model workload, run through /api/v1/programme.
Q
Qualification
Our signed test of a release on our GPUs for one contract. Only qualified releases run.
R
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.
S
Scope
A permission on an API key, such as programme:run.
T
terminalReason
Why a programme job failed or was cancelled.
W
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.