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:
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)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_KEYGoshen 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.
| Role | Can |
|---|---|
viewer | Read everything. Viewers cannot create keys, and the keys of someone who becomes a viewer turn read-only. |
member | Also upload and delete files, start and cancel jobs, delete fine-tuned models, create keys and call models. |
admin | Also manage projects, invites and members. |
owner | Also 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
GETrequests, so it can list and read but not upload, start jobs, delete or call models. Anything else gets 403 with coderead_only_key. - To some services:
inference(chat completions, embeddings,GET /v1/models),files(/v1/files) andfine_tuning(/v1/fine_tuning/jobsand deleting fine-tuned models). A call to another service gets 403 with codeservice_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.
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
| Model | Use | Licence |
|---|---|---|
qwen3-14b | Chat, tools, fine-tuning base. 32,768-token context. | Apache 2.0 |
qwen3-embedding-0.6b | Embeddings, 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.
{"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.jsonlGoshen 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}
}'| Hyperparameter | Default | Range |
|---|---|---|
n_epochs | 3 | 1 to 10 |
batch_size | 4 | 1 to 32 |
learning_rate_multiplier | 1 (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.
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:
| Quota | Default | When it is full |
|---|---|---|
| Active API keys | 25 | 429, quota_exceeded |
| Files | 50 | 400, too_many_files |
| File storage | 5 GB | 400, storage_quota_exceeded |
| Fine-tuning jobs queued or running | 3 | 429, 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"}}| Status | Meaning |
|---|---|
| 400 | The request is malformed. param names the field. too_many_files and storage_quota_exceeded mean the project's file quota is full. |
| 401 | Missing, wrong or revoked API key, or a key whose creator has left the organization. |
| 403 | The 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). |
| 404 | Unknown model, file, job or operation, or one in another project. |
| 409 | The project is being deleted (project_deleting). |
| 413 | The body or file is too large. |
| 429 | A rate limit was hit: wait for retry-after seconds. Or a quota is full (quota_exceeded, too_many_jobs): see Quotas. |
| 502, 503 | The model server is restarting or busy. Retry with backoff. |