Guide

# Migrate from OpenAI

Gridline's Files, fine-tuning and Batch APIs follow OpenAI's. Point the OpenAI SDK at Gridline's base URL with a Gridline token, change the model names, and check the differences below.

## Overview

OpenAI's API stops accepting new fine-tuning jobs on 6 January 2027, and models fine-tuned from gpt-5 or o3 shut down with those models on 11 December 2026 (OpenAI's [deprecations page](https://developers.openai.com/api/docs/deprecations)).

1. Set the SDK's base URL to Gridline's and its API key to a Gridline token.
2. Upload the same files: chat-format training files with `purpose=fine-tune`, batch input with `purpose=batch`.
3. Name a Gridline base model where you named a GPT model. A job that succeeds names its model `ft:<base>:<org>:<job>`; use that in batches.
4. Check the fields and features Gridline does not offer: each is refused by name, never silently ignored.

## Point your client at Gridline

Python, with the official `openai` package:

```
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.gridlinegpu.com/openai/v1",
    api_key=os.environ["GRIDLINE_TOKEN"],
)
```

Node, with the official `openai` package (JavaScript; in TypeScript, Gridline's extra fields below need a cast):

```
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.gridlinegpu.com/openai/v1",
  apiKey: process.env.GRIDLINE_TOKEN,
});
```

Both SDKs also read the base URL and key from the environment, so code that builds a client with no arguments needs no change:

```
export OPENAI_BASE_URL="https://api.gridlinegpu.com/openai/v1"
export OPENAI_API_KEY="$GRIDLINE_TOKEN"
```

Keep `/openai/v1` in the base URL. The OpenAI-compatible routes are all under it, so a request without it, such as `https://api.gridlinegpu.com/v1/models`, answers `404`. The models list is `https://api.gridlinegpu.com/openai/v1/models`.

## Your token

[Sign up](https://gridlinegpu.com/signup), verify your email and add credits by card at [Get started](https://gridlinegpu.com/get-started). Then create a token with the Gridline CLI. `gridline setup --apply` asks you to approve it in your browser, then saves a private token, valid for 30 days:

```
python -m pip install 'gridlinegpu[source-contract]>=0.3.2,<0.4'
gridline setup --apply
export GRIDLINE_TOKEN="$(cat ~/.config/gridline/python-token)"
```

Send it as the bearer token, where you sent your OpenAI key. OpenAI's organization and project settings have no Gridline equivalent; leave them unset.

## Check before you upload

The Gridline CLI checks your OpenAI training and validation files and your OpenAI job offline. It lists every line Gridline's upload would refuse, each job field Gridline would refuse or treat differently, and prints the equivalent Gridline call. Nothing is sent. `--job` takes the job's create body, or the job object OpenAI returns for `client.fine_tuning.jobs.retrieve`, saved as JSON:

```
gridline migrate openai train.jsonl \
  --validation-file valid.jsonl \
  --job openai-job.json \
  --model qwen2.5-7b-instruct
```

It exits 0 when Gridline would accept the files and the job, and 1 otherwise. The API's answer at upload and at create decides.

## Files

`client.files.create` works unchanged. The purposes:

| purpose | For |
| --- | --- |
| `fine-tune` | Training and validation files: one chat conversation per line, a messages list, as OpenAI's chat fine-tuning format. |
| `batch` | Batch input: one request per line, with custom_id, method, url and body. |
| `batch_output` | Written by Gridline for a batch's output and error files. You download them; you never upload with it. |

A fine-tuning job's training and validation files are each at most 64 MiB. A batch input file is at most 200 MB and 50,000 requests.

## Model names

Gridline fine-tunes and serves open models. Name one of these where you named a GPT model:

| Model | Weights | Fine-tuning | Batch endpoint |
| --- | --- | --- | --- |
| `qwen2.5-0.5b-instruct` | Qwen/Qwen2.5-0.5B-Instruct (Apache-2.0) | Yes | `/v1/chat/completions` |
| `qwen2.5-1.5b-instruct` | Qwen/Qwen2.5-1.5B-Instruct (Apache-2.0) | Yes | `/v1/chat/completions` |
| `qwen2.5-7b-instruct` | Qwen/Qwen2.5-7B-Instruct (Apache-2.0) | Yes | `/v1/chat/completions` |
| `mistral-7b-instruct-v0.3` | mistralai/Mistral-7B-Instruct-v0.3 (Apache-2.0) | Yes | `/v1/chat/completions` |
| `qwen3-8b` | Qwen/Qwen3-8B (Apache-2.0) | Yes | `/v1/chat/completions` |
| `llama-3.1-8b-instruct` | RedHatAI/Llama-3.1-8B-Instruct (Llama 3.1 Community License) | Yes | `/v1/chat/completions` |
| `gpt-oss-20b` | openai/gpt-oss-20b (Apache-2.0) | Yes (adapter download only) | `/v1/chat/completions` |
| `qwen3.5-9b` | Qwen/Qwen3.5-9B (Apache-2.0) | Yes (adapter download only) | `/v1/chat/completions` |
| `qwen3.8-27b` | Qwen/Qwen3.8-27B-FP8 (Apache-2.0) | No | `/v1/chat/completions` |
| `nemotron-3.5-lightning-30b-a3b` | nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4 (OpenMDW-1.1) | No | `/v1/chat/completions` |
| `gemma4-26b-a4b-it` | google/gemma-4-26B-A4B-it (Apache-2.0) | No | `/v1/chat/completions` |
| `qwen3-embedding-0.6b` | Qwen/Qwen3-Embedding-0.6B (Apache-2.0) | No | `/v1/embeddings` |
| `bge-m3` | BAAI/bge-m3 (MIT) | No | `/v1/embeddings` |

A fine-tuned model is named `ft:<base>:<org>:<job-suffix>`, where `<job-suffix>` is the first 12 hex characters of the job id after `ftjob-`:

```
ft:qwen2.5-7b-instruct:o3f9a1c2d4e5:7b2c9e41a0d3
```

Your `suffix` is not part of the name. A fine-tuned model is a chat model: name it in `/v1/chat/completions` batch lines. `GET /models` lists the catalog and your `ft:` models; `DELETE /models/{model}` deletes one.

## Fine-tuning jobs

`client.fine_tuning.jobs.create` takes OpenAI's fields. Gridline trains a LoRA or QLoRA adapter on the base model:

| Field | On Gridline | Notes |
| --- | --- | --- |
| `model` | Different | A Gridline base model id from the table above. |
| `training_file` | As OpenAI | A file id uploaded with purpose=fine-tune. |
| `validation_file` | As OpenAI | Optional. Without one, the job holds out 5% of the training rows to measure eval loss. |
| `hyperparameters` | As OpenAI | The table below. Send it at the top level or under method.supervised.hyperparameters, once. |
| `method` | Different | {"type": "supervised"} or {"type": "dpo"} (preference rows); reinforcement is refused. Gridline adds the adapter: method.supervised.adapter (method.dpo.adapter), or method as the string "lora" or "qlora". |
| `suffix` | Ignored | Accepted (at most 64 characters) and kept on the job, but not part of the model name: the job records a suffix_ignored event. |
| `seed` | As OpenAI | An integer from 0 to 2,147,483,647. Default 0. |
| `metadata` | As OpenAI | At most 16 string pairs. |
| `integrations` | Different | Accepted only as null or []: Gridline offers no Weights & Biases or other integration. |

Any other field is refused with `Unsupported parameter` and its name. The hyperparameters, OpenAI's and Gridline's:

| Hyperparameter | From | Range | Default |
| --- | --- | --- | --- |
| `n_epochs` | OpenAI | 1 to 10 | 1 (also "auto") |
| `learning_rate_multiplier` | OpenAI | 0.01 to 10 Times the base learning rate, 2e-4. | 1 (also "auto") |
| `batch_size` | OpenAI | 1 to 64 | 8 (also "auto") |
| `learning_rate` | Gridline | 1e-6 to 0.01 Instead of the multiplier, never with it. | 2e-4 |
| `lora_rank` | Gridline | 1 to 64 Up to 64 with method lora or qlora (adapters up to 383 MiB; on a 7B or 8B base, QLoRA runs on a 48 GB GPU at the LoRA rate at any rank); with DPO, at most the base's max rank, 24 to 64, so its adapter fits 128 MiB. | 16 |
| `lora_alpha` | Gridline | 1 to 256 | 32 |
| `max_seq_length` | Gridline | 128 to 8,192 Up to 8,192 for supervised tuning on Qwen2.5-7B, Qwen3-8B and Llama 3.1 8B. Above 2,048, use rank 16 or lower. DPO and other bases have a 2,048-token limit. | 2,048 |

Gridline's hyperparameters go in the same dictionary; the SDKs pass them through. Python:

```
training = client.files.create(file=open("train.jsonl", "rb"), purpose="fine-tune")
job = client.fine_tuning.jobs.create(
    model="qwen2.5-7b-instruct",
    training_file=training.id,
    method={
        "type": "supervised",
        "supervised": {"adapter": "qlora", "hyperparameters": {"n_epochs": 2, "lora_rank": 32}},
    },
)
print(job.gridline["quote"])  # creating the job started it; the quote shows what it can cost
```

Node:

```
import fs from "node:fs";

const training = await client.files.create({
  file: fs.createReadStream("train.jsonl"),
  purpose: "fine-tune",
});
const job = await client.fineTuning.jobs.create({
  model: "qwen2.5-7b-instruct",
  training_file: training.id,
  hyperparameters: { n_epochs: 2, lora_rank: 32 },
});
```

- Every job object carries Gridline's fields under `gridline`: the quote when it is created (an estimate, never a price) and, once it succeeds, the adapter and the eval loss before and after training.
- A job must finish within 2 hours; create refuses one estimated longer.One job trains at a time per account. New accounts can have 1 open at once, other accounts 3, unless Gridline has set a different limit; a refused create gives yours in error.limit.
- Download the adapter of a model that succeeded, two files: `GET /gridline/adapters/{model}/files/adapter_model.safetensors` and `.../adapter_config.json`, under the base URL (`https://api.gridlinegpu.com/openai/v1/gridline/adapters/...`).
- A QLoRA adapter is trained against a 4-bit quantized base and served on the bf16 base, so its outputs can differ slightly from what training measured.

The [Fine-tuning API](https://gridlinegpu.com/docs/fine-tuning) page has the training format, the quote and the errors.

## Batches

`client.batches.create` works unchanged. `completion_window` is `24h` or `72h` only, and `endpoint` is one of `/v1/chat/completions` or `/v1/embeddings`. Input lines keep OpenAI's shape: `custom_id`, `method`, `url` and `body`.

```
batch_input = client.files.create(file=open("requests.jsonl", "rb"), purpose="batch")
batch = client.batches.create(
    input_file_id=batch_input.id,
    endpoint="/v1/chat/completions",
    completion_window="24h",
)
```

```
const input = await client.files.create({
  file: fs.createReadStream("requests.jsonl"),
  purpose: "batch",
});
const batch = await client.batches.create({
  input_file_id: input.id,
  endpoint: "/v1/chat/completions",
  completion_window: "24h",
});
```

- One base model per batch. A chat batch may also name up to 8 of your `ft:` models of that base (fewer on some bases: [Models](https://gridlinegpu.com/docs/models#serving)), line by line in `body.model`.
- These request fields are refused at validation, never ignored: `tools`, `tool_choice`, `parallel_tool_calls`, `functions`, `function_call`, `stream`, `stream_options`, `logit_bias`, `service_tier`, `store`, `prediction`, `audio`, `modalities`, `metadata`, `reasoning_effort`, `web_search_options`, `suffix`, `best_of`, `dimensions`. On chat lines `response_format` is accepted as `text`, `json_object` or `json_schema` (see [Structured outputs](https://gridlinegpu.com/docs/batch-api#structured)); `user` is accepted and ignored.
- Output and error files are written as with OpenAI; download them with `client.files.content`.
- `72h` is Gridline's flex window, at the same price as `24h`. The OpenAI Python SDK's type admits only `"24h"`, so pass `completion_window="24h", extra_body={"completion_window": "72h"}` (see [Completion window](https://gridlinegpu.com/docs/batch-api#completion-window)).

The [Batch API](https://gridlinegpu.com/docs/batch-api) page has the line format, statuses, limits and retention.

## Usage and limits by API key

Each batch and fine-tuning job records the API key that created it. `GET /gridline/usage/keys` lists your keys with their batches, fine-tuning jobs, tokens (input, output, trained), the amounts held and charged, and their limits. `from` and `to` are UTC days (YYYY-MM-DD), at most 31 days in all; without them, this month so far. Batches and jobs made before per-key tracking are one line with `api_key_id` null.

`GET /gridline/usage/keys/{key}` pages one key's batches and jobs, newest first (`limit` 1 to 100, default 20; `after` a batch or job id), with the key's spend today and in total. `{key}` is a key id from the list, or `current` for the key you call with.

The account owner can give each key a daily and a total spend limit when signed in to Gridline, under Usage by API key. A create that would pass one is refused with HTTP 429, `error.code` `limit_exceeded` and `error.limit` naming the limit, the key, its spend and the maximum; nothing is created. A daily refusal carries `Retry-After` until 00:00 UTC.

| Limit | Counts | Resets | At most |
| --- | --- | --- | --- |
| `api_key_daily_spend_usd` | The key's spend in the current UTC day. | At 00:00 UTC. | 1,000 USD |
| `api_key_total_spend_usd` | The key's spend over its life. | Never. | 100,000 USD |

How the limits behave today:

- Limits are checked when a batch or fine-tuning job is created, not while it runs: a batch already running can finish above its key's limit.
- A batch or job counts toward its key's spend once it starts and reserves credit. Batches and jobs created but not yet started do not count yet, so several created close together, or at the same time, can each pass the same limit.
- A batch's cost is not known when it is created, so one batch can pass a limit by up to its own cost. A fine-tuning job is checked with its quoted maximum.
- Your account's credit and the account's own limits still bound what every key can spend.

## Routes

Every route under `https://api.gridlinegpu.com/openai/v1`:

| Route | Shape | Use |
| --- | --- | --- |
| `POST /files` | OpenAI's | Upload a file with purpose=fine-tune or purpose=batch. |
| `POST /files/distill` | Gridline's | Turn a batch's requests and results into fine-tune training and validation files. |
| `GET /files` | OpenAI's | List your files. |
| `GET /files/{file_id}` | OpenAI's | Retrieve a file object. |
| `GET /files/{file_id}/content` | OpenAI's | Download a file, such as a batch's output. |
| `DELETE /files/{file_id}` | OpenAI's | Delete a file. |
| `POST /fine_tuning/jobs` | OpenAI's | Create a fine-tuning job (fields below). |
| `GET /fine_tuning/jobs` | OpenAI's | List your jobs. |
| `GET /fine_tuning/jobs/{job_id}` | OpenAI's | Retrieve a job. |
| `GET /fine_tuning/jobs/{job_id}/events` | OpenAI's | Progress, warnings and eval metrics. |
| `GET /fine_tuning/jobs/{job_id}/checkpoints` | OpenAI's | A succeeded job's one checkpoint: its ft: model at the last step, with its metrics. |
| `POST /fine_tuning/jobs/{job_id}/cancel` | OpenAI's | Cancel a job. |
| `POST /batches` | OpenAI's | Create a batch. |
| `GET /batches` | OpenAI's | List your batches. |
| `GET /batches/{batch_id}` | OpenAI's | Retrieve a batch. |
| `POST /batches/{batch_id}/cancel` | OpenAI's | Cancel a batch. |
| `GET /models` | OpenAI's | The catalog models and your ft: models. |
| `GET /models/{model}` | OpenAI's | One model. |
| `DELETE /models/{model}` | OpenAI's | Delete one of your ft: models. |
| `GET /gridline/adapters/{model}/files/{name}` | Gridline's | Download your ft: model's adapter: adapter_model.safetensors and adapter_config.json. |
| `POST /gridline/adapters/{model}/merged` | Gridline's | Start a merge of your ft: model's adapter into its base, to download as full safetensors at no charge. |
| `GET /gridline/adapters/{model}/merged` | Gridline's | The merge's status and, once it has succeeded, the export's file list. |
| `DELETE /gridline/adapters/{model}/merged` | Gridline's | Delete the merged export, or cancel a pending merge. |
| `GET /gridline/adapters/{model}/merged/files/{path}` | Gridline's | Download one file of the merged export: a redirect that carries the file's SHA-256 in x-gridline-sha256. |
| `GET /gridline/usage/keys` | Gridline's | Usage per API key over a window of UTC days: batches, fine-tuning jobs, tokens, held and charged amounts, and each key's spend limits. |
| `GET /gridline/usage/keys/{key}` | Gridline's | One API key's batches and jobs, and its spend today and in total against its limits. {key} is a key id, or current for the key you call with. |

## What is different

Settings both take, with a different default or effect:

| Setting | On OpenAI | On Gridline |
| --- | --- | --- |
| n_epochs "auto" or absent | Chosen from the dataset size. | 1 epoch. Set n_epochs to the number your OpenAI job ran. |
| seed absent | OpenAI generates one. | 0. |
| Example length | Up to 65,536 tokens an example. | Rows longer than max_seq_length (default 2,048) are cut at max_seq_length tokens; a row left with no trained token is dropped. |

What OpenAI offers that Gridline does not, and what to do instead:

| On OpenAI | On Gridline |
| --- | --- |
| Real-time requests (POST /chat/completions, /embeddings, /responses) | Not offered: Gridline runs offline batches. Send the same request bodies as batch lines, or download your adapter and serve it yourself. |
| GPT base models | Fine-tune and batch the open models in the table above. |
| Reinforcement fine-tuning | Refused at create: supervised fine-tuning and DPO only (method.type "supervised" or "dpo"). |
| Full fine-tunes | Gridline trains a LoRA or QLoRA adapter on the base; full fine-tunes are not offered. To take the model with you, download the adapter merged into its base (POST /gridline/adapters/{model}/merged). |
| Tools, function calling and images in training files | Refused at upload, by name: tools, functions, name, tool_calls, content parts and the tool and function roles. |
| Intermediate fine-tuning checkpoints | Not offered: the checkpoints route lists only the final step's adapter, registered as one ft: model. |
| The Uploads API (POST /uploads) | Not offered: upload each file in one request. |
| Other batch endpoints (/v1/responses, /v1/completions, moderations, images, audio) | Refused at validation: batches target chat completions and embeddings. |
| Streaming, tools and the other refused fields in batch lines | Refused at validation, never ignored: the list above. On chat lines response_format is accepted as text, json_object or json_schema. |

## Costs

| Per 1M tokens | OpenAI, fine-tuned `gpt-4o-mini-2024-07-18` | Gridline, Llama 3.1 8B |
| --- | --- | --- |
| Training | $3.00 | $0.29 |
| Batch input | $0.15 | $0.049 |
| Batch output | $0.60 | $0.115 |

Same price for Qwen 2.5 7B and Mistral 7B.

For example, train a 250,000-token file for 3 epochs (750,000 training tokens), then run a batch on the fine-tuned model with 10,000,000 input and 1,000,000 output tokens. On OpenAI that is $2.25 for training and $2.10 for the batch, $4.35 in all. On Gridline it is $0.50 for training (the minimum per job) and $0.605 for the batch, $1.105 in all.

Different models; quality not compared. OpenAI prices as of 13 November 2026.

Fine-tuning and batch jobs are paid for from your prepaid credits.

What a credit is worth, the credit packs and each model's rates are on the [pricing page](https://gridlinegpu.com/pricing).

---

Source: https://gridlinegpu.com/docs/openai-migration
