HTTP API (Ollama / OpenAI)
TensorSharp.Server exposes focused Ollama- and OpenAI-compatible subsets plus its Web UI protocol, all on http://localhost:5000. OpenAI Chat Completions clients can use the /v1 base URL; Ollama chat clients need the non-standard /api/chat/ollama path.
| Style | Endpoints |
|---|---|
| Ollama-compatible | /api/generate, /api/chat/ollama, /api/tags, /api/show |
| OpenAI-compatible | /v1/chat/completions, /v1/models |
| Web UI (SSE) | /api/chat, /api/sessions, /api/models, /api/upload, /api/image-edit, /api/image-edit/stream |
| Utilities | /api/version, /api/queue/status |
Ollama chat path: the Ollama-compatible chat endpoint is POST /api/chat/ollama — not /api/chat, which is the Web UI's SSE endpoint. /api/generate, /api/tags, /api/show, and /api/version are at their standard Ollama paths.
Start with a required --model and an explicit --mmproj when vision/audio needs one. Requests must name that startup GGUF file or its basename. /api/models/load only reloads the same startup model/projector; it cannot load an arbitrary path or add a model to a model-less process. When a request omits max_tokens / num_predict, the Ollama and OpenAI compat endpoints default to 200 tokens (the Web UI uses --max-tokens, default 20000). See the copy/paste server quickstart.
The server has no API-key authentication or built-in TLS and listens on 0.0.0.0:5000. Use it on a trusted network or put an authenticating TLS reverse proxy in front of it.
Quick call after the server starts
The server quickstart hosts gemma-4-E4B-it-Q8_0.gguf. Copy this into a second terminal:
curl -s http://localhost:5000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gemma-4-E4B-it-Q8_0.gguf","messages":[{"role":"user","content":"Reply with one short hello."}],"max_tokens":32}'
The browser UI is http://localhost:5000/index.html; GET / is the plain liveness response.
1 · Ollama-compatible API
List & show models
curl http://localhost:5000/api/tags
curl -X POST http://localhost:5000/api/show \
-H "Content-Type: application/json" \
-d '{"model": "Qwen3-4B-Q8_0.gguf"}'
Generate (non-streaming)
curl -X POST http://localhost:5000/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen3-4B-Q8_0.gguf",
"prompt": "What is 1+1?",
"stream": false,
"options": { "num_predict": 50, "temperature": 0.7, "top_p": 0.9 }
}'
{
"model": "Qwen3-4B-Q8_0.gguf",
"response": "1+1 equals 2.",
"done": true,
"done_reason": "stop",
"prompt_eval_count": 15,
"eval_count": 10,
"prompt_cache_hit_tokens": 0,
"prompt_cache_hit_ratio": 0.0
}
prompt_cache_hit_tokens reports how many prompt tokens were served straight from the prior turn's KV cache. /api/generate always resets the session, so it is always 0; it is non-zero on /api/chat/ollama when the prompt prefix matches a previous turn.
Generate (streaming)
curl -X POST http://localhost:5000/api/generate \
-H "Content-Type: application/json" \
-d '{ "model": "Qwen3-4B-Q8_0.gguf", "prompt": "Tell me a joke.", "stream": true, "options": {"num_predict": 100} }'
Each line is a JSON object (newline-delimited JSON); the final "done": true chunk carries timing and cache fields.
Chat (multi-turn)
curl -X POST http://localhost:5000/api/chat/ollama \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen3-4B-Q8_0.gguf",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"}
],
"stream": false,
"options": {"num_predict": 100}
}'
Generate with an image (multimodal)
Images are sent as base64-encoded bytes in the images array:
IMG_B64=$(base64 < photo.png)
curl -X POST http://localhost:5000/api/generate \
-H "Content-Type: application/json" \
-d "{
\"model\": \"gemma-4-E4B-it-Q8_0.gguf\",
\"prompt\": \"What is in this image?\",
\"images\": [\"$IMG_B64\"],
\"stream\": false,
\"options\": {\"num_predict\": 200}
}"
Chat with thinking mode
Thinking-capable models accept "think": true and split chain-of-thought into message.thinking:
curl -X POST http://localhost:5000/api/chat/ollama \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen3-4B-Q8_0.gguf",
"messages": [{"role": "user", "content": "Solve 17 * 23 step by step."}],
"think": true, "stream": false, "options": {"num_predict": 200}
}'
{
"message": {
"role": "assistant",
"content": "17 * 23 = 391.",
"thinking": "17 * 20 = 340. 17 * 3 = 51. 340 + 51 = 391."
},
"done": true, "done_reason": "stop"
}
2 · OpenAI-compatible API
Chat Completions (non-streaming)
curl -X POST http://localhost:5000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen3-4B-Q8_0.gguf",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is 2+3?"}
],
"max_tokens": 50, "temperature": 0.7
}'
{
"id": "chatcmpl-abc123...",
"object": "chat.completion",
"model": "Qwen3-4B-Q8_0.gguf",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "2 + 3 = 5."},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 20, "completion_tokens": 8, "total_tokens": 28,
"prompt_tokens_details": { "cached_tokens": 0 }
}
}
usage.prompt_tokens_details.cached_tokens follows OpenAI's KV-cache-hit extension — on a follow-up turn that shares a prefix it approaches prompt_tokens.
Chat Completions (streaming)
curl -X POST http://localhost:5000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{ "model": "Qwen3-4B-Q8_0.gguf", "messages": [{"role": "user", "content": "Hello!"}], "max_tokens": 50, "stream": true }'
Each chunk is an SSE data: frame of object: "chat.completion.chunk"; the stream ends with data: [DONE].
Image input (OpenAI format)
IMG_B64=$(base64 < photo.png)
curl -X POST http://localhost:5000/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{
\"model\": \"gemma-4-E4B-it-Q8_0.gguf\",
\"messages\": [{
\"role\": \"user\",
\"content\": [
{\"type\": \"text\", \"text\": \"What is in this image?\"},
{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/png;base64,$IMG_B64\"}}
]
}],
\"max_tokens\": 200
}"
3 · Tool calling over HTTP
Send a tools array; the server detects the architecture's wire format and returns structured tool_calls.
curl -X POST http://localhost:5000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen3-4B-Q8_0.gguf",
"messages": [{"role": "user", "content": "What is the weather in Paris?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"units": {"type": "string", "enum": ["c", "f"]}
},
"required": ["city"]
}
}
}],
"max_tokens": 200
}'
{
"choices": [{
"message": {
"role": "assistant", "content": null,
"tool_calls": [{
"id": "call_abc123", "type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\",\"units\":\"c\"}" }
}]
},
"finish_reason": "tool_calls"
}]
}
Continue the loop by appending the assistant tool_calls plus a {"role": "tool", "tool_call_id": "...", "content": "..."} message with the function result, then calling the endpoint again. (The Ollama endpoint uses the same flow with a role: "tool" message.)
4 · Structured outputs (JSON schema)
The OpenAI response_format accepts text, json_object, and validated json_schema. The server injects strict JSON instructions and validates the output before returning it. JSON requests also constrain the first sampled token to a {-opening candidate, so chatty models cannot ramble prose before the object and streamed time-to-first-token reflects prefill latency (TS_JSON_FORCE_OPEN=0 disables).
curl -X POST http://localhost:5000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen3-4B-Q8_0.gguf",
"messages": [
{"role": "system", "content": "You are a concise extraction assistant."},
{"role": "user", "content": "Extract the city and country from: Paris, France."}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "location_extraction", "strict": true,
"schema": {
"type": "object",
"properties": {
"city": { "type": "string" },
"country": { "type": "string" },
"confidence": { "type": ["string", "null"] }
},
"required": ["city", "country", "confidence"],
"additionalProperties": false
}
}
},
"max_tokens": 120
}'
json_schema cannot be combined with tools or think. Invalid schemas return HTTP 400; outputs that fail validation return HTTP 422.
5 · Web UI SSE protocol (/api/chat)
This is the protocol the bundled chat UI uses; external UIs can plug into the same endpoint. Every event is a JSON object delivered as a single data: … SSE frame.
# Create / dispose a session (Web UI flow only)
curl -X POST http://localhost:5000/api/sessions # {"sessionId":"a3b1c2..."}
curl -X DELETE http://localhost:5000/api/sessions/a3b1c2...
# Streaming chat
curl -N -X POST http://localhost:5000/api/chat \
-H "Content-Type: application/json" \
-d '{ "messages": [{"role": "user", "content": "Hi"}], "maxTokens": 50, "sessionId": null, "newChat": false, "think": false, "tools": [] }'
Reusing the same sessionId splices prior assistant tokens into the KV-cache prefix on the next turn. The terminal frame reports reuse:
data: {"done":true,"tokenCount":187,"elapsed":2.143,"tokPerSec":87.23,"promptTokens":512,"kvReusedTokens":420,"kvReusePercent":82.0}
Event fields include token (content), thinking (reasoning chunk), tool_calls, replace/diffusionStep (DiffusionGemma previews), and the terminal done summary.
Uploads (/api/upload)
POST /api/upload takes a multipart form (first file used) and returns the stored path/URL plus per-type metadata; /api/chat messages then reference the returned server paths (imagePaths, audioPaths, …). Accepted types: images (.png .jpg .jpeg .gif .webp .bmp .heic .heif), video (.mp4 .mov .avi .mkv .webm — frames extracted at VIDEO_SAMPLE_FPS), audio (.mp3 .wav .ogg .flac .m4a), PDF, and text/code files (returned in full as textContent). Born-digital PDFs return their complete extracted textContent; scanned PDFs return page images like video frames when a vision model is hosted (otherwise a needsVision warning). TS_PDF_MAX_PAGES caps the pages read (default: all).
Image editing (/api/image-edit · /api/image-edit/stream)
Available when the hosted model is a qwen_image DiT. POST /api/image-edit runs one edit — multipart (image file + prompt, optional steps, cfg, seed) or JSON {"imagePath": "...", "prompt": "...", "steps": 0, "cfg": 0, "seed": 0} where imagePath must reference a file in the upload directory and steps/cfg of 0 mean auto — and returns { ok, url, width, height, elapsedSeconds } with the PNG served under /uploads/. POST /api/image-edit/stream takes the same JSON body but streams SSE denoising progress: per-step frames {"imageEdit": true, "step": i, "total": N, "image": "data:image/png;base64,..."} (up to 8 evenly spaced previews), then a final {"done": true, "url": "...", "width": ..., "height": ..., "elapsedSeconds": ...}. Edits are serialized process-wide.
6 · Sampling parameters
Ollama-style (inside the options object)
The defaults shown apply when a request omits the field — they are the server-wide defaults, configurable via server flags / TENSORSHARP_* env vars (see Server options).
| Parameter | Type | Default | Description |
|---|---|---|---|
num_predict | int | 200 | Maximum tokens to generate |
temperature | float | 0.8 | Sampling temperature (0 = greedy) |
top_k | int | 40 | Top-K filtering (0 = disabled) |
top_p | float | 0.9 | Nucleus sampling threshold (1.0 = disabled) |
min_p | float | 0 | Minimum probability filtering |
repeat_penalty | float | 1.1 | Repetition penalty (1.0 = none) |
presence_penalty / frequency_penalty | float | 0 | Presence / frequency penalties |
seed | int | -1 | Random seed (-1 = random) |
stop | array | null | Stop sequences |
OpenAI-style (top-level)
max_tokens (default 200 when omitted), temperature, top_p, presence_penalty, frequency_penalty, seed, stop (string or array), and response_format (text / json_object / json_schema). Note: top_k, min_p, and repetition_penalty are not parsed on the OpenAI surface — the server-wide defaults apply for those.
7 · Python client examples
Using requests (Ollama-style)
import requests
resp = requests.post("http://localhost:5000/api/generate", json={
"model": "Qwen3-4B-Q8_0.gguf",
"prompt": "What is machine learning?",
"stream": False,
"options": {"num_predict": 100, "temperature": 0.7}
})
print(resp.json()["response"])
Using the openai SDK
from openai import OpenAI
client = OpenAI(base_url="http://localhost:5000/v1", api_key="not-needed")
response = client.chat.completions.create(
model="Qwen3-4B-Q8_0.gguf",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is 2+3?"}
],
max_tokens=50, temperature=0.7
)
print(response.choices[0].message.content)
Streaming with the openai SDK
stream = client.chat.completions.create(
model="Qwen3-4B-Q8_0.gguf",
messages=[{"role": "user", "content": "Tell me about Python."}],
max_tokens=200, stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()