OBLETHdocs

3D mesh generation

Serve image-to-3D models as jobs that each caller creates, polls, downloads, deletes and lists through the gateway, the same way as video.

2 min readUpdated for v2.4.0Edit on GitHub

You needA backend that serves the mesh job API below
Turn it onRegister the model with model_type: "mesh"
It costsA flat price per created job (cost_per_video)
Who sees a jobThe key that created it, by default

A mesh model turns a picture of an object into a textured 3D model. Like a video, a mesh takes minutes, so it is a job: the create call returns a job id at once, the backend works in the background, and the client polls the job until it can download the model as binary glTF (.glb).

POST   /v1/meshes               create a job (an image, optional settings)
GET    /v1/meshes/{id}          job status, as the backend reports it
GET    /v1/meshes/{id}/content  the finished model (model/gltf-binary)
DELETE /v1/meshes/{id}          delete the job
GET    /v1/meshes               the caller's own jobs

The gateway handles mesh jobs exactly like video jobs: the same ownership, routing, billing and retention, with its own path. A mesh job is only ever found through /v1/meshes; its id sent to /v1/videos is a 404, and the other way round.

What the backend serves

There is no OpenAI standard for 3D, so the gateway asks little of the backend:

  • POST {api_base}/meshes accepts multipart/form-data (the image as a file field) or JSON, and answers with a JSON object whose id is the job id.
  • GET {api_base}/meshes/{id} and GET {api_base}/meshes/{id}/content return the job and the file. While the job runs, the download may answer any not-ready status; it is passed through.
  • DELETE {api_base}/meshes/{id} removes the job.
  • GET {api_base}/meshes returns {"data": [...]} with an id and created_at on each job. The gateway keeps only the caller's entries, so a backend may list every job it holds.

Which settings a create takes (resolution, face count, texture size, seed) and the statuses a job goes through are the backend's; the gateway does not check them. A backend that is reachable directly can require the model's upstream key on every call, so that only the gateway can use it.

Run a job

  1. 1

    Register the model

    curl -X POST http://localhost:9180/api/v1/models \
      -H "Authorization: Bearer $OBLETH_ADMIN_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "model_name": "image-to-3d",
        "model_type": "mesh",
        "upstream_model": "my-mesh-model",
        "api_base": "https://mesh.example.internal/v1",
        "api_key": "the-backend-token",
        "cost_per_video": 0.20,
        "enabled": true
      }'
    

    api_base ends in /v1; the gateway appends /meshes. Mesh models share the flat per-job price field with video models, so the price goes in cost_per_video; the model's form on the Models page labels it Per mesh.

  2. 2

    Create a job

    curl -s http://localhost:8088/v1/meshes \
      -H "Authorization: Bearer $OBLETH_KEY" \
      -F model=image-to-3d -F image=@chair.png -F seed=42
    

    The response is the backend's own job object.

  3. 3

    Poll, download and clean up

    curl -s http://localhost:8088/v1/meshes/$MESH_ID -H "Authorization: Bearer $OBLETH_KEY"
    curl -s http://localhost:8088/v1/meshes/$MESH_ID/content -H "Authorization: Bearer $OBLETH_KEY" -o chair.glb
    curl -s -X DELETE http://localhost:8088/v1/meshes/$MESH_ID -H "Authorization: Bearer $OBLETH_KEY"
    curl -s http://localhost:8088/v1/meshes -H "Authorization: Bearer $OBLETH_KEY"
    

In Python:

import os
import time
import requests

url = "http://localhost:8088/v1/meshes"
headers = {"Authorization": f"Bearer {os.environ['OBLETH_KEY']}"}

with open("chair.png", "rb") as image:
    job = requests.post(url, headers=headers, files={"image": image}, data={"model": "image-to-3d"}).json()

while job["status"] not in ("completed", "failed"):
    time.sleep(5)
    job = requests.get(f"{url}/{job['id']}", headers=headers).json()

if job["status"] == "completed":
    with open("chair.glb", "wb") as f:
        f.write(requests.get(f"{url}/{job['id']}/content", headers=headers).content)

Ownership, billing and limits

Everything in Video generation applies, with mesh in place of video:

  • Who can see a job follows OBLETH_VIDEO_JOB_SCOPE (key by default, or tenant). A job id you don't own returns 404 no such mesh job, the same as an id that never existed.
  • Billing. A create is charged the model's per-job price once the backend accepts the job and the gateway has recorded it, as request type mesh. Polls, downloads, deletes and listings are free and write no usage row.
  • Admission. The create takes a slot in the model's pool only while the create call runs, so fairshare limits how fast jobs are created, not how many run at once. A backend with a queue limit can answer 429, which passes through.
  • Records share the job table and its retention (OBLETH_VIDEO_JOB_RETENTION_HOURS), with the job's kind recorded on each row.
  • Health. Mesh models get no generation probe; the check is whether the backend's catalog lists them. GET /v1/models reports them as model_type: "mesh".

The input image is not scanned by guardrails, and a multipart create is sent once and never retried.

In the Playground

The Playground's 3D mode takes an image (choose, drop or paste one, or send a generated picture from Image mode), shows the job's stage and progress, and opens the finished model in a viewer you can turn around, with a download button.