清晰地构建。
兼容 OpenAI 的 API,模型路由公开,数据处理方式明明白白。
当前进展
API 已构建并测试完毕。在第一条模型路由开放之前,聊天请求会返回 503 inference_not_configured。实时状态始终可在 https://undergrove.vercel.app/api/status 查看。
它遵循 OpenAI API 格式,因此现有的 SDK 只需更改基础 URL 即可使用:支持带图片的聊天、函数调用和结构化输出,以及嵌入、转录和语音。要让请求连我们的网关也无法读取,请像工作区一样使用加密模式。
快速入门
基础 URL:https://undergrove.vercel.app/v1。用 GET /v1/models 列出模型,然后以流式方式获取回复:
curl https://undergrove.vercel.app/v1/chat/completions \
-H "Authorization: Bearer $UNDERGROVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"stream": true,
"messages": [{"role": "user", "content": "Help me explore an idea."}]
}'HTTP/2 200
9 个标头
- content-type
- text/event-stream; charset=utf-8
- cache-control
- no-store
- x-undergrove-route
- tinfoil
- x-undergrove-attestation
- verified-by-gateway
- x-undergrove-enclave-host
- inference.tinfoil.sh
- x-undergrove-enclave-measurement
- c0ffee12…9a7d
- x-undergrove-enclave-key
- 4f2b81e0…d3c6
- x-undergrove-attestation-sha256
- 8e41c7aa…01fb
- x-undergrove-attestation-url
- /v1/attestation?sha256=8e41c7aa…01fb
目前的回答
只改一行,从 OpenAI 迁移过来
Undergrove 支持 OpenAI API,因此官方 SDK 可以直接使用。在创建客户端的那一行代码中,填入 Undergrove 的基础 URL 和 Undergrove 密钥,然后指定这里提供的某个开放模型即可。
import os
from openai import OpenAI
client = OpenAI(base_url="https://undergrove.vercel.app/v1", api_key=os.environ["UNDERGROVE_API_KEY"])
response = client.chat.completions.create(
model="<model-id>",
messages=[{"role": "user", "content": "Summarise this contract in three points."}],
)
print(response.choices[0].message.content)
需要更改 2 行:客户端的基础 URL 和密钥,以及模型。
也可以完全不改代码:在创建客户端时未传入这两项的情况下,两个 SDK 都会读取 OPENAI_BASE_URL 和 OPENAI_API_KEY,因此只要把它们分别设为 https://undergrove.vercel.app/v1 和你的密钥,就能原样切换应用。流式输出、图片、函数调用和结构化输出按下文所述工作,适用于支持它们的模型。
开发者工具
在你自己的机器上验证安全飞地并加密的客户端。每个客户端都会先检查 Tinfoil 安全飞地的证明,再将每个请求加密发送到该安全飞地,因此 Undergrove 的中继在转发时无法读取它。它们位于仓库的 packages/ 文件夹中,针对本地替身服务进行测试,目前尚未发布到任何包注册表。
试用 CLI
这个终端会重放真实的 undergrove 命令。它的输出录制自用于测试这些包的本地替身服务:verify 是对一份录制的 Tinfoil 证明进行的真实检查,models 列出当前的模型目录,而 ask 的回答是预先写好的。此页面背后没有模型,你输入的任何内容都不会离开此页面。
滚动到终端时才会加载。
示例应用
packages/examples 中的三个小型 Web 应用,每个都由一个页面和一个持有密钥的 Node 服务器组成。没有密钥时,它们针对本地替身服务运行并使用预先写好的回答,任何内容都不会离开你的电脑;设置 UNDERGROVE_API_KEY 后,同样的代码会验证安全飞地,并将每个请求加密发送给它。
根据帮助中心文件作答的聊天,以服务器发送事件的形式流式传送到页面。
npm run support-bot位于 packages/examples;源代码在 packages/examples/support-bot
const SYSTEM = `You are the support assistant for Acme Bikes.
Answer only from the help centre below. Quote prices and times exactly.
When it doesn't cover a question, say so and offer the contact options.
Keep answers short: two to four sentences, or a short list.
${knowledge}`;
// Streams one answer. `history` is the conversation so far, as
// [{ role: "user" | "assistant", content }].
export async function answer(undergrove, history, send) {
const stream = await undergrove.chat.completions.create({
model,
stream: true,
max_tokens: 600,
messages: [{ role: "system", content: SYSTEM }, ...history.slice(-12)],
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content;
if (text) send({ text });
}
}在你的编辑器和 CI 中
- Node SDK@undergrove/sdk:OpenAI 风格的聊天、流式输出、嵌入、转录和语音,默认加密。
packages/sdk-js - Python SDKundergrove,基于 Tinfoil 的 Python SDK,同样默认加密。
packages/sdk-python - CLIundergrove ask、models 和 verify,密钥保存在系统钥匙串中。面向编辑器:--events 以 JSON 行流式输出,acp 将其作为智能体运行,mcp 将其作为工具运行。
packages/cli - VS Code一个聊天面板,以及针对眼前代码的“Explain selection”“Fix this”(含“Apply fix”)和“Write tests”。
packages/vscode - JetBrains IDEs在 IntelliJ IDEA、PyCharm 等 IDE 中提供工具窗口聊天和同样的三个操作,密钥保存在 IDE 的密码保险库中。
packages/jetbrains - ZedUndergrove 作为 Agent Panel 中的智能体,支持 /explain、/fix、/tests 和 /review,或作为 Zed 自带智能体的工具。两者都运行 CLI,在你的机器上完成加密。
packages/zed - Neovim:UndergroveAsk,以及对可视选区使用 :UndergroveExplain,回答会流式显示在浮动窗口中。
packages/neovim - Raycast在任何应用中使用“Ask Undergrove”和“Ask About Selected Text”。
packages/raycast - GitHub ActionUndergrove Review:每个拉取请求一次审查,包含行内评论和建议,并在页脚附上证明。
packages/github-action
试验场
在此页面用你自己的密钥发送聊天请求,查看回答、用量和原始响应,以及对应的 curl 请求。它和其他 API 调用一样,从你的浏览器发送到本站的 /v1,并照常消耗你的配额或额度。
选择“加密”即可按 SDK 加密模式的方式发送:Tinfoil 的 SDK 在此页面中验证安全飞地,然后用它的密钥密封请求。试验场会显示实际离开页面的字节、返回的内容,以及请求所密封到的安全飞地的回执。
用 curl 发送同一请求
curl --no-buffer https://undergrove.vercel.app/v1/chat/completions \
-H "Authorization: Bearer $UNDERGROVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"messages": [
{
"role": "user",
"content": "帮我探索一个想法。"
}
],
"stream": true
}'身份验证
以 Bearer token 的形式发送 Undergrove API 密钥。密钥以 ugk_ 开头,内测期间通过邀请发放。在开启了自助密钥的地方,内测成员可在工作区设置中创建最多五个密钥,每个密钥都会消耗所属账户的配额和额度。我们只存储每个密钥的单向哈希,因此丢失的密钥无法找回,只能更换。
每个自助密钥都可以有自己的限制,可在创建时或之后在设置中配置:每日支出上限(达到后,该密钥的请求会被拒绝,直到 UTC 午夜),以及它可以使用的模型列表。设有上限的密钥仍会计入所属账户的配额。在限制功能推出之前创建的密钥没有限制,照常使用。
curl https://undergrove.vercel.app/v1/models \
-H "Authorization: Bearer $UNDERGROVE_API_KEY"端点
| 端点 | 用途 |
|---|---|
| GET /v1/models | 当前可用的模型,每个都附有类型(聊天、嵌入、转录或语音)、许可证、上下文长度、价格、推理级别、对图片、工具和网页搜索的支持情况、节奏(快速或审慎),以及运行位置。 |
| POST /v1/chat/completions | Chat, streamed or as a single response, with images, function calling and structured outputs where the model supports them. |
| POST /v1/embeddings | 由 Nomic Embed Text v1.5 生成的向量(768 维),用于搜索和相似度计算。最多 256 个输入。每 48 个输入或 67,200 个字符收费 $0.002,以较多者为准。 |
| POST /v1/audio/transcriptions | 由 Whisper Large v3 Turbo 生成的转录文本。以 multipart 上传,最大 4.4 MB,可选指定语言和提示词。每个起始兆字节收费 $0.01。 |
| POST /v1/audio/speech | 由 Qwen3-TTS 生成的 WAV 音频,共九种声音:aiden、dylan、eric、ono_anna、ryan、serena、sohee、uncle_fu 和 vivian。最多 4,096 个字符。每起始 600 个字符收费 $0.005。 |
| GET /api/status | 服务状态、用量限制和完整的模型登记表。 |
响应使用标准的 chat.completion 结构。流式响应发送 chat.completion.chunk 事件,并以 data: [DONE] 结束。推理模型会在 delta.reasoning_content 中单独流式输出推理过程,最后一个分块会报告 token 用量。如果流被中断,最后一个事件会是一个错误对象而不是 [DONE],因此不完整的回答绝不会被误认为完整的回答。
curl https://undergrove.vercel.app/v1/embeddings \
-H "Authorization: Bearer $UNDERGROVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "undergrove/nomic-embed-text",
"input": ["Where is the signed contract?", "Payment terms"]
}'参数
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
model | string | 必填 | 来自 /v1/models 的 ID。 |
messages | array | 必填 | 最多 200 条 system、user、assistant 和 tool 消息。对于支持视觉的模型,user 消息最多可包含 16 个 image_url 部分,格式为 base64 数据 URL(PNG、JPEG、GIF 或 WebP);不会抓取链接。除非模型另有报告,每张图片按 3,000 个输入 token 计算。 |
tools, tool_choice, parallel_tool_calls | array, string | object, boolean | — | 函数调用,适用于在 /v1/models 中标有工具支持的模型:最多 128 个函数。流式调用以 tool_calls 增量的形式到达,回答以 finish_reason tool_calls 结束。将结果作为 tool 消息连同其 tool_call_id 发回。 |
response_format | object | — | 结构化输出:{"type": "json_object"},或带有 name 和 schema 的 json_schema。 |
stream | boolean | false | 发送 true 即可以服务器发送事件的形式流式输出回答。 |
max_tokens | integer | 模型的上限 | 也可写作 max_completion_tokens。上限为模型的输出限制。 |
temperature, top_p | number | 由模型决定 | 采样控制:0–2 和 0–1。 |
reasoning_effort | string | 由模型决定 | 推理模型在回答前思考的时长。可接受的值因模型而异,列在 /v1/models 中;工作区的“快速”和“深入”使用列出的 fast 和 thorough 值。 |
web_search_options | object | — | 发送 {} 即可让模型进行网页搜索。查询会经过筛查以去除个人信息,结果会经过筛查以防范提示词注入。来源以 url_citation 注释的形式返回。每次搜索请求从你的配额中扣除 $0.05。 |
stop | string | string[] | — | 一个字符串,或最多四个字符串。 |
presence_penalty, frequency_penalty, seed | number, integer | — | 在模型路由支持时原样传递。 |
curl https://undergrove.vercel.app/v1/chat/completions \
-H "Authorization: Bearer $UNDERGROVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"max_tokens": 500,
"messages": [{"role": "user", "content": "Is it raining in Lisbon?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}]
}'加密模式
标准 API 由我们的网关在内存中读取,以检查每个请求。要让请求对 Undergrove 也不可读,请用你的密钥搭配 Tinfoil 的 SDK。它会验证安全飞地的硬件证明,然后加密每个请求,使只有该安全飞地能读取。我们的中继检查你的密钥和配额,并把密文转发出去。
- 你的代码Tinfoil 的 SDK 先验证安全飞地,然后将每个请求加密发送给它。可以读取请求
- 已密封至安全飞地Undergrove 中继检查密钥和配额,把密文转发出去。无法读取内容。只能看到密文
- 仍处于密封状态模型安全飞地唯一解密请求并运行模型的地方。可以读取请求
import { SecureClient } from "tinfoil"; // npm install tinfoil
const RELAY = "https://undergrove.vercel.app/api/enclave";
const client = new SecureClient({
baseURL: `${RELAY}/v1/`,
attestationBundleURL: RELAY,
});
await client.ready(); // verifies the enclave; throws if it can't
const response = await client.fetch("chat/completions", {
method: "POST",
headers: {
authorization: `Bearer ${process.env.UNDERGROVE_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
model: "gemma4-31b", // enclave_model in /v1/models
max_tokens: 1000,
messages: [{ role: "user", content: "Summarise this contract." }],
}),
});
console.log((await response.json()).choices[0].message.content);同一个客户端还可以访问 embeddings、audio/transcriptions 和 audio/speech。由于中继无法读取这些请求,模型需以 /v1/models 中的 enclave_model ID 指定,回答以 Tinfoil 的格式返回,用量则根据加密数据的大小估算。配额和额度照常适用。
MCP 服务器
Undergrove 同时也是一个 Model Context Protocol 服务器,地址为 https://undergrove.vercel.app/mcp。每个实时模型都是一个工具,例如 ask_<model>,Claude Code、Cursor 和 Claude Desktop 等 MCP 客户端可以用你的 API 密钥调用它。工具接受一个 prompt,以及可选的 system、max_tokens,对于能搜索的模型还有 web_search,并以文本形式返回模型的回答。
工具调用和其他 API 请求一样:网关在内存中读取它,通过模型公开的路由发送出去,不保留副本,并计入你密钥的限制、配额和额度。服务器使用可流式 HTTP,以 JSON 回答,不保留会话。它支持的协议版本为 2025-11-25、2025-06-18 和 2025-03-26。
Claude Code
在终端中运行一次:
claude mcp add --transport http undergrove https://undergrove.vercel.app/mcp \
--header "Authorization: Bearer ugk_your_key"Cursor
写入 ~/.cursor/mcp.json,或项目中的 .cursor/mcp.json:
{
"mcpServers": {
"undergrove": {
"url": "https://undergrove.vercel.app/mcp",
"headers": {
"Authorization": "Bearer ugk_your_key"
}
}
}
}Claude Desktop
写入 claude_desktop_config.json(Settings › Developer › Edit Config)。Claude Desktop 通过 mcp-remote 连接远程服务器,这需要 Node.js:
{
"mcpServers": {
"undergrove": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://undergrove.vercel.app/mcp",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": {
"AUTH_HEADER": "Bearer ugk_your_key"
}
}
}
}任意客户端
也可以直接调用。它通过可流式 HTTP 使用 JSON-RPC 通信:
curl https://undergrove.vercel.app/mcp \
-H "Authorization: Bearer ugk_your_key" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'然后调用某个模型的工具:
curl https://undergrove.vercel.app/mcp \
-H "Authorization: Bearer $UNDERGROVE_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "ask_<model>",
"arguments": {"prompt": "Name three uses for a sealed envelope."}
}
}'反过来,工作区也可以使用你在设置中连接的 MCP 服务器提供的工具。这些调用经由 Undergrove 的服务器以明文发送到你连接的服务器,因此请只使用你放心交给其工具相应内容的服务器。
错误与限制
错误使用 OpenAI 格式:{"error": {"message", "type", "code"}}。
| 状态 | 错误码 |
|---|---|
| 400 | invalid_request, invalid_json, unsupported_parameter, unsupported_content, context_length_exceeded, model_rejected_request, unsupported_api_version, async_needs_webhook, async_stream, invalid_webhook_url |
| 401 | authentication_required、invalid_api_key、invalid_session、sandbox_key(ugk_sandbox_ 密钥,只有本地替身服务会响应) |
| 403 | alpha_access_required、model_not_allowed_for_key(该密钥仅限使用其他模型) |
| 404 | model_not_found, webhook_not_found, thread_not_found, assistant_not_found, prompt_not_found, version_not_found |
| 409 | idempotency_in_progress(使用此 Idempotency-Key 的第一个请求仍在运行)、version_conflict(密封条目在读取后已被更改)、id_taken |
| 413 | request_too_large(聊天和转录超过 4.4 MB,嵌入超过 1 MB,语音超过 100 KB) |
| 422 | idempotency_key_reused(同一个 Idempotency-Key 用于不同的请求) |
| 429 | rate_limited、budget_exhausted、capacity_exhausted、key_budget_exceeded(该密钥已达到自身的每日上限;消息中会注明重置时间)。请按 Retry-After 标头等待,在工作区设置中添加额度,或在那里提高该密钥的上限。 |
| 502 | model_unavailable, stream_interrupted |
| 503 | capacity_exhausted(附带 Retry-After)、inference_not_configured、realtime_not_available(某条音频路由未通过证明) |
| 504 | model_timeout |
内测期间,每个密钥或账户都有每分钟请求数、每日请求数和每日支出配额。当前数值发布在 /api/status。
HTTP/2 429
retry-after: 41
{
"error": {
"message": "You’re sending messages quickly. Please wait a moment.",
"type": "rate_limit_error",
"code": "rate_limited"
}
}了解你的数据去向
- 你的服务器使用你的密钥发送请求。可以读取请求
- HTTPSUndergrove 网关检查密钥和限制,在内存中读取请求,不保留副本。可以读取请求
- 加密发送到已验证的安全飞地模型安全飞地Tinfoil 的硬件安全飞地在公开的路由上运行模型。可以读取请求
网关检查你的密钥、执行限制,并只把上述字段转发到模型路由。它不保留提示词或回答的副本,也不记录它们。其日志只记录路由名称、状态码和耗时。速率限制计数器使用你的密钥或账户的单向哈希,并在两天内过期。
每条模型路由的运营方、所在地和数据保留方式都发布在 /api/status 的模型登记表和隐私页面上。请求绝不会回退到其他路由。我们的托管方 Vercel 会保留 IP 地址等标准请求元数据。
在机密计算路由上,API 网关在建立连接前会验证安全飞地的硬件证明,并加密每个请求,使只有该安全飞地能读取。但网关本身仍会在内存中读取 API 请求以进行检查。
工作区数据
同步、共享链接和共享项目只存储浏览器生成的密文。每张图都逐步展示谁持有密钥。
验证能证明什么
在发送任何内容之前,工作区会在你的浏览器中运行 Tinfoil 的开源验证器。它对照 AMD 或 Intel 的签名密钥检查安全飞地的硬件证明报告,检查安全飞地中度量的代码是否与 Tinfoil 在 GitHub 上发布并签名的版本一致,并检查所用的加密密钥是否属于该安全飞地。在工作区中选择已验证,即可查看每一项检查、版本和度量值。
它证明你的消息只能由运行该公开代码的那台硬件解密。它不能证明公开的代码没有缺陷,不能防范处理器本身的弱点,也不涵盖回答到达你的浏览器之后你如何使用它们。工作区代码由 Undergrove 提供,因此你需要信任我们按公开版本提供验证器;验证此页面会把你的浏览器加载的代码与构建清单进行比较,npm run verify:site 则会把网站与你自己基于同一提交构建的版本进行比较。我们计划进行独立评估。
来自安全飞地的每个回答都有一个回执按钮。它会显示在发送该请求之前验证过的安全飞地、版本和度量值,以及请求加密所用的密钥,并可将它们下载为 JSON。在标签页关闭之前,下载内容包含证明报告,你以后可以再次对其运行验证器;之后则包含报告的哈希,以及获取该安全飞地当前报告的位置。回执是你浏览器的记录,而不是签名声明,其中列出的安全飞地是 Tinfoil 的模型路由器;路由器会把请求转交给模型自己的安全飞地。回执也会列出该安全飞地,你的浏览器会尝试以同样的方式验证它。大多数模型安全飞地运行在 Intel TDX 上,浏览器验证器目前还无法检查;对于这些安全飞地,回执会注明这一点,检查则依赖路由器,其公开代码会在使用每个模型安全飞地之前对其进行验证。
# The attestation bundle the gateway verified, what it checked
# in it, and the steps to check it again yourself
curl https://undergrove.vercel.app/v1/attestation可验证的响应
对于在安全飞地中运行的模型,每个 API 响应都会说明网关在发送你的请求前验证了哪个安全飞地,GET /v1/attestation 则会返回其背后的证明包,因此你可以对同样的证据运行 Tinfoil 的开源验证器。
| 标头 | 数值 |
|---|---|
x-undergrove-attestation | verified-by-gateway |
x-undergrove-enclave-host | 经过证明的安全飞地:Tinfoil 的模型路由器 |
x-undergrove-enclave-measurement | 代码度量值,与已签名的版本相匹配 |
x-undergrove-enclave-release | 版本标签(以及 -release-digest,即其公开的摘要) |
x-undergrove-enclave-key | 请求加密所用的安全飞地公钥 |
x-undergrove-attestation-sha256 | 证明报告的 SHA-256 |
x-undergrove-attestation-url | 获取证明包的位置:/v1/attestation?sha256=… |
任何客户端都可以读取它们。使用 OpenAI Python SDK 时,请求原始响应即可:
curl -si https://undergrove.vercel.app/v1/chat/completions \
-H "Authorization: Bearer $UNDERGROVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "<model-id>", "messages": [{"role": "user", "content": "Say hello."}]}' \
| grep -i '^x-undergrove-'要检查某个响应,这个 Node 脚本会发送请求,获取其标头所指明的证明包,用 @tinfoilsh/verifier 进行验证,并检查密钥、度量值、安全飞地和报告哈希是否全部一致。上方的试验场在你的浏览器中通过在此浏览器中检查做同样的事。
verify-response.mjs
// Checks the attestation evidence on an Undergrove API response yourself.
//
// npm install @tinfoilsh/verifier
// UNDERGROVE_API_KEY=ugk_… node verify-response.mjs
import { createHash } from "node:crypto";
import { Verifier } from "@tinfoilsh/verifier";
const base = process.env.UNDERGROVE_BASE_URL || "https://undergrove.vercel.app/v1";
const model = process.env.UNDERGROVE_MODEL || "undergrove/gpt-oss-120b";
// 1. Each response from an attested route names the enclave the gateway
// verified and encrypted the request to.
const response = await fetch(`${base}/chat/completions`, {
method: "POST",
headers: {
authorization: `Bearer ${process.env.UNDERGROVE_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
model,
max_tokens: 50,
messages: [{ role: "user", content: "Say hello." }],
}),
});
if (!response.ok) throw new Error(`The API answered ${response.status}.`);
const header = (name) => response.headers.get(`x-undergrove-${name}`);
const claim = {
host: header("enclave-host"),
key: header("enclave-key"),
measurement: header("enclave-measurement"),
report: header("attestation-sha256"),
};
if (!claim.key) throw new Error("This response carries no attestation evidence.");
// 2. Fetch the attestation bundle it names. Without a report hash, the
// route's current bundle (its key must still match).
const query = claim.report ? `?sha256=${claim.report}` : "";
const { bundle } = await (await fetch(`${base}/attestation${query}`)).json();
// 3. Verify the bundle with Tinfoil's open-source verifier: the chip
// maker's signature on the report, the release's signed build record
// from GitHub, and the enclave's certificate. It throws if any fails.
const verifier = new Verifier({ configRepo: "tinfoilsh/confidential-model-router" });
const attested = await verifier.verifyBundle(bundle);
const verified = verifier.getVerificationDocument();
// 4. The verified bundle must attest exactly what the response claimed.
const { format, body } = bundle.enclaveAttestationReport;
const hash = createHash("sha256").update(format + body).digest("hex");
const checks = [
["report hash", !claim.report || hash === claim.report],
["encryption key", attested.hpkePublicKey === claim.key],
["code measurement", verified.codeFingerprint === claim.measurement],
["enclave host", verified.enclaveHost === claim.host],
];
for (const [name, ok] of checks) if (!ok) throw new Error(`The ${name} doesn’t match the response.`);
console.log(
`Verified: ${verified.enclaveHost} runs ${verified.configRepo} ${verified.releaseTag || ""},` +
` and its attested key is the one this response names (${claim.key.slice(0, 16)}…).`,
);
检查通过说明什么
说明确实存在一个持有该密钥、运行该度量代码的安全飞地:真正的 AMD 或 Intel 机密计算硬件,运行着 Tinfoil 在 GitHub 上发布并签名的版本。同时说明网关声称它在转发你的请求之前验证了该安全飞地,并用其密钥加密了你的请求。
不能说明什么
不能说明网关确实用该密钥加密了你的请求。这些标头是网关的报告,而不是由安全飞地签名的声明,而且网关会在内存中读取 API 请求以进行检查。如果想依靠你自己的验证,请使用加密模式,由你的客户端自行检查安全飞地并加密每个请求。这里列出的安全飞地是 Tinfoil 的模型路由器,它会把请求转交给模型自己的安全飞地。
网站助手
只需一个 script 标签,就能在你自己的网站上放一个助手。它会添加一个按钮,点开后是聊天面板,由你选择的开放模型作答,支出不超过你设定的每日上限。面板是由 Undergrove 提供的框架,因此你的页面无法读取访客输入的内容。
- 在工作区中打开设置,在网站助手下创建一个可公开密钥:设置名称、模型、可以使用它的网站,以及每日上限。
- 复制生成的代码,粘贴到每个页面的
</body>之前。 - 先在演示页面上试试,那是一家虚构的商店。
<script
src="https://undergrove.vercel.app/embed.js"
data-key="ugp_…"
data-model="<model-id>"
data-instructions="我们是一家陶艺工作室。帮忙解答课程、营业时间和配送方面的问题。"
async
></script>| 属性 | 类型 | 默认 | 含义 |
|---|---|---|---|
data-key | string | 必填 | 你的可公开密钥,ugp_… |
data-model | string | 必填 | 该密钥所列模型之一。 |
data-title | string | “Ask a question” | 面板的标题。 |
data-greeting | string | — | 访客看到的第一条消息。 |
data-instructions | string | — | 你的网站是关于什么的、需要帮忙解答哪些问题,以及语气。随每个问题一起发送;访客可以在你的页面中看到它。 |
data-label | string | “Ask” | 按钮上的文字。 |
data-position | string | right | right 或 left。 |
data-theme | string | auto | auto(跟随访客的设备)、light 或 dark。 |
data-open | boolean | false | 设为 true 时,页面加载后即打开面板。 |
为什么可公开密钥可以放在公开页面中
- 它只能用于聊天,而且只能从 Undergrove 自己的助手框架中使用。在其他任何地方,API 都会拒绝它。
- 只有当框架嵌入在你列出的某个网站中时,它才有效。浏览器会报告是哪个页面嵌入了框架,服务器会进行检查。
- 它始终设有每日支出上限和模型列表。浏览器之外的程序可以冒充任何网站,因此请把这个密钥视为公开的,并把上限设为你愿意一天内花费的金额。
- 每位访客都有每分钟和每日的提问次数限制,发布在 /api/status。 访客按其 IP 地址的带密钥单向哈希计数,从不记录地址本身,计数在两天内过期。
和任何密钥一样,回答会消耗你账户的配额和额度。对于在安全飞地中运行的模型,框架会像工作区一样,在访客的浏览器中把每条消息加密发送到已验证的安全飞地;其他模型则经由网关访问,网关在内存中读取请求,不保留副本。你的网站两者都看不到。
“Verified by Undergrove”徽章
带有助手的网站可以显示一个小徽章,链接到 Undergrove 为该密钥提供的页面。该页面从 Undergrove 的服务器读取密钥对应的网站和模型,在访客的浏览器中验证其背后的安全飞地,并说明徽章能证明什么、不能证明什么。把这个标签放在徽章应出现的位置。
或者不用脚本,只用一张图片和一个链接(深色页面使用 badge-dark.svg):
<script src="https://undergrove.vercel.app/badge.js" data-key="ugp_…" async></script>- 徽章只是一个链接。它不会从你的页面发出任何请求,自身也不做任何检查;检查由它打开的页面完成。
- 它会写明你的可公开密钥,而这个密钥本来就为了助手放在你的页面中。徽章页面显示该密钥的网站和模型,从不显示账户、上限或已花费的金额。
- 该链接会请求浏览器发送你网站的来源,这样页面就能告诉访客他们是否来自该密钥所列的网站之一。如果徽章被复制到其他网站,页面会在那里如实说明。
- 它只为助手作担保,不涉及你网站的其他部分。
集成
Undergrove 自己的大多数客户端都会先用 Tinfoil 的验证器检查安全飞地的证明,然后把每个请求加密发送到该安全飞地,因此中继在转发时无法读取它。每个客户端都使用你的 API 密钥和 Undergrove 的模型 ID。它们尚未发布到包注册表;每个都可以从本站仓库的 packages/ 构建。
Rust、Java 和 Kotlin 客户端以及 Make 应用改用开发者 API,因为没有可供它们构建的 Tinfoil SDK:网关在内存中读取每个请求,自行验证安全飞地,并在回答的标头中报告它验证了什么。
| 包 | 适用于 | 作用 |
|---|---|---|
@undergrove/sdk | Node | OpenAI 风格的客户端,支持流式输出、工具、嵌入、转录和语音。 |
undergrove | Python | Python 客户端,并以附加组件形式提供 LangChain(ChatUndergrove)和 LlamaIndex(UndergroveLLM)支持。 |
Go | Go | 基于 Tinfoil Go 验证器的客户端,支持流式输出和工具调用。 |
Undergrove | Swift | 适用于 macOS 14 和 iOS 17 的 Swift 包,基于 Tinfoil 的 Swift 验证器。 |
@undergrove/ai-sdk-provider | Vercel AI SDK | generateText、streamText 和工具,每个回答的 provider 元数据中都附有验证过的内容。 |
@undergrove/langchain | LangChain.js | 聊天模型和嵌入。 |
@undergrove/react | React | 聊天组件和 hooks,以及一个让 API 密钥不进入浏览器的服务器路由。 |
create-undergrove-app | 入门 | npm create undergrove-app:一个私密聊天应用,两条命令即可部署到 Vercel。 |
@undergrove/agents | 智能体 | 一个工具调用循环,记录每一步由哪个已验证的安全飞地作答,形成任何人都能检查是否被篡改的记录,并支持固定安全飞地。 |
undergrove | Rust | 开发者 API 的客户端,支持流式输出和工具调用,并为每个回答附上网关的证明材料。它自身不会把请求加密发送到安全飞地。 |
dev.undergrove:undergrove | Java 和 Kotlin | 开发者 API 的客户端,支持流式输出、工具和嵌入,并为 Kotlin 提供协程和 Flow。不是加密模式:网关会在内存中读取请求。 |
undergrove[jupyter] | Jupyter | %%undergrove 单元格魔法命令,以笔记本变量和 DataFrame 作为上下文,并附带证明小组件。 |
@undergrove/office | Excel 和 Word | 在任意单元格中使用 =UNDERGROVE.ASK(),在 Word 中对所选内容进行总结、改写或提问。 |
@undergrove/office | Outlook | 总结当前打开的邮件,并起草回复,由你检查后再发送。 |
undergrove-obsidian | Obsidian | 就当前打开的笔记提问、总结、改写和续写,每个回答下方都显示已验证的安全飞地。仅限桌面版。 |
Undergrove for Alfred | Alfred | 输入 ug 和一个问题:回答以大字显示并复制到剪贴板,经由 undergrove CLI 加密。 |
n8n-nodes-undergrove | n8n | 社区节点:向模型提问、创建嵌入和列出模型,每个条目都附有验证过的内容。 |
Undergrove for Zapier | Zapier | “Ask a Model”和“Create Embeddings”操作,每个请求都从 Zap 的进程中加密发送到安全飞地。 |
Undergrove for Make | Make | 基于开发者 API 的自定义应用:网关验证安全飞地,并在每个模块的输出中报告它验证了什么。 |
Undergrove Enclave Pin | GitHub Actions | 一个步骤:在 runner 上用 Tinfoil 的验证器验证安全飞地,如果不是你固定的那个,就让任务失败。 |
使用 Vercel AI SDK:
import { streamText } from "ai";
import { undergrove } from "@undergrove/ai-sdk-provider";
const result = streamText({
model: undergrove("undergrove/gemma-4-31b"),
prompt: "Summarise this contract.",
});
for await (const text of result.textStream) process.stdout.write(text);使用 React 组件和入门模板时,消息从页面发送到你自己的服务器,由服务器进行密封;和任何服务器端集成一样,运行该服务器的人可以读取这些消息。
在 CI 中测试
用于测试这些包的替身服务会响应沙盒密钥:任何以 ugk_sandbox_ 开头的密钥。你的测试可以使用这样的密钥,无需机密也不产生费用,而 Undergrove 本身会以 sandbox_key 拒绝它们。它的回答是预先写好的,并非来自模型。在 GitHub Actions 中,用一个步骤启动它,并设置 UNDERGROVE_BASE_URL 和 UNDERGROVE_API_KEY:
- uses: ekempinski/undergrove/packages/github-action/sandbox@main
- run: npm test # UNDERGROVE_API_KEY=ugk_sandbox_ci
# And before a deploy: fail if the enclave isn’t the one you pinned.
- uses: ekempinski/undergrove/packages/github-action/pin@main
with:
measurement: 9134838da8aa4e1792566b2988820340356517c4db0c94261eb8154843e0357554ba72fcf185f950d9301fbede574c90接下来
| 已提供 | Workspace with encryption from your browser to the enclave, streaming gateway, OpenAI-compatible API with images, function calling, structured outputs, embeddings, transcription and speech, encrypted API mode, web search, documents, folders and codebases, images, audio transcripts, read-aloud, search across long documents, voice input, model register, saved conversations and export on your device. |
|---|---|
| 下一个 | 第一条模型路由、内测登录、共享用量限制。 |
| 已构建,尚未开启 | 自助密钥、付费套餐,以及用于超出每日配额部分的预付额度。 |
| 计划中 | 团队访问。 |
| 需要验证 | 对工作区、中继和网关进行独立安全评估。 |