Goshen API

The Goshen API follows the OpenAI API for chat completions, embeddings, models, files and fine-tuning jobs. The base URL is https://api.vpcloud.ca/v1.

Quickstart

Create an account, then create a key on the API keys page of the dashboard. Keys start with gsk_ and are shown once. Each key belongs to one project (see Projects and keys). Then call the API with any OpenAI SDK by changing the base URL:

Python
pip install openai

from openai import OpenAI
import os

client = OpenAI(base_url="https://api.vpcloud.ca/v1", api_key=os.environ["GOSHEN_API_KEY"])
reply = client.chat.completions.create(
    model="qwen3-14b",
    messages=[{"role": "user", "content": "Write a haiku about the first snow of winter."}],
)
print(reply.choices[0].message.content)
Node.js
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.vpcloud.ca/v1", apiKey: process.env.GOSHEN_API_KEY });
const reply = await client.chat.completions.create({
  model: "qwen3-14b",
  messages: [{ role: "user", content: "Write a haiku about the first snow of winter." }],
});
console.log(reply.choices[0].message.content);

Authentication

Send your key as a Bearer token on every request:

Authorization: Bearer $GOSHEN_API_KEY

Goshen stores a SHA-256 hash of each key, not the key, so a lost key cannot be recovered. Revoke it in the dashboard and create a new one. Revoked keys stop working on the next request.

Projects and keys

Your account starts with an organization of its own and one project, called Default. An organization can have more projects, and you can invite people to it from the dashboard. Files, fine-tuning jobs, fine-tuned models and keys all belong to one project.

A key works only in its own project. Another project's files, jobs and models answer 404, as if they did not exist, even in the same organization. You create keys in the dashboard, in the project you have open there.

People in an organization have one of four roles, which hold for every project in it. A key acts with the current role of the person who created it, so it never has more rights than they do.

RoleCan
viewerRead everything. Viewers cannot create keys, and the keys of someone who becomes a viewer turn read-only.
memberAlso upload and delete files, start and cancel jobs, delete fine-tuned models, create keys and call models.
adminAlso manage projects, invites and members.
ownerAlso make and remove owners.

When someone leaves an organization or is removed from it, their keys in its projects stop working.

When you create a key, you can limit it further:

  • Read-only. The key can send only GET requests, so it can list and read but not upload, start jobs, delete or call models. Anything else gets 403 with code read_only_key.
  • To some services: inference (chat completions, embeddings, GET /v1/models), files (/v1/files) and fine_tuning (/v1/fine_tuning/jobs and deleting fine-tuned models). A call to another service gets 403 with code service_not_allowed.

Chat completions

POST /v1/chat/completions with a model and a list of messages.

curl https://api.vpcloud.ca/v1/chat/completions \
  -H "Authorization: Bearer $GOSHEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-14b",
    "messages": [
      {"role": "system", "content": "You answer in one sentence."},
      {"role": "user", "content": "What is LoRA?"}
    ],
    "max_tokens": 200,
    "temperature": 0.7
  }'

Standard parameters such as temperature, top_p, max_tokens, stop, tools and response_format are passed to vLLM. Qwen3 thinks before answering by default; its reasoning comes back in reasoning_content, separate from content. To turn thinking off for a request, send "chat_template_kwargs": {"enable_thinking": false}.

The response includes a usage object with the token counts Goshen bills.

Streaming

Set "stream": true to receive server-sent events as tokens are generated. Add "stream_options": {"include_usage": true} to get a final event with token counts.

Python
stream = client.chat.completions.create(
    model="qwen3-14b",
    messages=[{"role": "user", "content": "Count to five."}],
    stream=True,
    stream_options={"include_usage": True},
)
for event in stream:
    if event.choices and event.choices[0].delta.content:
        print(event.choices[0].delta.content, end="", flush=True)
    if event.usage:
        print("\n", event.usage)

A stream that finishes is metered from the model server's own count, whether or not you ask for the usage event. If a stream is cut short, because your client disconnects or the model server fails partway, that count never arrives, so we bill an estimate of the prompt and of the tokens sent until the stream stopped.

Embeddings

POST /v1/embeddings with model: "qwen3-embedding-0.6b". Vectors have 1024 dimensions. input can be a string or a list of strings.

curl https://api.vpcloud.ca/v1/embeddings \
  -H "Authorization: Bearer $GOSHEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "qwen3-embedding-0.6b", "input": ["first passage", "second passage"]}'

Models

ModelUseLicence
qwen3-14bChat, tools, fine-tuning base. 32,768-token context.Apache 2.0
qwen3-embedding-0.6bEmbeddings, 1024 dimensions. 8,192-token input.Apache 2.0
ft:qwen3-14b:…Your fine-tuned models. Only keys of the project that trained them can see or call them.Yours

GET /v1/models lists the base models and the fine-tuned models of the key's project.

Files

Training data is a JSONL file with one chat example per line. Each line has a messages list with at least one assistant message to learn from. Each message has a role of system, user, assistant or tool, and its content is a string, not a list of content parts. Files can be up to 100 MB and 100,000 examples.

train.jsonl
{"messages": [{"role": "system", "content": "You are Maple Outfitters' support assistant."}, {"role": "user", "content": "Do you ship to Nunavut?"}, {"role": "assistant", "content": "Yes. Orders to Nunavut take 7 to 10 business days."}]}
{"messages": [{"role": "user", "content": "Can I return boots after 30 days?"}, {"role": "assistant", "content": "Returns are accepted within 60 days with a receipt."}]}
curl https://api.vpcloud.ca/v1/files \
  -H "Authorization: Bearer $GOSHEN_API_KEY" \
  -F purpose=fine-tune \
  -F file=@train.jsonl

Goshen checks every line on upload and rejects the file with the line number of the first problem. Files are encrypted at rest with a key of their own. GET /v1/files lists them, GET /v1/files/{id}/content downloads one, and DELETE /v1/files/{id} removes it.

Fine-tuning

Start a LoRA job on qwen3-14b with an uploaded file:

curl https://api.vpcloud.ca/v1/fine_tuning/jobs \
  -H "Authorization: Bearer $GOSHEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-14b",
    "training_file": "file-...",
    "suffix": "support",
    "hyperparameters": {"n_epochs": 3, "batch_size": 4, "learning_rate_multiplier": 1}
  }'
HyperparameterDefaultRange
n_epochs31 to 10
batch_size41 to 32
learning_rate_multiplier1 (learning rate 2e-4)0.05 to 10

Jobs move from queued to running to succeeded, failed or cancelled. Follow one with GET /v1/fine_tuning/jobs/{id} and its progress with GET /v1/fine_tuning/jobs/{id}/events, which reports step and loss. Cancel with POST /v1/fine_tuning/jobs/{id}/cancel. A project can have three jobs queued or running at once (see Quotas).

When a job succeeds, its fine_tuned_model field holds the name to call, such as ft:qwen3-14b:support:1a2b3c4d. Use it as the model in chat completions.

Python
job = client.fine_tuning.jobs.create(model="qwen3-14b", training_file=file.id, suffix="support")
# ...once job.status == "succeeded"
client.chat.completions.create(model=job.fine_tuned_model, messages=messages)

Operations

Actions that take a while run as operations, which move from pending to running to succeeded, failed or cancelled. GET /v1/operations lists your project's operations, newest first, and GET /v1/operations/{id} returns one. limit takes 1 to 100 (default 20), and status filters by status.

{
  "id": "op-...", "object": "operation", "project_id": "...", "service": "control",
  "kind": "project.delete", "status": "succeeded", "target": {"type": "project", "id": "..."},
  "result": {"models": 1, "files": 2, "jobs": 3}, "error": null, "attempts": 1,
  "created_at": 1793000000, "started_at": 1793000003, "finished_at": 1793000004
}

So far the only operation is deleting a project, which you start in the dashboard. It revokes that project's keys first, so for now the list is empty for every key.

Audit log

Goshen records who did what in each project: keys created and revoked, files uploaded and deleted, jobs started and cancelled, models deleted, and changes to the organization, its projects, members and invites. Entries name the account and the key that acted, never the content of a request, and the log you see does not include IP addresses. GET /v1/audit_log returns your project's entries, newest first. It needs a key that is not limited to some services.

curl "https://api.vpcloud.ca/v1/audit_log?limit=50" -H "Authorization: Bearer $GOSHEN_API_KEY"

limit takes 1 to 500 (default 100). For the next page, pass the last entry's id as before. The dashboard shows the same log.

Deleting data

  • DELETE /v1/files/{id} deletes the encrypted file and its key.
  • DELETE /v1/models/{id} unloads a fine-tuned model and deletes its adapter weights from the GPU host.
  • Deleting a project in the dashboard does both for everything in it, cancels its jobs and revokes its keys.
  • Deleting your account deletes every organization you are the only member of, with everything in it. From organizations you share, you leave: Goshen revokes your keys there, and what you made stays with the project.

Backups are deleted when they are 14 days old, so data you delete is gone from the backups within 15 days. Details are on the privacy page.

Rate limits

Each key has a requests-per-minute limit (60 by default), a tokens-per-minute limit (200,000 by default) and a cap of 16 requests in flight. Responses carry x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-limit-tokens and x-ratelimit-remaining-tokens. A refused request gets HTTP 429 with a retry-after header. Write to michael@goshen.fyi to raise your limits.

Quotas

Each project has quotas. Goshen checks them before it creates anything:

QuotaDefaultWhen it is full
Active API keys25429, quota_exceeded
Files50400, too_many_files
File storage5 GB400, storage_quota_exceeded
Fine-tuning jobs queued or running3429, too_many_jobs

An organization can have 20 projects and 100 members, and an account can own 10 organizations; past that, creating a project or an organization, or accepting an invite, gets 429 with quota_exceeded. A quota_exceeded error names the quota in param, and its message says how much is in use:

{"error": {
  "message": "Quota reached: 25 of 25 active API keys in use. Delete some, or ask Goshen to raise the quota.",
  "type": "quota_error", "param": "api_keys", "code": "quota_exceeded"
}}

These errors carry no retry-after header: delete something, or wait for a job to finish. Write to michael@goshen.fyi to raise a quota.

Errors

Errors use the OpenAI shape:

{"error": {"message": "Incorrect API key provided.", "type": "invalid_request_error", "param": null, "code": "invalid_api_key"}}
StatusMeaning
400The request is malformed. param names the field. too_many_files and storage_quota_exceeded mean the project's file quota is full.
401Missing, wrong or revoked API key, or a key whose creator has left the organization.
403The account is not active yet or is suspended (account_not_active), or the key is read-only (read_only_key) or limited to other services (service_not_allowed).
404Unknown model, file, job or operation, or one in another project.
409The project is being deleted (project_deleting).
413The body or file is too large.
429A rate limit was hit: wait for retry-after seconds. Or a quota is full (quota_exceeded, too_many_jobs): see Quotas.
502, 503The model server is restarting or busy. Retry with backoff.