团队很少会拿公网 API 来落地编码 agent。常见的形态是:你们已经跑着模型,前面架一个网关,key 由基础设施管而不是每个开发者自己管。Cetas 把这种用法当作一等供应商对待。
工作原理
providers 里每一个没有被内置供应商占用的 key,都会变成一个 自定义供应商。不需要注册、不需要写插件:key 本身就是供应商 id,而且它和内置供应商同等对待 —— /login、/model、目录刷新、模型缓存、编辑器入口全都认得它。端点说哪种协议,由条目里的 protocol 声明。
最小配置
{
"providers": {
"my-gateway": {
"base_url": "https://llm.internal.example/v1",
"api_key": "sk-...",
"models": [
{ "id": "qwen3-32b", "context_window": 131072 }
]
}
}
}base_url—— 必填。服务端要求/v1时就带上;大多数 OpenAI 形状的服务端已经把版本号写进 base URL。api_key—— 实际使用中必填。也可以不写在这里,改用/login my-gateway,凭据会落到<CETAS_HOME>/.cetas/credentials/,密钥不进 settings 文件。models—— 必填,且不能为空。每个条目要有id,id 不能重复。
endpoint_prefix 是给”base_url 只写了裸主机”(http://10.0.0.4:8080)而 API 挂在 /v1 下的情况准备的。能把前缀写进 base_url 就写进去,这一项留空。
选择协议
自定义端点说三种协议之一:
protocol |
请求 | 协议 |
|---|---|---|
chat_completions(缺省) |
POST {base_url}{endpoint_prefix}/chat/completions |
OpenAI Chat Completions |
responses |
POST {base_url}{endpoint_prefix}/responses |
OpenAI Responses API |
messages |
POST {base_url}{endpoint_prefix}/messages |
Anthropic Messages API |
{
"providers": {
"claude-proxy": {
"base_url": "https://api.anthropic.com",
"endpoint_prefix": "/v1",
"protocol": "messages",
"api_key": "sk-ant-...",
"models": [{ "id": "claude-sonnet-5", "context_window": 200000 }]
}
}
}- 不写
protocol就是chat_completions:协议出现之前写的条目,行为一个字节都不变。 - 路径后缀接在
base_url+endpoint_prefix之后。版本段二选一写:写进base_url(https://llm.internal.example/v1),或者裸主机配endpoint_prefix: "/v1"—— 别两边都写。 protocol是供应商级的,models[]的每一条都可以覆盖它,所以「一个端点、几个模型系列各说不同协议」仍然是一个供应商 id。- 写了不认识的值会明确报错并列出可选值,不会退化成 chat 协议:退化的结果是把请求发到错误形状的端点上,然后收到一个看不懂的
400。
凭据放在哪
auth 决定 key 怎么上线:
auth |
效果 |
|---|---|
auto(缺省) |
chat_completions/responses 用 Authorization: Bearer;messages 用 x-api-key 加 anthropic-version: 2023-06-01 |
bearer |
所有协议都用 Authorization: Bearer —— 给用 Bearer 挡在 Messages 前面的网关 |
x_api_key |
所有协议都用 x-api-key |
各协议的推理档位
档位的含义到处一样 ——「这个模型该想多深」—— 但每条协议写法不同,而且不会给一个不知道刻度的端点编数字:
chat_completions—— 顶层reasoning_effort,原样发出。responses——reasoning.effort;选了档位就不再发 temperature,推理端点会拒。messages——thinking: {"type": "enabled"}。档位名字不会被换算成budget_tokens数字。off—— 哪条协议都不发,端点用自己的默认值。
responses 还是无状态调用(store: false),不回放 reasoning items:通用端点并不声明 encrypted reasoning 契约。
输出上限
max_output_tokens(正整数,缺省 8192)是 messages 协议上的 max_tokens,该字段必填。回合自己带了上限就以回合为准,这一项只补缺口。
声明模型能力
选择器只能列出你声明过的东西。除了 id,其余全是可选项,而留空比猜一个更安全:
| 字段 | 写在 | 作用 |
|---|---|---|
context_window |
模型 | 容量(token);不写则该模型不做自动压缩 |
compact_threshold |
模型 | 触发比例,需满足 > 0 且 <= 1(默认 0.88) |
image_input |
模型或供应商 | 接受图像输入;供应商级作为其模型的默认值 |
reasoning_effort |
模型或供应商 | 固定一个档位 —— 不出现选择器 |
reasoning_efforts |
模型或供应商 | 选择器里要列出的档位 |
{
"providers": {
"my-gateway": {
"base_url": "https://llm.internal.example/v1",
"models": [
{
"id": "qwen3-235b",
"context_window": 262144,
"compact_threshold": 0.85,
"reasoning_efforts": ["low", "medium", "high"],
"image_input": false
}
]
}
}
}安全与稳定性
- 优先用
/login my-gateway而不是内联api_key:凭据在<CETAS_HOME>/.cetas/credentials/,不会跟着文件被粘进工单。 - 拒绝
prompt_cache_key的网关也能用:Cetas 会在本进程内记住这次拒绝,后续不再发送,而不是让整个回合失败。 - 自定义供应商不会从任何公开模型库刷新。它的目录就是你声明的那些 —— 这也正是为什么写错的
context_window会安静地拖低压缩质量,而不会报错。
验证
/login my-gateway
/model然后发一条消息。如果 /model 是空的,要么这个供应商没组装起来(检查 id 是否合法、是否撞了内置 id),要么 models 数组被拒绝了 —— 写坏的段会明确报错,而不会被修补。
同一份配置编辑器入口和 runner 直接继承,不需要每个入口重配一遍。内置 id 的优先级见供应商。