HTTP API (Ollama / OpenAI)

TensorSharp.Server.Host(服务端)暴露聚焦的 Ollama / OpenAI 兼容子集以及 Web UI 协议,全部位于 http://localhost:5000。OpenAI Chat Completions 客户端可使用 /v1 基址;Ollama 聊天客户端必须改用非标准路径 /api/chat/ollama。

风格端点
兼容 Ollama/api/embed、/api/generate、/api/chat/ollama、/api/tags、/api/show
兼容 OpenAI/v1/embeddings、/v1/chat/completions、/v1/responses、/v1/models、/v1/videos/generations、/v1/skills
兼容 Jev/v1/systemone(在托管的 DiffusionGemma 模型上做类型化决策——见下文)
Web UI (SSE)/api/chat、/api/sessions、/api/models、/api/models/load、/api/upload、/api/image-generate、/api/image-generate/stream、/api/image-edit、/api/image-edit/stream、/api/video-generate、/api/video-generate/stream、/api/skills、/api/code/artifacts
工具端点/health、/api/version、/api/queue/status、/uploads/*(已存储的上传文件与生成文件)
⚠️

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 只能重新加载同一启动模型 / 投影器,不能加载任意路径,也不能给无模型进程添加模型。请求省略 max_tokens / num_predict / max_output_tokens 时,所有端点——Ollama、OpenAI 与 Web UI 一样——都回落到 --max-tokens / MAX_TOKENS(默认 20000);用该参数或环境变量显式固定的值还会截断要求更多的请求。服务端没有旧式 /v1/completions 路由,也没有 /v1/images/* 路由。见可复制粘贴的服务器快速开始。

🔒

服务没有 API Key 身份验证或内置 TLS,并监听 0.0.0.0:5000。只应在可信网络中使用,或在前方部署带身份验证与 TLS 的反向代理。

嵌入服务

使用 --model encoder.gguf --embeddings 托管 Snowflake、MiniLM 或 EmbeddingGemma 2。POST /v1/embeddings 与 POST /api/embed 返回归一化向量;EmbeddingGemma 2 配合 --mmproj 还支持图像/音频/视频与混合内容序列。后端与验证范围依模型而定。 配方、API 与验证 →

服务启动后的快速调用

服务器快速开始托管 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/:带有 wwwroot 内容时,GET / 直接提供 index.html。GET /health 是稳定的纯文本存活检查;只有不带 UI 的无界面部署才会让 / 回退到同一响应。

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.5-9B-Q8_0.gguf"}'

生成(非流式)

curl -X POST http://localhost:5000/api/generate \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen3.5-9B-Q8_0.gguf",
    "prompt": "What is 1+1?",
    "stream": false,
    "options": { "num_predict": 50, "temperature": 0.7, "top_p": 0.9 }
  }'
{
  "model": "Qwen3.5-9B-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.5-9B-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.5-9B-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.5-9B-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.5-9B-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.5-9B-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.5-9B-Q8_0.gguf", "messages": [{"role": "user", "content": "Hello!"}], "max_tokens": 50, "stream": true }'

每个块是一个 object: "chat.completion.chunk" 的 SSE data: 帧;流以 data: [DONE] 结束。解析器暂扣尚无法归类的文本时(Nemotron-H Reasoning-128K 关闭思考的回复,在证实是推理还是答案之前),流每 15 秒发送一行 SSE 注释 : keep-alive,以免被代理的空闲超时切断;SSE 客户端会跳过它。/v1/responses 发送同样的注释,/api/chat/ollama 发送 message 为空的分块。

图像输入(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
  }"

Responses API(/v1/responses)

curl -X POST http://localhost:5000/v1/responses \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen3.5-9B-Q8_0.gguf",
    "instructions": "You are a helpful assistant.",
    "input": "What is 2+3?",
    "max_output_tokens": 50
  }'

input 可以是字符串,也可以是消息项数组;只读取消息项,其他类型(function_call、function_call_output、reasoning)会被跳过并记入日志。instructions 成为系统消息,max_output_tokens 省略时回落到 --max-tokens,reasoning 对象开启思考(reasoning.effort 是 reasoning_effort 在 Responses 中的写法),text.format 请求结构化输出。该路由上 text.format 不能与 reasoning 或 tools 同时使用(任何家族都返回 400);§5 中语法延后生效的例外只适用于 Chat Completions 的 response_format。采样字段、tools、skills、skills_discovery 与 multi_agent 的用法与 Chat Completions 相同。"stream": true 时以具名 SSE 事件推送(response.created、response.output_text.delta……response.completed)。服务端按请求无状态:previous_response_id 会以 400 拒绝,因此请把完整对话放进 input。store(默认 true)只是保留已完成的响应,供 GET /v1/responses/{id} 再次取回——存在内存中,设置了 TS_RESPONSES_STORE_REDIS_URL(或 --redis-url)时存在 Redis 中。

Jev 类型化决策(/v1/systemone)

托管模型为 DiffusionGemma 时,POST /v1/systemone 针对一个 state(字符串、对象或数组)回答 Jev 风格的类型化问题,每个样本只做一次带种子的去噪读取,而不是生成文本:noul(为“是”的概率)、choice(2–26 个具名选项)与 score(有序等级,以从 0 起算的期望下标作答)。每个问题形如 {"type", "instructions", "criteria"}:choice 的 criteria 是选项名到描述的映射,score 的 criteria 是按顺序排列的等级数组,noul 的 criteria 可省略,也可以是 {"true": …, "false": …} 对象;问题中的其他字段返回 422。model 可以写已加载的文件名、jev-latest 或 jev-preview——后两者只是已加载模型的别名,不是另外的检查点。

curl -X POST http://localhost:5000/v1/systemone \
  -H "Content-Type: application/json" \
  -d '{
    "model": "jev-latest",
    "state": "I was charged twice for the same subscription this month.",
    "questions": { "billing": { "type": "noul", "instructions": "Is this a billing issue?" } },
    "samples": 1, "seed": 42
  }'

响应包含 answers.<id>(noul;或带 probabilities 与 confidence 的 choice / score)、usage 与 diagnostics。这些概率只是在所列标签上做的 softmax,并不是经过校准的“答对概率”。一个请求最多 64 个问题、最多 8 张内联 images(base64 或 data: URL,按上传限额存储;需要用 --mmproj 传入视觉塔,否则返回 503);其余字段为 instructions、samples(1–32,或默认的 "auto")、auto_max、auto_threshold、seed、chunk_rows 与 chunk_prompt,而 steps 与 think 只接受 1 与 0。错误码:Content-Type 不是 application/json(包括 multipart 上传)时 415,JSON 格式错误 400,超过 8 MiB 请求体上限 413(TS_JEV_MAX_BODY_MB,1–64),内联图像超出上传单文件上限或配额时 413 / 507,请求无效 422(含音频与视频),模型名未知 404,未加载 DiffusionGemma 模型 503;已接纳(运行中或排队)的请求达到 TS_JEV_MAX_PENDING(默认 32)时返回 529 并带 Retry-After: 1。→ Jev 指南

3 · 通过 HTTP 进行工具调用

发送一个 tools 数组;服务器会检测该架构的线格式并返回结构化的 tool_calls。托管 DiffusionGemma 时模型没有工具调用通道,携带 tools 的请求返回 400;Mistral 3 与 Hunyuan Dense 不渲染工具声明。

curl -X POST http://localhost:5000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen3.5-9B-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 · 通过 HTTP 使用 Agent Skills

Agent Skill(智能体技能)是一个面向模型的说明文件夹——一份 SKILL.md,外加它引用的脚本、参考文档与素材——服务端只在任务需要时才加载它。所有聊天接口 —— /v1/chat/completions、/v1/responses、兼容 Ollama 的 /api/chat/ollama,以及 Web UI 的 /api/chat —— 都接受同样的两个可选字段:

"skills": ["pdf", "xlsx"],
"skills_discovery": true

skills_discovery 默认为 true:本次请求没有点名的技能,其名称与描述也会展示给模型,好让它自己发现你没想到要点名的那个。设为 false 则把本次请求限制在它列出的技能上。点名一个服务端没有的技能会返回 400 —— 用 GET /api/skills 查看已注册了哪些。

对能调用工具的家族,初始提示词只携带元数据,显式选中的技能也一样;模型激活匹配技能时再用 skills_read 读取 SKILL.md。若一个家族无法同时渲染并解析完整工具往返,则改为内联选中技能正文,并丢弃发现目录;当前 Mistral 3、Hunyuan Dense、DiffusionGemma 以及没有注册聊天协议的架构走这条路径。Qwen 3.8 Flash Next(qwen4exp)现已能解析工具调用,走元数据路径。

# 兼容 OpenAI
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": "把这份对账单里的合计表格提取出来。"}],
    "skills": ["pdf"],
    "max_tokens": 600
  }'

# 兼容 Ollama
curl -X POST http://localhost:5000/api/chat/ollama \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemma-4-E4B-it-Q8_0.gguf",
    "messages": [{"role": "user", "content": "帮我做一张预算表。"}],
    "skills": ["xlsx"],
    "skills_discovery": false,
    "stream": false
  }'

# Web UI SSE
curl -N -X POST http://localhost:5000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "总结这份 PDF。"}], "skills": ["pdf"], "maxTokens": 400}'

返回的是一条普通回复。模型通过内置的 skills_list / skills_read 工具取用技能的其余内容,而这两个工具由服务端自己在进程内、紧挨着权重应答,因此客户端永远不会收到一个它无法执行的工具调用,现有的 OpenAI SDK 也无需改动(把这两个字段放进 extra_body 即可)。技能还能与你自己的 tools 共存:技能工具会并入你发来的工具列表,服务端只应答属于它自己的那几个,而对你的工具的调用照常回传——此时模型从技能里读到的内容已经在对话中了。开启脚本执行时,读取带脚本的 SKILL.md 还会附上一段简短的宿主说明,给出现成的 skills_run 示例;skills_run 的 args 可以是字符串组成的 JSON 数组(数组被编码成字符串传来时也可以),也可以是 shell 风格的文本。

📌

每次内置读取或代码操作都可能引出一次额外生成,而每一轮都以流式运行。服务端吞掉自己的工具标记,同时转发解码中的正文 / 推理,因此客户端收到的是完成后的回答,而不是内部调用。--skills-max-rounds 限制循环:默认 8,提供代码执行时自动变为 24;运维显式设置的值始终原样保留。

Web UI 的流式响应会为每一次已执行的技能工具调用推送一帧,随完成即时送出:

data: {"skill_step":"skills_read","agent_id":"/root","skill":"pdf","detail":"references/forms.md","ok":true,"round":1,"files":null}

skill_step 是执行过的内置工具,agent_id 指明发起调用的智能体(除非是子智能体发起,否则为 /root),skill/detail 标识目标,ok 表示成功与否,round 从 1 起算;files 以 {name, bytes, url} 携带生成产物。→ Web UI SSE 协议

智能体代码执行

代码执行是服务端启动能力,不是逐请求开关。带上 --code-exec 后,每个能调用工具的聊天接口都可以使用四个服务端自有工具:read_file、write_file、shell 与 apply_patch。它们与技能共用同一个有界循环,绝不会转发客户端;调用方定义的工具仍由调用方处理。这里没有逐命令审批 API。→ 参数、沙箱与信任模型

每个 OpenAI 或 Ollama 请求都得到唯一的私有工作区,在内部轮次之间共享,并在完成、取消或失败后删除。具名 Web UI 会话则跨聊天轮次保留工作区;技能脚本与代码工具共用它。保留的产物可通过 GET /api/code/artifacts/{runId} 列出,并从 GET /api/code/artifacts/{runId}/{*path} 下载;下载始终带 attachment 与 nosniff。

🔒

脚本与命令执行都默认关闭,OS 沙箱默认 required(要么隔离,要么拒绝)。macOS Seatbelt 与 Linux bubblewrap 0.12+ 会约束写入、主目录读取与默认关闭的网络,但 macOS 无法保证清理故意脱离的子进程。Windows job object 只约束进程树,不能限制文件或网络,因此代码需要显式 --code-exec-unconfined,技能脚本需要 --skills-sandbox preferred。--code-exec-allow-network 赋予生成命令不受限的宿主 IP 网络访问(含 LAN / loopback);它与宿主代安装、--skills-allow-network 彼此独立。

子智能体(multi_agent)

在能调用工具的家族上——即既渲染工具声明又能解析工具调用的家族,因此不包括 Mistral 3、Hunyuan Dense 与 DiffusionGemma——/v1/chat/completions、/v1/responses、/api/chat/ollama 与 Web UI 的 /api/chat 还会提供五个服务端自有的协调工具:spawn_agent、wait_agent、send_input、close_agent 与 list_agents。模型可以借此把有界的子任务交给在同一个已加载模型上运行的子智能体。该功能默认开启,与技能和代码执行无关,结构化输出请求与 /api/generate 不提供;是否委派由模型决定。explorer 与 reviewer 子智能体只读(skills_list、skills_read、read_file 与协调工具),worker 除非服务端带 --agents-allow-worker-tools 启动,否则同样只读。子智能体永远拿不到客户端自己的工具,同一请求树内的宿主工具调用逐个执行,每个子智能体使用 KV 状态的独立副本。客户端只看到父智能体的回答;上报的 token 用量包含子智能体。目前没有公布委派的时延或质量数据。

"multi_agent": false

只有 JSON false 会起作用:它为该请求关闭委派。服务端以 --no-multi-agent(或 TS_NO_MULTI_AGENT)启动时,请求无法把它打开,也不能提高任何 --agents-* 上限。→ 角色与上限

社区技能发现通过 find-skills 的 skills_search、skills_inspect 与 skills_install 工具完成,需要技能网络权限;不是独立的 HTTP 安装端点。服务端/应用可安装,默认 CLI 只读并提供搜索/检查。 条件与当前技能包 →

管理技能注册表

同一份数据有两种形态:只读、偏 OpenAI 风格的 /v1/skills,以及 Web UI 的 /api/skills——后者额外提供加载错误、单文件读取、上传与删除。

端点用途
GET /v1/skills全部已注册技能,包装为 {"object": "list", "data": [...]}。
GET /v1/skills/{name}单个技能,并把 SKILL.md 正文放在 instructions 字段里返回。
GET /api/skills同样的对象,形如 {"enabled": …, "installable": …, "skills": [ … ], "errors": [{"path", "message"}]} —— 管理界面因此还能列出那些看起来像技能却加载失败的目录,并给出原因。
GET /api/skills/{name}单个技能,含 instructions。
GET /api/skills/{name}/files/{*path}技能随包的某个文件,走的是与模型自身读取相同的那道路径关卡,并且始终以 text/plain 加 nosniff 返回:技能里可能带 .html 或 .js 文件,按真实类型返回等于在服务端自己的源上执行上传来的内容。
POST /api/skills以 multipart 上传 .zip 安装技能(file=@pdf.zip),可选 overwrite=true 覆盖同名的已安装技能。成功返回 201 与安装后的技能对象。
POST /api/skills/rescan不重启即可让磁盘上的改动生效;返回体与 GET /api/skills 相同。
DELETE /api/skills/{name}删除一个已安装的技能 —— {"removed":true}。只有从这里上传的技能("origin": "installed")可以删除;在运维配置的目录中扫描到的技能属于对方自己的文件树,会被拒绝。
curl http://localhost:5000/v1/skills
curl http://localhost:5000/v1/skills/pdf

curl http://localhost:5000/api/skills
curl http://localhost:5000/api/skills/pdf
curl http://localhost:5000/api/skills/pdf/files/references/forms.md

# 上传技能文件夹的 .zip(pdf/SKILL.md),或其内容的 .zip(SKILL.md 位于压缩包根部)
curl -X POST http://localhost:5000/api/skills -F "file=@pdf.zip" -F "overwrite=true"

curl -X POST http://localhost:5000/api/skills/rescan
curl -X DELETE http://localhost:5000/api/skills/pdf   # {"removed":true}

技能对象:

{
  "id": "pdf",
  "object": "skill",
  "name": "pdf",
  "description": "Extract text and tables from PDF files, fill in PDF forms, and merge or split documents...",
  "license": "Apache-2.0",
  "compatibility": "Requires python3 with pypdf installed.",
  "files": [
    {"path": "scripts/extract_tables.py", "bytes": 4021, "kind": "script", "text": true},
    {"path": "references/forms.md", "bytes": 18233, "kind": "reference", "text": true}
  ],
  "bytes": 41288,
  "origin": "installed",
  "warnings": [],
  "modified": "2026-08-29T12:00:00Z"
}

kind 取值为 script / reference / asset / manifest / other;origin 为 discovered 表示是扫描配置目录发现的,installed 表示是从这里上传的;warnings 收录那些不完全合规但仍然加载成功的问题(name 与目录名不一致、描述超过 1024 字符上限等)。GET /api/models 会以 "skills": {"enabled": true, "installable": true, "allowScripts": false, "count": 7} 报告该功能本身(allowScripts 表示是否提供 skills_run),服务端关闭技能时该字段为 null —— Web UI 正是据此决定要不要显示相关控件。错误遵循服务端按前缀区分的惯例:/v1/* 返回 {"error": {"message": "…", "type": "invalid_request_error"}},/api/* 返回 {"error": "…"}。

🔒

上传的技能属于不受信任的内容,因此落盘前会校验:ZIP 条目使用与模型读取相同的路径关卡,按解压后大小计数,并限制 4096 个文件、单文件 64 MB、总计 256 MB 与 200× 压缩比。除非服务端带 --skills-allow-exec 启动,否则不会提供 skills_run;在默认 required 策略下,它要么进入合格 OS 沙箱,要么拒绝。这仍是在授权模型选择任意代码,只应放在用户与技能都可信的宿主上。→ 技能相关参数

5 · 结构化输出(JSON schema)

OpenAI 的 response_format 接受 text、json_object 与经过校验的 json_schema。服务器会注入严格的 JSON 指令,并在返回前校验输出。它还会把 schema(或通用的 JSON 对象语法)编译成约束解码的语法,逐 token 生效。语法无法表达的 schema 会退回到只把首个采样 token 约束为以 { 开头的候选,使爱闲聊的模型仍无法在 JSON 对象前输出散文。

curl -X POST http://localhost:5000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen3.5-9B-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 同样会被拒绝,但语法能等推理结束后再生效的家族除外——Gemma 4、Qwen 3.8 Flash Next、GPT-OSS、Muse-Glimmer、Muse Spark、DeepSeek V4.1、glm5next 与 Nemotron-H——在这些家族上语法从推理结束处开始约束。它可以与 skills 组合,但选中技能正文会被内联,而且该请求不会提供任何内置技能 / 代码工具,因为 schema 约束下无法生成调用标记。无效 schema 返回 HTTP 400;校验失败的输出返回 HTTP 422。

个人知识库 API

独立服务器设置 TS_KNOWLEDGE_DIRECTORY 后启用 /api/knowledge:GET/POST /collections、DELETE /collections/{id}、文档列表/上传/删除/重新索引与版本化原文件下载,以及 POST /search 和 POST /read。上传每请求一个 multipart 文件;搜索/读取显式传入 collectionIds。TensorAgent 的重命名、聊天导入和预览扩展不代表独立宿主提供同样端点。

推理请求使用 camel-case knowledgeCollections,支持 /api/chat(WebUI)、/v1/chat/completions、/v1/responses、/api/chat/ollama 与 /api/generate。缺失/空选择不检索;无效选择在流开始前拒绝。Generate 仅检索/引用,不运行智能体工具循环。回复的 knowledge_sources、knowledge_citation_repairs 与有界 knowledge_tool_steps 提供来源及实际工具执行元数据。知识库兼容流会缓冲最终回答以完成引用绑定,再发送只追加的文本增量;结构化 JSON 保留标记并单独提供来源元数据。配置、请求示例、OCR 与覆盖限制 →

6 · 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 预览)、单独的 replace(到目前为止的完整答案:已作为答案流式发送的文本被证实是推理时,在 thinking 帧之后发送,即 Nemotron-H Reasoning-128K 在关闭思考的 prompt 已关闭推理块后又自己关闭它;之后的 token 帧在其后追加,客户端在下一轮发回的是它最终显示的内容)、skill_step(已完成内置工具以及 round/files)、tool_progress(临时的 writing / running / finished 活动,含 tool、text、seconds、detail、agents),以及终止的 done 摘要。每个 skill_step 帧都带 agent_id:父智能体为 /root(没有委派时所有帧都是这个值),子智能体为 /root/<task_name> 这样的路径。每个 tool_progress 帧都带 agents:只有 wait_agent 运行期间它才是数组,其余时候为 null;数组中本请求的每个子智能体一项(父智能体 /root 不在其中),含 agent_id、parent_id、task、agent_type、status、tool、tool_status、detail、result 与 error——内置 UI 据此渲染实时的子智能体面板。一次性的 {"artifact_verified": true, "files": [{name, bytes, url}]} 帧会在某个生成的交付文件通过宿主检查后给出它。浏览器完成后会清除活动状态,不构造工具历史记录,但会保留产物下载标签。

data: {"tool_progress":"writing","tool":"shell","text":"{\"command\":\"python","seconds":0,"detail":null,"agents":null}
data: {"tool_progress":"running","tool":"shell","text":"writing report.xlsx\n","seconds":2.1,"detail":"python make_report.py","agents":null}
data: {"tool_progress":"finished","tool":"shell","text":null,"seconds":2.4,"detail":null,"agents":null}

WebUI 回答生成期间,用同一会话发送下一条用户指令并带 "steer": true,会先取消并清理旧生成,再继续;保留已生成助手文本。此扩展需要 v2026.10.10 后的构建,是 /api/chat 会话行为,不是所有兼容端点的通用参数。

文件上传(/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 限制读取页数(默认全部)。文件存放在服务端可执行文件旁的 uploads/ 中(TENSORSHARP_UPLOAD_DIR 可改位置),并通过 /uploads/ 提供。--upload-max-mb 限制客户端提交的单个文件(上传文件与解码后的 base64 附件都算,默认 500;超出返回 413);--upload-quota-mb 限制整个目录(含生成的输出,超出返回 507),--upload-ttl-hours 删除过旧的文件——两者默认关闭。

图像生成与编辑(/api/image-generate · /api/image-edit · /stream)

托管模型为 Qwen-Image-2.1 的 qwen_image DiT 时可用。托管其他模型时,/api/image-edit 返回 400 与 The loaded model is not a Qwen-Image-2.1 model.,/api/image-generate 返回 400 与 Text-to-image generation requires a Qwen-Image-2.1 model.;对应的 /stream 路由则在最终的 {"done": true, "error": …} 事件里给出同一条消息,而不是返回 400。POST /api/image-generate 接收形如 {"prompt": "...", "width": 2048, "height": 2048, "steps": 40, "cfg": 1, "seed": 42} 的 JSON 并生成图像。POST /api/image-edit 执行一次编辑,且至少需要一张参考图:可提交 multipart(一个或多个 image 部分,按参考图顺序,外加 prompt),也可提交 JSON,用 imagePaths(或旧的单个 imagePath)指向上传目录中的文件。两者都接受 negativePrompt、targetArea、width、height、steps、cfg 与 seed:省略的值,或为 0 的 steps / cfg,使用模型默认值(40 步、CFG 1,尺寸为 2048×2048 或与第一张参考图宽高比一致、面积大致相同——若服务端启动时同时给了 --width 与 --height,该默认值仅用于请求未指定尺寸或 targetArea 时;启动默认值不在网格上会向下取整并给出警告),但服务端启动时加载的、带采样配方的 --lora 插件会以该配方中的步数与 CFG 代替(见 LoRA 插件);显式尺寸须为 32 的倍数,并优先于 targetArea。/api/image-edit 还接受 keepSourceSize:为 true 时返回第一张参考图的精确宽高——按其宽高比、在该编辑原本使用的面积与其自身面积之内采样,再缩放回源图尺寸——因此对编辑结果再次编辑时尺寸保持不变;服务端 Web UI 的每次编辑都会发送它。它不能与 width/height 同时使用,/api/image-generate 也不接受它,两种情况都返回 400。seed 决定初始噪声;编辑的噪声还取决于参考图,因此用生成某张图时的种子和尺寸去编辑它,不会从这张图自己的噪声开始(以 TS_QWEN21_EDIT_NOISE=seed 启动的服务端只按种子为编辑取噪声)。两者都返回 { ok, url, width, height, elapsedSeconds },PNG 从 /uploads/ 提供。POST /api/image-generate/stream 与 POST /api/image-edit/stream 接收相同 JSON,但以 SSE 流式返回去噪进度:每步帧为 {"imageGenerate": true, …} 或 {"imageEdit": true, "step": i, "total": N, "image": "data:image/png;base64,..."}(最多 8 张均匀预览),最后返回 {"done": true, "url": "...", "width": ..., "height": ..., "elapsedSeconds": ...} 或 {"done": true, "error": …}。整个进程中的图像请求串行执行;服务端 Web UI 使用这些图像路由,而 TensorAgent 在自己的 /api/chat 宿主中分派图像轮次。

蒙版编辑。Multipart 的 /api/image-edit 接收一个 mask 文件。JSON 与 /api/image-edit/stream 先通过 /api/upload 上传源图和蒙版,再用返回的文件名填写 imagePaths 与 maskPath。可选字段为 maskMode(grayscale 白色编辑,或 alpha 透明像素编辑)、maskInvert、maskFeather(0–1024)、maskCrop 与 maskCropPadding(0–16384,默认 64)。蒙版必须与第一张参考图同尺寸;结果和预览均保留该画布与保护像素。蒙版需要输入图和 Qwen-Image-2.1,生成接口会拒绝蒙版,没有蒙版时也拒绝蒙版选项。选区工作流与限制 →

LTX 2.5: 使用 --config config/ltx-2.5-distilled.json;NVIDIA 用 cuda/ggml_cuda,Apple 用 mlx/ggml_metal(实际为 MLX),CPU 别名选择直接 CPU。默认 1024×1024×17、24 fps,宽高为 64 倍数、帧数 8k+1;8 步半尺寸加 3 步精修,CFG 1。 五文件、音频、条件与限制 →

视频生成(/api/video-generate · /api/video-generate/stream · /v1/videos/generations)

当托管的模型能生成视频时可用 —— LTX 2.5 与 MiniMax-H3 联合生成音视频,Wan 只生成视频。判定依据是 IVideoGenerationModel 接缝而不是架构字符串,因此其他模型会返回 400 与 The loaded model is not a video-generation model.。模型自己拒绝的请求 —— 用 Ref2VA 的输入去跑 FL2VA 权重、在同一个请求体里同时给关键帧与具名参考、模式缺少必需输入 —— 在 /api/video-generate 上以 400 返回模型自己的报错信息,而不是笼统的 500;在流式接口上则以终止帧 {"done": true, "error": …} 返回同一条信息。三个接口共用同一个解析器,因此字段完全一致;视频生成在进程内串行执行。

# MiniMax-H3:提示词 -> MP4,外加一同生成的 32 kHz 立体声 WAV
curl -s http://localhost:5000/api/video-generate \
  -H "Content-Type: application/json" \
  -d '{
        "prompt": "a red fox trotting through falling snow, cinematic",
        "width": 640, "height": 384, "frames": 22, "fps": 24,
        "steps": 8, "cfg": 1.0, "seed": 42,
        "videoMode": "t2v", "generateAudio": true
      }'
{ "ok": true, "url": "/uploads/video-8f3c….mp4", "audioUrl": "/uploads/video-8f3c….wav",
  "width": 640, "height": 384, "frames": 22, "fps": 24,
  "seed": 42, "codec": "h264", "elapsedSeconds": 100.0 }

请求字段(camelCase):prompt(必填)、width、height、frames、steps、cfg、cfg2、seed、fps、flowShift、negativePrompt、sampler、cfgCacheStride、videoMode(t2v / i2v / fl2v / ref)、generateAudio、imagePath、image、endImage、referenceImages、referenceVideos、referenceAudios、referenceVideoAudios。其中为音视频联合生成与参考条件新增的七个字段同时接受 snake_case —— video_mode、generate_audio、end_image、reference_images、reference_videos、reference_audios、reference_video_audios —— 两种写法同时出现时 camelCase 优先;其余字段只接受 camelCase。

image 是内联 base64 形式(允许带 data:…;base64, 前缀)。imagePath、endImage 以及每一个 reference* 条目都必须指向 /api/upload 之前返回过的文件,并被限制在上传目录之内 —— 目录之外的路径会被拒绝,报错形如 referenceImages entries must reference previously uploaded files.。referenceVideoAudios 与 referenceVideos 按下标配对:第 i 项就是第 i 段片段的音轨。当模型没有产出音轨时 audioUrl 为 null —— 保留独立 WAV;FFmpeg 与 Apple AVFoundation 路径可混入音轨,其他导出提供器可能返回分离音轨。

POST /api/video-generate/stream 接收相同请求体,并以 SSE 流式返回:每个心跳是 {"videoGen": true, "step": i, "total": N, "phase": …, "detail": …, "elapsedSeconds": …, "etaSeconds": …},最后是终止帧 {"done": true, "url": …, "audioUrl": …, "width": …, "height": …, "frames": …, "fps": …, "seed": …, "codec": …, "elapsedSeconds": …} —— 或者 {"done": true, "error": …}。

POST /v1/videos/generations 是同一任务的 OpenAI images 风格封装。在上述字段之外,它还接受 "size": "832x480"(会拆成 width/height)、negative_prompt,以及取值 "url" 或 "b64_json" 的 response_format,返回 { created, data: [{ url, b64_json }], audio_url, width, height, frames, fps, seed, codec, elapsed_seconds },MP4 由 /uploads/ 提供。

GET /api/models 是客户端在构造请求之前了解托管视频模型能接受什么的入口:除模型列表外,它还会返回一个 video 对象,包含 family("ltx-2.5"、"minimax-h3" 或 "wan")、supportsAudio、supportsImageConditioning、supportsEndImageConditioning、supportsReferenceConditioning 与 maxReferenceImages(MiniMax-H3 Ref2VA 上为 9)—— 对所有非视频模型则为 null。内置网页界面读的正是这一块,用来决定该提供哪些附件控件 —— 首帧、尾帧,或数量被 maxReferenceImages 限住的参考图 —— 而不是去匹配架构名字符串;完全拿不到这一块时(较旧的服务端,或不上报能力的模型),它会退回到一直以来那种单张图像的请求形式。

LTX 2.5 还接受 imageConditions(别名 image_conditions):数组中每项提供 imagePath(已上传文件)或内联 base64 image、frameIndex(非负数,或 -1 表示末帧)与可选 strength(0–1)。帧位置不可重复;这种列表用于时间引导,videoMode: "ref" 的 Ingredients 身份参考是另一条契约。

7 · 采样参数

Ollama 风格(在 options 对象内)

下表默认值用于请求省略字段时,可通过服务端参数 / TENSORSHARP_* 环境变量配置(见服务端参数)。

参数类型默认说明
num_predictint--max-tokens(20000)最大生成 token 数
temperaturefloat0.8采样温度(0 = 贪心)
top_kint40Top-K 过滤(0 = 禁用)
top_pfloat0.9核采样阈值(1.0 = 禁用)
min_pfloat0最小概率过滤
repeat_penaltyfloat1.1重复惩罚(1.0 = 无)
repeat_last_nint64重复 / 存在 / 频率惩罚回看的最近 token 数
presence_penalty / frequency_penaltyfloat0存在 / 频率惩罚
seedint-1随机种子(-1 = 随机)
stoparraynull停止序列

OpenAI 风格(顶层)

max_tokens 或 max_completion_tokens(省略时默认 --max-tokens,即 20000)、temperature、top_p、presence_penalty、frequency_penalty、seed、stop(字符串或数组),以及 response_format(text / json_object / json_schema)。在 OpenAI 规范之外,同一顶层还接受 top_k、min_p、repeat_penalty(或 repetition_penalty)与 repeat_last_n,默认值见上表。

8 · Python 客户端示例

使用 requests(Ollama 风格)

import requests

resp = requests.post("http://localhost:5000/api/generate", json={
    "model": "Qwen3.5-9B-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.5-9B-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.5-9B-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()