LLM & Media APIChat & completions

Chat & completions

The native GPUniq chat API — completions, server-side chat sessions with full history, shareable transcripts, the generations library, and natural-language to shell commands.

The native surface at /v1/llm/* returns GPUniq's wrapped envelope and adds what the OpenAI protocol has no room for: server-side chat sessions, USD cost on every response, and the generations library.

If you want a drop-in replacement for api.openai.com instead — for Claude Code, Cursor, Aider or the official SDKs — use OpenAI compatibility.

Requests are authenticated with X-API-Key: gpuniq_... on native routes (a dashboard JWT via Authorization: Bearer also works). The default model when you omit model is claude-haiku-4-5.

Simple chat

response = client.llm.chat("claude-haiku-4-5", "Explain how transformers work")
print(response)

Chat completion (full)

data = client.llm.chat_completion(
    messages=[
        {"role": "system", "content": "You are a helpful AI assistant."},
        {"role": "user", "content": "What is gradient descent?"},
    ],
    model="claude-sonnet-4-6",
    temperature=0.7,
    max_tokens=1000,
    top_p=0.9,
)

print(data["content"])
print(f"Tokens used: {data['tokens_used']}  cost: ${data['cost_usd']:.6f}")

Parameters

body
messages

List of message objects with role ("system", "user", "assistant") and content.

body
model

Model slug (e.g., claude-opus-4-7, gpt-5.2, gemini-3-pro). Defaults to claude-haiku-4-5.

body
max_tokens

Maximum tokens in the response.

body
temperature

Sampling temperature (0.0-2.0). Higher = more creative.

body
top_p

Top-p nucleus sampling parameter.

A non-streaming request asking for max_tokens > 4096 is served through an internal keep-alive path rather than refused — but it holds the connection for the whole generation and reports upstream failures inside a 200. Set "stream": true or use the job-based long-poll API — see Long generations.

Cached input. When the provider that served the call gave GPUniq a cache-read discount, the cached part of your prompt (usage.prompt_tokens_details.cached_tokens) is billed at the catalog's retail_cached_input_usd_per_mtok instead of the input rate. When it did not, cached tokens are billed as normal input — the discount mirrors what GPUniq actually paid, so two identical requests served by different routes can cost differently. usage.cost on every response is the amount charged.

Chat sessions

Persistent conversations stored server-side — the model sees the full history on every call, so you send one message rather than re-uploading the transcript.

Persistent conversations stored server-side — the model sees the full history on every call:

# Create a session
session = client.llm.create_chat_session(
    model="claude-sonnet-4-6",
    title="Research Assistant",
)

# Send messages within the session
reply = client.llm.send_message(
    chat_id=session["id"],
    message="What are the key papers on attention mechanisms?",
    temperature=0.5,
)

# List all sessions
sessions = client.llm.list_chat_sessions(limit=50)

# Get a session with full message history
full = client.llm.get_chat_session(chat_id=session["id"])

# Update title
client.llm.update_chat_session(chat_id=session["id"], title="New Title")

# Delete
client.llm.delete_chat_session(chat_id=session["id"])

Sharing a transcript

POST /v1/llm/chats/{chat_id}/share returns a public link to a read-only copy of the conversation, reusing the existing link if one was already created. Anyone with the token can read it through GET /v1/llm/chats/shared/{share_token} without a key.

curl -X POST https://api.gpuniq.com/v1/llm/chats/42/share \
  -H "X-API-Key: gpuniq_your_key"

Messages and context

EndpointWhat it does
GET /v1/llm/chats/{chat_id}/messagesPage through a session's messages.
DELETE /v1/llm/chats/{chat_id}/messages/{message_id}Remove one message from the history.
GET /v1/llm/chats/{chat_id}/contextThe messages the next turn will be sent with. Pass ?model= to see the history as trimmed for that model's context window.
POST /v1/llm/chats/{chat_id}/turnsRecord a turn you already generated and paid for on a streaming surface, so it lands in the session history. Never charges.

Generating images inside a session

Posting to /v1/llm/chats/{chat_id}/messages with an image model puts the prompt and the resulting image into the chat history as a turn. See Generating an image inside a chat session.

The generations library

Every image, video, music and audio generation you run is recorded. GET /v1/llm/generations returns a page of them, newest first.

Query parameterEffect
kindFilter by media kind.
modelFilter by model slug.
qFree-text search over prompts.
limitPage size, default 40.
before_idPass the previous page's next_before_id to continue. A null there means there is nothing older.

GET /v1/llm/generations/{generation_id} returns one in full; DELETE on the same path removes it.

curl "https://api.gpuniq.com/v1/llm/generations?kind=video&limit=20" \
  -H "X-API-Key: gpuniq_your_key"

Generate terminal commands

POST /v1/llm/generate-commands turns a natural-language task into an ordered list of shell commands, each annotated with a danger_level. The Python SDK has no helper for this endpoint — call it over HTTP.

FieldTypeNotes
promptstringRequired, 1–1000 characters.
max_commandsint1–10, default 5.
modelstringOptional; the server default is used when omitted.
contextobjectOptional — OS, shell history, environment.
import requests

body = requests.post(
    "https://api.gpuniq.com/v1/llm/generate-commands",
    headers={"X-API-Key": "gpuniq_your_key"},
    json={"prompt": "find all Python files larger than 1MB and sort by size",
          "max_commands": 5},
).json()
if body["exception"] != 0:              # refusals arrive as HTTP 200
    raise RuntimeError(body["data"])

for c in body["data"]["commands"]:
    print(f"[{c['danger_level']}] {c['command']}  # {c['description']}")

The response also carries explanation, warnings, alternatives, tokens_used, cost_usd and balance_usd. Per-command fields beyond command, description and danger_level (step, requires_confirmation, expected_output) are written by the model and may be missing. If the model's reply cannot be parsed, commands comes back empty with the reason in explanation — treat an empty list as a result to handle, not as success.

This endpoint predates the shared error catalog: its error_code values are upper-case (INSUFFICIENT_BALANCE), unlike the lower-case codes on every other surface. Compare case-insensitively if you branch on them.

Errors

Every failure returns a structured error_code you can branch on. The full catalog, envelope shapes and recovery recipes live in the Error reference.