Managed training
vertex() tunes Gemini models on Vertex AI with supervised fine-tuning. It's the same fineTune() call as local training, with a different trainer. What changes is who holds your data and your model: Google trains in your project, and the tuned model lives in Vertex.
Vertex AI was picked as the first managed trainer because its tuning API is generally available, takes plain JSONL from Cloud Storage, and returns a model that only exists inside the provider, which exercises the provider-model artifact. OpenAI's fine-tuning platform no longer takes new users.
Setup
- A Google Cloud project with the Vertex AI API enabled.
- A Cloud Storage bucket in the same project for the uploaded datasets:
stagingis ags://prefix in it. - An identity that can create tuning jobs (
roles/aiplatform.user) and write to the bucket (roles/storage.objectCreator).
Authentication
June doesn't depend on Google's SDKs. It calls the Vertex and Cloud Storage REST APIs with fetch, using an OAuth access token you supply:
accessToken: a fixed token, fine for a short script.getAccessToken(): called before every request, so it can hand back a fresh token. Tuning jobs outlast a token's hour.
Get one however suits your environment: gcloud auth print-access-token on a laptop, the metadata server on Google Cloud, or google-auth-library in your own code. Long-lived credentials, such as a service account key, stay with whatever mints the token; June only ever sees short-lived access tokens. It holds a fixed accessToken in memory on the trainer, calls getAccessToken() again for every request rather than caching its result, and sends tokens only in the Authorization header. They never appear in job refs, manifests or errors, and June doesn't write them anywhere.
import { vertex } from "june/trainers";
const trainer = vertex({
project: "acme-ml",
staging: "gs://acme-tuning/june",
getAccessToken: async () =>
(await Bun.$`gcloud auth print-access-token`.text()).trim(),
});
Data leaves your machine
Managed training uploads your datasets to Google, and Google trains on them. Before you run the example below, know what goes where:
- Your examples, converted to Gemini's tuning format, are uploaded to
<staging>/<name>-<id>/train.jsonland, if you pass a validation set,validation.jsonlin your Cloud Storage bucket. June doesn't delete them when the job ends; remove them yourself, or set a lifecycle rule on the bucket, if they shouldn't stay. - The tuning job runs in your Google Cloud project, on Google's infrastructure.
- The tuned model exists only in Vertex, as an endpoint in your project. It never comes back to your machine.
Check that's acceptable for the data before you use it. Local training keeps it all on your machine.
A first fine-tune
import { dataset, fineTune } from "june/train";
import { vertex } from "june/trainers";
declare function getAccessToken(): Promise<string>;
const result = await fineTune({
name: "fonzie",
model: "gemini-2.5-flash",
trainer: vertex({
project: "acme-ml",
region: "us-central1",
staging: "gs://acme-tuning/june",
getAccessToken,
}),
dataset: dataset("./fonzie.jsonl"),
method: "sft",
config: { epochs: 3 },
});
result.artifact;
// {
// type: "provider-model",
// provider: "vertex",
// modelId: "projects/acme-ml/locations/us-central1/endpoints/4567",
// baseModel: "gemini-2.5-flash",
// metadata: { vertex: { tuningJob: "…/tuningJobs/123", endpoint: "…", model: "…" } }
// }
Options
| Option | |
|---|---|
project | the Google Cloud project id |
region | where to tune; us-central1 by default |
staging | a gs://bucket/prefix for uploaded datasets |
accessToken | a fixed OAuth token |
getAccessToken | called before every request for a fresh token; one of these two is required |
retry | { attempts, delay }; see Retries |
baseURL | the Vertex API; https://{region}-aiplatform.googleapis.com/v1 by default |
storageURL | the Cloud Storage API; https://storage.googleapis.com by default |
Methods, models and options
Vertex supports sft on text examples: completion and conversation, without tool messages. Which base models can be tuned, how many examples they need, and their limits are up to Google; see Vertex's list of tunable models.
config.epochs sets the epoch count. Vertex ignores the rest of config (learningRate, batchSize, maxSequenceLength, seed) and every method option, since its tuning API doesn't take them. Vertex-specific settings go in providerOptions.vertex:
| Option | |
|---|---|
adapterSize | the LoRA adapter size: 1, 4, 8 or 16 |
learningRateMultiplier | scales Vertex's recommended learning rate |
labels | labels on the tuning job |
import { dataset, fineTune } from "june/train";
await fineTune({
model: "gemini-2.5-flash",
trainer,
dataset: dataset("./fonzie.jsonl"),
method: "sft",
providerOptions: { vertex: { adapterSize: 4, learningRateMultiplier: 0.5 } },
});
Vertex picks its recommended values for anything unset. Other trainers ignore the vertex namespace, so one call can carry options for several trainers.
What happens
- Each dataset is converted to Gemini's tuning format and uploaded to
<staging>/<name>-<id>/train.jsonl(andvalidation.jsonl). Completion examples become a user turn and a model turn; a conversation's system messages become itssystemInstruction. - June creates a tuning job with the uploaded files and the hyperparameters, and polls it until it settles.
- On success, the artifact's
modelIdis the tuned model's endpoint, which is what you call it by. The tuning job, endpoint and model resource names are inartifact.metadata.vertex.
Vertex job states map to June's: pending and queued are queued; running, paused, updating and cancelling are running; failed, expired and partially succeeded are failed.
Retries
June retries a small, fixed set of responses, up to 3 attempts with exponential backoff from 500 ms (vertex({ retry: { attempts, delay } })), honoring Retry-After up to a minute:
429and503, for every request, since Vertex rejects those before doing anything.500,502and504, and network errors, only for uploads, status checks and cancels, which are safe to repeat. Creating a tuning job isn't retried on those, so a timeout can't start two jobs.
Anything else fails straight away with Vertex's error message.
Provider model artifacts
A provider-model artifact is opaque: it only works inside the provider that made it. Call it with the provider's own AI SDK package and the artifact's modelId; June doesn't ship one. Unlike a local adapter, you can't download it or move it to another provider, and deleting the endpoint in Vertex deletes the model.
Swapping trainers
Datasets, evals and the fineTune() call don't change between trainers. Only the trainer and its namespaced options do:
import { dataset, fineTune } from "june/train";
import { trl, vertex } from "june/trainers";
declare const local: boolean;
const data = dataset("./fonzie.jsonl");
const { artifact } = await fineTune({
model: local ? "Qwen/Qwen3-8B" : "gemini-2.5-flash",
trainer: local
? trl()
: vertex({
project: "acme-ml",
staging: "gs://acme-tuning/june",
accessToken: "…",
}),
dataset: data,
method: "sft",
});
The artifact differs (an adapter or checkpoint from TRL, a provider-model from Vertex), so code that serves the tuned model looks at artifact.type.
Testing
June's tests run the real adapter, over real fetch, against a local HTTP server that emulates Cloud Storage uploads and the tuning API. A smoke test creates a real tuning job and cancels it straight away; it's skipped unless you set JUNE_VERTEX_SMOKE to <project>,gs://<bucket>/<prefix> and are logged in with gcloud:
JUNE_VERTEX_SMOKE=acme-ml,gs://acme-tuning/june make test path=packages/june/src/trainers/vertex