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.htmlGET / 返回纯文本存活检查。

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 接受 textjson_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 不能与 toolsthink 同时使用。无效 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_callsreplace/diffusionStep(DiffusionGemma 预览)以及终止的 done 摘要。

文件上传(/api/upload

POST /api/upload 接收 multipart 表单(使用第一个文件),返回存储路径 / URL 与该类型的元数据;随后由 /api/chat 消息引用返回的服务端路径(imagePathsaudioPaths 等)。接受的类型包括:图像.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,以及可选 stepscfgseed),也可提交 JSON {"imagePath": "...", "prompt": "...", "steps": 0, "cfg": 0, "seed": 0};其中 imagePath 必须指向上传目录中的文件,steps / cfg0 表示自动。响应为 { 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_predictint200最大生成 token 数
temperaturefloat0.8采样温度(0 = 贪心)
top_kint40Top-K 过滤(0 = 禁用)
top_pfloat0.9核采样阈值(1.0 = 禁用)
min_pfloat0最小概率过滤
repeat_penaltyfloat1.1重复惩罚(1.0 = 无)
presence_penalty / frequency_penaltyfloat0存在 / 频率惩罚
seedint-1随机种子(-1 = 随机)
stoparraynull停止序列

OpenAI 风格(顶层)

max_tokens(省略时默认 200)、temperaturetop_ppresence_penaltyfrequency_penaltyseedstop(字符串或数组),以及 response_formattext / json_object / json_schema)。注意:OpenAI 接口不解析 top_kmin_prepetition_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()