Build a Training Provider
Package a training system as a provider-neutral Connection with exact model discovery, durable reconciliation, and content-addressed checkpoints.
A Training Provider is integration code on the ordinary Connection package substrate. It does not introduce a second integration system. Installation creates a governed Resource with exact code, configuration, Credentials, egress, Policy, and capability identity. Jobs select that exact Resource revision and invoke its asynchronous train operation.
The provider owns model-specific API calls and recovery. Constal owns Dataset and Scorer resolution, admission, idempotency, durable handles, budgets, usage settlement, audit, schedules, and the later checkpoint-to-Model handoff.
Declare discovery metadata
Use trainingProvider(...) to publish the provider's immutable contract. The Console derives its method and base-model pickers from this metadata; it does not carry provider-specific forms.
import {
canonicalJson,
trainingProvider,
type DriverRequest,
type ReconcileRequest,
type TrainingJobFact,
} from "@constal/sdk";
const acmeTraining = trainingProvider({
id: "acme-training",
version: "1.0.0",
kind: "service",
displayName: "Acme Training",
description: "Train adapter weights in an Acme account.",
credentialSlots: {
account: { title: "Acme Credential", provider: "acme-api" },
},
capabilities: [], // trainingProvider adds the exact training capability
modelOffers: [],
training: {
lifecycleVersion: 1,
methods: ["sft", "rl"],
models: [{
id: "base-20b",
name: "Base 20B",
providerModel: "acme/base-20b",
contextTokens: 65536,
pricing: {
currency: "USD",
unit: "million_tokens",
prefillMicroUsd: 200000,
cachedPrefillMicroUsd: 40000,
sampleMicroUsd: 500000,
trainMicroUsd: 600000,
},
}],
checkpoints: {
formats: ["acme-peft"],
resume: true,
fork: true,
},
},
billing: { upstreamPayer: "tenant" },
configSchema: { type: "object", additionalProperties: false },
egress: { rules: [{
operations: ["train"],
origin: "https://api.acme.example",
pathPrefix: "/v1/training/",
methods: ["GET", "POST", "DELETE"],
}] },
catalog: () => [{
op: "train",
description: "Train one immutable recipe.",
schema: {},
resultSchema: {},
effect: "reconcilable",
recovery: { kind: "reconcile" },
async: true,
usage: { meter: "none", priceTable: null },
}],
async invoke(request: DriverRequest, ctx) {
const input = request.args as { provider?: { crn: string; hash: string } };
if (input.provider?.crn !== request.resource.crn ||
input.provider.hash !== request.resource.hash) {
throw new TypeError("training request must pin this provider");
}
const remote = await startAcmeJob(request, await ctx.secret("account"));
await ctx.state.set(`job:${request.handleId}`, { remote });
return { accepted: true };
},
async reconcile(request: ReconcileRequest, ctx) {
const pending = await ctx.state.get<{ remote: string }>(`job:${request.handleId}`);
const remote = await readAcmeJob(pending!.remote, await ctx.secret("account"));
if (!remote.complete) return { status: "in-progress", retryAfterMs: 5000 };
const descriptor = {
provider: "acme",
account: remote.account,
model: remote.model,
revision: remote.revision,
};
const ref = await ctx.store(canonicalJson(descriptor));
const result: TrainingJobFact = completedFact(request, {
step: remote.step,
ref,
format: "acme-peft",
createdAt: Date.now(),
});
return { status: "completed", result, usage: remote.usage };
},
async cancel(request, ctx) {
const pending = await ctx.state.get<{ remote: string }>(`job:${request.handleId}`);
if (pending) await cancelAcmeJob(pending.remote, await ctx.secret("account"));
},
});
export default acmeTraining;startAcmeJob, readAcmeJob, cancelAcmeJob, and completedFact above stand for provider-specific code. Keep that code behind this adapter. The contract requires train to be asynchronous, reconcilable, and cancellable so an uncertain network response cannot create a hidden second job.
Publish checkpoints safely
Each reported checkpoint needs step, ref, format, and createdAt. Store a canonical JSON descriptor of at most 64 KiB with ctx.store(...) and return its native Artifact CAS hash as ref. The descriptor should contain the opaque provider locator and immutable revision needed by a compatible Gateway—not model weights or Credentials.
const descriptor = {
provider: "acme",
account: "tenant-account-42",
model: "jobs/job-91/checkpoints/400",
revision: "sha256:provider-revision",
};
const ref = await ctx.store(canonicalJson(descriptor));
const checkpoint = {
step: 400,
ref,
format: "acme-peft",
createdAt: Date.now(),
};Constal resolves that descriptor from durable storage and verifies its content address before passing it to a Gateway. The Training Provider's Credential remains bound to the provider Resource; the descriptor must never contain an API key, access token, or refresh token.
Use a distinct format name when another Gateway could not consume the checkpoint without conversion. Format matching is exact. Changing supported models, prices, formats, recovery behavior, or code requires a new immutable package revision.
Complete the integration
Package and install the provider like a custom Gateway. Test duplicate delivery, timeout before and after the upstream request, reconciliation, cancellation, provider pin mismatch, usage reporting, checkpoint descriptor stability, resume validation, and terminal upstream failures.
To make successful jobs usable for inference, implement the same format in a checkpoint-importing Gateway. The provider does not create Models, modify Agent bindings, or decide promotion. Those remain explicit platform operations with separate Policy and audit records.