文档导航

API 参考

ollaya serve 在 http://localhost:11435 上暴露两个 API:

  • 原生 API,位于 /api/* 下,仿照 Ollama 的设计,用于决策和模型管理;
  • TypeSafe 兼容 API,位于 /v1/* 下,与 TypeSafe 的线上格式完全一致,所以现有的 TypeSafe SDK 无需改动即可使用。见 TypeSafe 兼容性。
方法 路径 用途
GET、HEAD / 存活检查:Ollaya is running
GET /api/version 服务器版本
POST /api/decide 就某个状态回答类型化的问题;也用于加载和卸载模型
GET /api/tags 本机上的模型
POST /api/show 某个模型的详情
GET /api/ps 已加载到内存的模型
POST /api/pull 下载一个模型(流式传输进度)
DELETE /api/delete 删除一个模型
POST /api/copy 把模型复制成一个新名字
POST /api/create 从另一个模型创建一个模型(流式传输进度)
POST /v1/systemone TypeSafe System One
POST /v1/decisions /v1/systemone 的别名
GET /v1/models TypeSafe 模型列表

/api/push 和 /api/blobs/:digest 是保留端点,返回 501 NOT_IMPLEMENTED。Ollama 的文本端点(/api/generate、/api/chat、/api/embed)返回 404:决策模型从不生成文本。

约定

  • JSON。 请求和响应体都是 JSON 对象。无论 Content-Type 是什么,请求体都按 JSON 解析,所以 curl -d 直接可用。请求最大 8 MiB。
  • 字段名是 snake_case。未知的请求字段会被忽略;null 表示缺省。
  • 模型名是 [host/][namespace/]model[:tag],不区分大小写。省略 tag 表示 latest。响应始终使用规范形式,如 laya:latest。
  • 数字。 概率、置信度、score 和 noul 四舍五入到 4 位小数。时长是以纳秒计的整数;时间戳是 UTC 的 RFC 3339。
  • 流式传输。 /api/pull 和 /api/create 流式传输以换行分隔的 JSON,每行一个对象,最后恰好以一个 {"status":"success"} 或一行错误结束。要单个响应就发送 "stream": false。
  • 请求 ID。 每个响应都带 X-Request-Id,/v1/* 的响应还带 x-typesafe-request-id。客户端发来的合法 X-Request-Id 会被回显。
  • 并发。 一个已加载的模型一次只跑一个请求,每个请求一趟答完它的所有问题。发往同一个模型的请求排队,所以一次多发并不会更快完成;这时每个请求的往返时间都包含等待。把关于一个状态的所有问题放进一个请求里问。不同的已加载模型并行运行。
  • 没有隐式拉取。 没有任何端点会把下载模型当作副作用。ollaya run 会先拉取;应用程序调用 /api/pull。

错误

每个端点上的每个错误都有这样的响应体:

{
  "error": "model \"laya:xl\" not found, try pulling it first",
  "code": "MODEL_NOT_FOUND"
}
字段 含义
error 人类可读的消息。不要解析它;唯一固定的一条消息是 model "<name>" not found, try pulling it first,与 Ollama 一样。
code 机器可读的代码。据此分支。
detail 仅对 INVALID_REQUEST、TOO_MANY_OPTIONS、INPUT_TOO_LONG 和 STATE_TRUNCATED 出现:每个校验问题,采用 TypeSafe(FastAPI)的 ValidationError 形状:loc、msg、type,有时还有 ctx。
代码 HTTP 出现时机 重试
INVALID_JSON 400 请求体缺失、不是 JSON,或不是对象 否
INVALID_REQUEST 422 请求体未通过校验;detail 列出每个问题 否
TOO_MANY_OPTIONS 422 一个问题的选项超出了模型的选项预算 否
INPUT_TOO_LONG 422 state 超过 65,536 个 token 否
STATE_TRUNCATED 422 /v1/systemone 或 /v1/decisions 为了塞进模型的上下文会丢掉 state 的一部分 否
UNAUTHORIZED 401 设置了 OLLAYA_API_KEY 而请求没带密钥 否
FORBIDDEN 403 浏览器的 Origin 或 Host 头不被允许 否
MODEL_NOT_FOUND 404 模型(或路由器的目标)不在这台机器上;对拉取而言,是不在 registry 里 否
NOT_FOUND 404 没有这个端点 否
METHOD_NOT_ALLOWED 405 端点存在,方法不存在 否
OPERATION_IN_PROGRESS 409 一次拉取或创建正在写同一个模型名 它结束之后
REQUEST_TOO_LARGE 413 请求体超过 8 MiB 否
QUEUE_FULL 503 已有 OLLAYA_MAX_QUEUE 个请求在等待;附带 Retry-After: 1 发送 是
MODEL_LOAD_FAILED 500 模型无法加载(文件损坏、内存、OLLAYA_LOAD_TIMEOUT) 很少
INFERENCE_FAILED 500 运行器在决策过程中失败 是
STORAGE_ERROR 500 磁盘已满、权限或 I/O 否
INTERNAL 500 程序缺陷;服务器日志在请求 ID 下有详情 是
UNSUPPORTED_MODEL 501 这个构建无法运行该模型的格式 否
NOT_IMPLEMENTED 501 保留端点 否
REGISTRY_ERROR 502 registry 无法访问或无效 是
DIGEST_MISMATCH 502 下载的内容与其 sha256 不符,已被丢弃 是

代码集合是开放的:遇到未知代码就按它的 HTTP 状态码处理。一个校验错误会一次列出所有问题:

{
  "error": "state: Field required; questions.urgency.score.criteria: List should have at least 2 items after validation, not 1",
  "code": "INVALID_REQUEST",
  "detail": [
    {"loc": ["body", "state"], "msg": "Field required", "type": "missing"},
    {
      "loc": ["body", "questions", "urgency", "score", "criteria"],
      "msg": "List should have at least 2 items after validation, not 1",
      "type": "too_short",
      "ctx": {"field_type": "List", "min_length": 2, "actual_length": 1}
    }
  ]
}

一旦流已经开始,失败会以同样形状的一行出现在最后,如 {"error": "…", "code": "DIGEST_MISMATCH"}。在把每一行当作进度读取之前,先检查它有没有 error。

问题

/api/decide、/v1/systemone 和 /api/create 共用同一套问题 schema,即 TypeSafe 的。一个请求有 1–256 个问题,以任意 id 为键;答案按相同顺序返回。

type instructions criteria 答案
choice 可选 必填:对象 标签 → 描述,或一组标签的数组;2–255 个选项 choice、confidence、probabilities
score 可选 必填:档位描述的数组,从档位 0 开始;2–10 个档位 score、confidence、legend、probabilities
noul 可选 可选:{"true": "…", "false": "…"} noul
  • instructions 可以是字符串、对象、数组或 null。当它缺省或为 null 时,模型改为读取问题 id,所以像 is_spam 这样有描述性的 id 本身就能用。
  • state 是字符串、对象或数组,最多 65,536 个 token。如果它超出模型可用的上下文,/api/decide 会截断它并报告 state_truncated: true。/v1/systemone 和 /v1/decisions 返回 422 STATE_TRUNCATED,并在 detail[0].ctx.model 里给出作答的模型。
  • 模型限制。 每个选项都需要在模型的上下文里占空间:laya:en 约 125 个选项(512 token),laya:multilingual 约 250 个(1,024)。再多的就是 422 TOO_MANY_OPTIONS。对路由器来说,适用目标模型的限制。

答案是 TypeSafe 的形状,字段顺序如下:

type 字段
choice choice:最可能的标签。confidence。probabilities:标签 → 概率,按 criteria 顺序。
score score:期望档位 Σ i·pᵢ,可以落在档位之间。confidence。legend:"0"… → 该档位的描述。probabilities:"0"… → 概率。
noul noul:该陈述成立的概率。没有 confidence,与 TypeSafe 一样。

confidence 是 TypeSafe 归一化后的最高概率,对 K 个选项即 (K · pmax − 1) / (K − 1):当每个选项等可能时为 0,当一个选项占尽全部概率时为 1。公式对每个模型都一样,但一个给定的置信度意味着什么却不一样:各模型的校准不同,所以要在你自己的数据上为每个模型调一个阈值。概率用各模型的温度来校准。在 CUDA GPU 上跑的是 fp16 计算图,它的答案在接近平手时可能与 fp32 不同。

keep_alive

一个模型在请求结束后保持加载多久,语义与 Ollama 相同:

取值 含义
"5m"、"1h30m"、"300ms"、300、"300" 请求结束后保持加载这么久
0、"0"、"0s" 请求一结束就卸载
-1、"-5m"、任何负值 保持加载,直到服务器停止或显式卸载
缺省或 null OLLAYA_KEEP_ALIVE,默认 5m

计时器在请求结束时启动,以最近一次请求的取值为准。对路由器来说,它作用于作答的那个目标。/v1/* 会忽略 keep_alive。

决策

POST /api/decide

在一次前向传播中就某个状态回答类型化的问题。请求体是 /v1/systemone 的请求体加上原生选项;响应是 TypeSafe 的响应加上原生的字段,所以 TypeSafe 客户端也能解析它。

字段 类型 必填 说明
model string 是 模型名
state string、object 或 array 决策时必填 没有它,请求会加载或卸载模型(见下)
questions object 必填,除非模型有内置问题 完全替换模型自带的问题
preset string 否 一个 预设 的名字,内置或自定义,用来代替 questions
images 字符串数组 否 对视觉模型:多张 PNG 图片,base64 或 base64 的 data: URL。Decider 接受一张;winnow:e4b-vision 最多接受 16 张。见 图像
keep_alive string 或 number 否 见 keep_alive
extras 字符串数组 否 ["laya"] 会给每条答案加上 laya 自己的置信度和动作概率
stream boolean 否 保留;true 会被拒绝
curl http://localhost:11435/api/decide -d '{
  "model": "laya",
  "state": "I was charged twice for my subscription this month. Please refund the second charge.",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this ticket?",
      "criteria": {
        "billing": "Payments, invoices and refunds",
        "technical": "Bugs, errors and outages",
        "account": "Login, profile and settings"
      }
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is this ticket?",
      "criteria": ["Can wait", "Needs attention this week", "Needs attention today"]
    },
    "refund": {
      "type": "noul",
      "instructions": "The customer asks for money back.",
      "criteria": {"true": "Asks for a refund", "false": "Does not ask for a refund"}
    }
  },
  "keep_alive": "10m"
}'
{
  "model": "laya:en",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "billing",
      "confidence": 0.7781,
      "probabilities": {"billing": 0.8521, "technical": 0.0611, "account": 0.0868}
    },
    "urgency": {
      "type": "score",
      "score": 1.1982,
      "confidence": 0.3418,
      "legend": {"0": "Can wait", "1": "Needs attention this week", "2": "Needs attention today"},
      "probabilities": {"0": 0.1203, "1": 0.5612, "2": 0.3185}
    },
    "refund": {"type": "noul", "noul": 0.9127}
  },
  "usage": {"input_tokens": 118, "output_tokens": 0},
  "routing": {
    "router": "laya:latest",
    "model": "laya:en",
    "route": "english",
    "reason": "English Latin text"
  },
  "state_truncated": false,
  "done_reason": "decide",
  "created_at": "2026-09-24T09:30:12.418Z",
  "total_duration": 18734512,
  "load_duration": 0,
  "eval_duration": 16302117
}
字段 含义
model 作答的模型:对路由器来说,是它的目标(laya 请求对应 laya:en)
answers 问题 id → 答案,按问题顺序
usage 读入的 input_tokens;output_tokens 永远是 0
routing 对路由器来说:router、选中的 model、一个稳定的 route 键和一个有信息量的 reason。否则为 null。
state_truncated 如果为了塞进模型的上下文丢掉了一部分状态,则为 true
done_reason "decide"、"load" 或 "unload"
created_at 响应生成的时间
total_duration 从收到请求到响应的纳秒数,包含排队
load_duration 等待模型加载所花的纳秒数;模型已热时为 0
eval_duration 在运行器里花掉的纳秒数:tokenization、前向传播、校准

带上 "extras": ["laya"] 时,每条答案还会有一个 laya 对象:confidence(laya 基于熵的置信度)和 act_probability(来自模型的动作头,或 null)。

图像

视觉模型(decider:2b-vision 或 winnow:e4b-vision)既能就状态、也能就一张图片回答问题。把图片以 base64 编码放在 images 里发送,就像 Ollama 的 images 那样:

curl http://localhost:11435/api/decide -d '{
  "model": "decider:2b-vision",
  "state": "A photo from the warehouse camera.",
  "images": ["'"$(base64 -w0 shelf.png)"'"],
  "questions": {
    "blocked": {"type": "noul", "instructions": "Is the aisle blocked?"},
    "fill": {"type": "score", "instructions": "How full is the shelf?", "criteria": ["empty", "half full", "full"]}
  }
}'
  • Decider: 每个请求一张图片,仅限 PNG。模型的预处理是逐值复现的,所以像素必须与模型作者解码出的一致。Rust 的 JPEG 解码器与 libjpeg-turbo 在某些像素上最多差 4 个色阶,所以目前还不接受 JPEG:先把它转成 PNG。
  • Decider: 图片会按模型期望的方式缩放到 32 像素的整数倍,之后最多可以有 4,096 个 16x16 像素的 patch,约一百万像素(1024x1024)。更大的图片会得到一个说明这一点的 422;先把它缩小。
  • Decider: 问题最多 10 个选项。同一个模型也能回答纯文本请求。
  • Winnow E4B vision: 最多 16 张有序 PNG,每个问题 2–64 个选项,都在图片、状态与问题合并后的上下文之内。对应的投影器从同一作者修订版单独下载。现有的 Winnow 文本标签不会加载它。
  • 一个不读图片的模型,遇到带 images 的请求会返回 422。

/v1/systemone 和 /v1/decisions 与 TypeSafe 的 API 保持完全一致,后者没有 image 字段。

加载与卸载。 不带 state 和 questions 的请求从不做决策。在没有 keep_alive,或它为正或负时,它会加载模型(对路由器则是每个目标)并返回 done_reason: "load"。用 keep_alive: 0 则卸载它("unload")。ollaya run 就是这样预加载,ollaya stop 则卸载。

curl http://localhost:11435/api/decide -d '{"model": "laya:en", "keep_alive": -1}'
curl http://localhost:11435/api/decide -d '{"model": "laya:en", "keep_alive": 0}'

一次决策对存储的数据没有副作用,所以可以安全重试。

预设

预设是一组有名字的问题。内置六个(triage、email、guard、moderation、router、agent),你也可以保存自己的。把 "preset": "NAME" 发给 /api/decide 来代替 questions。

curl http://localhost:11435/api/presets/create -d '{
  "name": "billing-check",
  "description": "Billing, and how upset the customer is",
  "questions": {
    "billing": {"type": "noul", "instructions": "The message is about a charge, an invoice or a refund."},
    "tone": {"type": "choice", "instructions": "How does the customer sound?", "criteria": {"calm": null, "annoyed": null, "angry": null}}
  }
}'
curl http://localhost:11435/api/decide -d '{"model": "winnow:e4b", "state": "I was charged twice this month.", "preset": "billing-check"}'
端点 请求体 作用
GET /api/presets – 内置预设在前、自定义在后:name、builtin、description、问题 id、modified_at
POST /api/presets/create name、questions、description(可选) 保存一个自定义预设,替换同名的那个
POST /api/presets/show name 一个预设及其问题
DELETE /api/presets/delete name 删除一个自定义预设

名字是 1 到 64 个小写字母、数字、- 和 _ 字符。内置名字不能被复用(422)或删除(403),未知名字是 404。自定义预设存放在模型旁边,所以服务器的每个客户端看到的都是同一批。

路由器

像 laya(laya:latest)这样的路由器没有权重:每个请求它都挑一个目标,由那个目标作答。laya 只读 state:

状态 route 作答者
英文 english laya:en
大多数是非拉丁文字(阿拉伯文、西里尔文、CJK 等) multilingual laya:multilingual
拉丁文字,但不是英文(土耳其语、德语等) multilingual laya:multilingual
完全没有字母 english(默认) laya:en

不带重音字母的全大写短文本,比如信用卡账单上的商户名(MIGROS KADIKOY ISTANBUL TR)、SKU 或用户名,通常无法识别,会走 laya:en。如果你知道语言,直接请求 laya:multilingual 或 laya:en;响应里的 model 会说明是哪个 checkpoint 作答的。

路由只花微秒级的时间。要按 route 分支,绝不要按 reason,它的措辞可能会变。路由器永远不会挑 laya:typed-decisions;直接请求它。

列出本机模型

GET /api/tags

本机上的模型,最新的在前。每个条目有 name、model(与之相同)、modified_at、以字节计的 size、digest(manifest 的 sha256,裸十六进制)和 details:parent_model、format(onnx、gguf 或 router)、family、families、parameter_size 和 quantization_level(它携带的精度,如 F16/F32,或 GGUF 模型的量化,如 Q8_0)。

{
  "models": [
    {
      "name": "laya:en",
      "model": "laya:en",
      "modified_at": "2026-09-24T08:11:02.117Z",
      "size": 853634822,
      "digest": "bf30e4654e9483ff1e6a4fe6fb21b8a71baff6c8a01013046e7d13339020efd7",
      "details": {
        "parent_model": "",
        "format": "onnx",
        "family": "laya",
        "families": ["laya"],
        "parameter_size": "421M",
        "quantization_level": "F16/F32"
      }
    }
  ]
}

显示模型详情

POST /api/show
curl http://localhost:11435/api/show -d '{"model": "laya:en"}'
字段 含义
license 许可证文本
modelfile 一份重建该模型的 Modelfile
parameters 设置在模型上的参数,每行一个 name value,如 precision fp32
questions 内置问题,或 null
router 对路由器来说:strategy、default 和 routes(route → model)。否则为 null。
details 同 /api/tags
model_info general.architecture、general.languages、general.source(固定的 Hugging Face 仓库),再加上各家族特有的键,如 laya.context_length。general.languages 列出模型训练和评测所用的语言(很多是 multilingual);构建在多语言基座上的模型仍可能读其他语言,所以在你的数据上量一下。
capabilities 它能回答的问题类型(choice、score、noul),如果有动作头还有 act
modified_at 同 /api/tags

路由器按它自身显示,不解析成目标。

列出运行中的模型

GET /api/ps

已加载的模型,按名字排序。路由器从不出现在这里;它们已加载的目标会出现。每个条目有 name、model、size(内存,RAM 加 VRAM)、digest、details(带实际加载的精度:F16 或 F32,或 GGUF 模型的量化)、expires_at(它何时卸载,保持加载时为 null)、size_vram、context_length 和 device(cpu、cuda:0、metal 等)。

拉取模型

POST /api/pull
{"model": "laya:en"}

把模型下载到本地存储,并用 sha256 校验每个 blob。拉取一个路由器也会拉取它路由到的每个模型。只下载本机需要的那几层,模型之间共享的 blob 只下载一次,中断的下载会续传。

响应流式传输进度,用 Ollama 的状态字符串:

{"status":"pulling manifest"}
{"status":"pulling 891102d37268","digest":"sha256:891102d372688fc2a094dac56a384bc537b87c63f21f9f3dac0be2b7cbc8d86c","total":842609210,"completed":420557117}
{"status":"pulling 891102d37268","digest":"sha256:891102d372688fc2a094dac56a384bc537b87c63f21f9f3dac0be2b7cbc8d86c","total":842609210,"completed":842609210}
{"status":"verifying sha256 digest"}
{"status":"writing manifest"}
{"status":"success"}

模型只有在 writing manifest 之后才出现在 /api/tags 里。对路由器来说,最后只有一个 success。无法解析的名字、registry 里没有的模型、以及无法访问的 registry,都是流开始之前的普通 HTTP 错误(422、404、502),所以 curl --fail 能生效。用 "stream": false 时,完成后响应是 {"status": "success"}。对同一个名字的第二次拉取会并入正在进行的那个。可以安全重试。

删除模型

DELETE /api/delete
{"model": "triage"}

删除该名字,以及没有其他模型使用的 blob。已加载的模型在其请求结束后卸载;删除一个路由器会保留它的目标。响应是 200 和空响应体,名字不存在时是 404 MODEL_NOT_FOUND;超时之后,把它当作成功。

复制模型

POST /api/copy
{"source": "laya:en", "destination": "my-guardrail"}

把模型复制成一个新名字,覆盖已存在的目标。响应是 200 和空响应体。

创建模型

POST /api/create

ollaya create -f Modelfile 背后的 API:CLI 读取 Modelfile 和它点名的文件,把它们的内容以 JSON 发送。

字段 类型 必填 说明
model string 是 要创建的名字
from string 是 一个本地模型,可能是路由器。它永远不会被拉取。
questions object 否 内置问题,像决策请求那样校验
calibration object 否 temperature:最多 3 个数字(choice、score、noul)。temperature_by_options:"<type>:<2|3-5|6-10|11+>" → number。
parameters object 否 precision:"fp16" 或 "fp32",用来固定一个计算图
license string 或 array 否 许可证文本
description string 否 一行,由 /v1/models 和 ollaya show 显示
stream boolean 否 默认 true
curl http://localhost:11435/api/create -d '{
  "model": "triage",
  "from": "laya:en",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this ticket?",
      "criteria": ["billing", "technical", "account"]
    }
  },
  "parameters": {"precision": "fp32"},
  "description": "Support ticket triage"
}'

流会为每个继承的层报告 using existing layer sha256:…,为每个新层报告 creating new layer sha256:…,然后是 writing manifest 和 success。层是内容寻址的,所以重复一次创建会得到同一个模型。

版本

GET /api/version
{"version": "0.1.0"}

TypeSafe 兼容端点

端点 说明
POST /v1/systemone 请求:model、state(必填)和 questions。响应:恰好是 model、answers 和 usage。
POST /v1/decisions /v1/systemone 的别名
GET /v1/models 本机模型,形式为 {"models": [{"name", "description", "release_date"}]}

/v1/* 会忽略 keep_alive 和 extras 这样的原生字段,也从不给响应加原生字段。错误使用与 /api/* 相同的响应体,TypeSafe SDK 能正确读取。见 TypeSafe 兼容性。

安全

服务器绑定到 127.0.0.1:11435,并且像 Ollama 一样信任本地调用方。把它绑定到另一个地址(OLLAYA_HOST=0.0.0.0)会让所有能连到这个端口的人都能够跑决策,以及拉取、删除和创建模型,所以:

  • OLLAYA_API_KEY 会让除 GET /、HEAD / 和 CORS 预检之外的每个请求都要求 Authorization: Bearer <key>;否则答案是 401 UNAUTHORIZED。TypeSafe SDK 就是这样发送它的密钥的,ollaya CLI 发送的是 $OLLAYA_API_KEY。当服务器在 loopback 之外监听却没设密钥时,它会记录一条警告。
  • TLS 不由服务器终结;要远程访问就在前面放一个反向代理。
  • 浏览器。 带 Origin 头的请求只允许来自 localhost、127.0.0.1、0.0.0.0 和 [::1](任意端口)、应用和编辑器的 webview,以及 OLLAYA_ORIGINS 里的来源(逗号分隔,* 通配)。loopback 服务器还会拒绝意外的 Host 头,这能挡住 DNS 重绑定。
  • 你的数据。 状态和问题永远不会被记录,也永远不会在错误里回显。
变量 默认值 作用
OLLAYA_HOST 127.0.0.1:11435 绑定地址;客户端的目标。loopback 地址还会在 [::1] 上监听,所以 Windows 程序能无延迟地通过 localhost 访问 WSL 里的服务器
OLLAYA_API_KEY 未设置 要求 Authorization: Bearer <key>
OLLAYA_ORIGINS 未设置 额外允许的浏览器来源
OLLAYA_KEEP_ALIVE 5m 默认的 keep_alive
OLLAYA_MAX_LOADED_MODELS 3 已加载模型的上限
OLLAYA_MAX_QUEUE 512 触发 503 QUEUE_FULL 前允许在途的请求数
OLLAYA_LOAD_TIMEOUT 5m 触发 500 MODEL_LOAD_FAILED 前的加载时限
OLLAYA_DEVICE auto auto、cpu、cuda 或 cuda:<n>
OLLAYA_MODELS ~/.ollaya/models 模型存储
OLLAYA_REGISTRY ollaya.dev 名字里的默认 registry 主机