HTTP API (Ollama / OpenAI)
TensorSharp.Server 暴露聚焦的 Ollama / OpenAI 兼容子集以及 Web UI 协议,全部位于 http://localhost:5000。OpenAI Chat Completions 客户端可使用 /v1 基址;Ollama 聊天客户端必须改用非标准路径 /api/chat/ollama。
| 风格 | 端点 |
|---|---|
| 兼容 Ollama | /api/generate、/api/chat/ollama、/api/tags、/api/show |
| 兼容 OpenAI | /v1/chat/completions、/v1/models |
| Web UI (SSE) | /api/chat、/api/sessions、/api/models、/api/models/load、/api/upload、/api/image-edit、/api/image-edit/stream |
| 工具端点 | /api/version、/api/queue/status |
Ollama 聊天路径:兼容 Ollama 的聊天端点是 POST /api/chat/ollama,不是 /api/chat;后者是 Web UI 的 SSE 端点。/api/generate、/api/tags、/api/show 与 /api/version 使用标准 Ollama 路径。
启动时必须传入 --model;视觉 / 音频需要投影器时还要显式传入 --mmproj。请求必须指明该启动 GGUF 文件或其基名。/api/models/load 只能重新加载同一启动模型 / 投影器,不能加载任意路径,也不能给无模型进程添加模型。Ollama / OpenAI 兼容端点省略 max_tokens / num_predict 时默认生成 200 token(Web UI 使用 --max-tokens,默认 20000)。见可复制粘贴的服务器快速开始。
服务没有 API Key 身份验证或内置 TLS,并监听 0.0.0.0:5000。只应在可信网络中使用,或在前方部署带身份验证与 TLS 的反向代理。
服务启动后的快速调用
服务器快速开始托管 gemma-4-E4B-it-Q8_0.gguf。在第二个终端中粘贴:
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}'
浏览器 UI 位于 http://localhost:5000/index.html;GET / 返回纯文本存活检查。
1 · 兼容 Ollama 的 API
列出与查看模型
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"}'
生成(非流式)
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 报告有多少提示 token 直接由上一轮的 KV 缓存提供。/api/generate 总会重置会话,因此它始终为 0;当提示前缀与之前某轮匹配时,/api/chat/ollama 上它会非零。
生成(流式)
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} }'
每行是一个 JSON 对象(换行分隔的 JSON);最后的 "done": true 块携带计时与缓存字段。
聊天(多轮)
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}
}'
带图像生成(多模态)
图像以 base64 编码字节放在 images 数组中发送:
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}
}"
带思考模式的聊天
具备思考能力的模型接受 "think": true,并把思维链拆分到 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 的 API
Chat Completions(非流式)
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 遵循 OpenAI 的 KV 缓存命中扩展 —— 在共享前缀的后续轮次中它会逼近 prompt_tokens。
Chat Completions(流式)
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 }'
每个块是一个 object: "chat.completion.chunk" 的 SSE data: 帧;流以 data: [DONE] 结束。
图像输入(OpenAI 格式)
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 · 通过 HTTP 进行工具调用
发送一个 tools 数组;服务器会检测该架构的线格式并返回结构化的 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"
}]
}
续接循环:把助手的 tool_calls 与一条携带函数结果的 {"role": "tool", "tool_call_id": "...", "content": "..."} 消息追加进去,再次调用该端点。(Ollama 端点使用同样的流程,配以一条 role: "tool" 消息。)
4 · 结构化输出(JSON schema)
OpenAI 的 response_format 接受 text、json_object 与经过校验的 json_schema。服务器会注入严格的 JSON 指令,并在返回前校验输出。JSON 请求还会把首个采样 token 约束为以 { 开头的候选,使爱闲聊的模型无法在 JSON 对象前输出散文,流式首 token 时延因此反映 prefill 延迟(TS_JSON_FORCE_OPEN=0 可关闭)。
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 不能与 tools 或 think 同时使用。无效 schema 返回 HTTP 400;校验失败的输出返回 HTTP 422。
5 · Web UI SSE 协议(/api/chat)
这是内置聊天 UI 所用的协议;外部 UI 也可接入同一端点。每个事件都是一个 JSON 对象,以单个 data: … SSE 帧传递。
# 创建 / 销毁会话(仅 Web UI 流程)
curl -X POST http://localhost:5000/api/sessions # {"sessionId":"a3b1c2..."}
curl -X DELETE http://localhost:5000/api/sessions/a3b1c2...
# 流式聊天
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": [] }'
复用相同的 sessionId 会在下一轮把之前的助手 token 拼接进 KV 缓存前缀。终止帧会报告复用情况:
data: {"done":true,"tokenCount":187,"elapsed":2.143,"tokPerSec":87.23,"promptTokens":512,"kvReusedTokens":420,"kvReusePercent":82.0}
事件字段包括 token(内容)、thinking(推理块)、tool_calls、replace/diffusionStep(DiffusionGemma 预览)以及终止的 done 摘要。
文件上传(/api/upload)
POST /api/upload 接收 multipart 表单(使用第一个文件),返回存储路径 / URL 与该类型的元数据;随后由 /api/chat 消息引用返回的服务端路径(imagePaths、audioPaths 等)。接受的类型包括:图像(.png .jpg .jpeg .gif .webp .bmp .heic .heif)、视频(.mp4 .mov .avi .mkv .webm,按 VIDEO_SAMPLE_FPS 抽帧)、音频(.mp3 .wav .ogg .flac .m4a)、PDF 与文本 / 代码文件(以 textContent 完整返回)。数字版 PDF 返回完整提取的 textContent;扫描版 PDF 在托管视觉模型时像视频一样返回页面图像,否则给出 needsVision 警告。TS_PDF_MAX_PAGES 限制读取页数(默认全部)。
图像编辑(/api/image-edit · /api/image-edit/stream)
托管模型为 qwen_image DiT 时可用。POST /api/image-edit 执行一次编辑:可提交 multipart(image 文件 + prompt,以及可选 steps、cfg、seed),也可提交 JSON {"imagePath": "...", "prompt": "...", "steps": 0, "cfg": 0, "seed": 0};其中 imagePath 必须指向上传目录中的文件,steps / cfg 为 0 表示自动。响应为 { ok, url, width, height, elapsedSeconds },PNG 从 /uploads/ 提供。POST /api/image-edit/stream 接收相同 JSON,但以 SSE 流式返回去噪进度:每步帧为 {"imageEdit": true, "step": i, "total": N, "image": "data:image/png;base64,..."}(最多 8 张均匀预览),最后返回 {"done": true, "url": "...", "width": ..., "height": ..., "elapsedSeconds": ...}。整个进程中的编辑请求串行执行。
6 · 采样参数
Ollama 风格(在 options 对象内)
下表默认值用于请求省略字段时,可通过服务端参数 / TENSORSHARP_* 环境变量配置(见服务端参数)。
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
num_predict | int | 200 | 最大生成 token 数 |
temperature | float | 0.8 | 采样温度(0 = 贪心) |
top_k | int | 40 | Top-K 过滤(0 = 禁用) |
top_p | float | 0.9 | 核采样阈值(1.0 = 禁用) |
min_p | float | 0 | 最小概率过滤 |
repeat_penalty | float | 1.1 | 重复惩罚(1.0 = 无) |
presence_penalty / frequency_penalty | float | 0 | 存在 / 频率惩罚 |
seed | int | -1 | 随机种子(-1 = 随机) |
stop | array | null | 停止序列 |
OpenAI 风格(顶层)
max_tokens(省略时默认 200)、temperature、top_p、presence_penalty、frequency_penalty、seed、stop(字符串或数组),以及 response_format(text / json_object / json_schema)。注意:OpenAI 接口不解析 top_k、min_p 与 repetition_penalty;这些字段使用服务端默认值。
7 · Python 客户端示例
使用 requests(Ollama 风格)
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"])
使用 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)
用 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()