명확하게 개발하세요.
공개된 모델 경로와 명확한 데이터 처리를 갖춘 OpenAI 호환 API.
현재 상황
API는 구축과 테스트를 마쳤습니다. 첫 모델 경로가 열릴 때까지 채팅 요청은 503 inference_not_configured를 반환합니다. 실시간 상태는 언제나 https://undergrove.vercel.app/api/status에서 확인할 수 있습니다.
OpenAI API 형식을 따르므로 기본 URL만 바꾸면 기존 SDK가 작동합니다. 이미지가 포함된 채팅, 함수 호출, 구조화된 출력과 함께 임베딩, 음성 텍스트 변환, 음성 합성을 지원합니다. 게이트웨이조차 요청을 읽을 수 없게 하려면 워크스페이스처럼 암호화 모드를 사용하세요.
빠른 시작
기본 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에 있는 작은 웹 앱 세 개로, 각각 페이지 하나와 키를 보관하는 Node 서버로 구성됩니다. 키가 없으면 미리 작성된 답변을 내는 로컬 대체 서버를 대상으로 실행되며 아무것도 컴퓨터를 벗어나지 않습니다. UNDERGROVE_API_KEY를 설정하면 같은 코드가 엔클레이브를 검증하고 각 요청을 그 엔클레이브로 암호화합니다.
도움말 센터 파일을 바탕으로 답하는 채팅으로, 서버 전송 이벤트로 페이지에 스트리밍됩니다.
npm run support-botpackages/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 SDKTinfoil의 Python SDK 기반 undergrove로, 같은 기본 암호화를 제공합니다.
packages/sdk-python - CLIundergrove ask, models, verify를 제공하며, 키는 시스템 키체인에 보관됩니다. 편집기용으로 --events는 JSON 줄을 스트리밍하고, acp는 에이전트로, mcp는 도구로 실행합니다.
packages/cli - VS Code채팅 패널과 함께, 보고 있는 코드에 대해 선택 영역 설명, 이 부분 수정(수정 적용 포함), 테스트 작성을 제공합니다.
packages/vscode - JetBrains IDEsIntelliJ IDEA, PyCharm 등에서 도구 창 채팅과 같은 세 가지 작업을 제공하며, 키는 IDE의 비밀번호 보관소에 저장됩니다.
packages/jetbrains - ZedAgent Panel에서 /explain, /fix, /tests, /review를 지원하는 에이전트로, 또는 Zed 자체 에이전트의 도구로 Undergrove를 사용합니다. 두 방식 모두 내 컴퓨터에서 암호화하는 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
}'인증
Undergrove API 키를 Bearer 토큰으로 보내세요. 키는 ugk_로 시작하며, 알파 기간에는 초대를 통해 발급됩니다. 셀프 서비스 키가 켜져 있는 경우 알파 멤버는 워크스페이스 설정에서 키를 최대 5개까지 만들 수 있으며, 각 키는 계정의 일일 한도와 크레딧을 사용합니다. 저희는 각 키의 단방향 해시만 저장하므로, 잃어버린 키는 복구할 수 없고 교체만 할 수 있습니다.
각 셀프 서비스 키에는 만들 때나 나중에 설정에서 별도의 한도를 지정할 수 있습니다. 일일 지출 상한에 도달하면 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로 만든 녹취록. 최대 4.4MB의 멀티파트 업로드이며, 언어와 프롬프트는 선택 사항입니다. 시작된 메가바이트당 $0.01. |
| POST /v1/audio/speech | Qwen3-TTS가 만드는 WAV 오디오, 음성 9종: 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로 따로 스트리밍하며, 마지막 청크에서 토큰 사용량을 보고합니다. 스트림이 중단되면 마지막 이벤트는 [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 | 필수 | system, user, assistant, tool 메시지 최대 200개. 비전 모델의 경우 user 메시지에 base64 데이터 URL(PNG, JPEG, GIF 또는 WebP) 형식의 image_url 파트를 최대 16개까지 넣을 수 있으며, 링크는 가져오지 않습니다. 모델이 달리 보고하지 않는 한 이미지 하나는 입력 토큰 3,000개로 계산됩니다. |
tools, tool_choice, parallel_tool_calls | array, string | object, boolean | — | /v1/models에서 도구를 지원하는 모델용 함수 호출: 함수 최대 128개. 스트리밍된 호출은 tool_calls 델타로 도착하며, 답변은 finish_reason tool_calls로 끝납니다. 결과는 tool_call_id와 함께 tool 메시지로 돌려보내세요. |
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를 사용하세요. 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는 https://undergrove.vercel.app/mcp에 있는 Model Context Protocol 서버이기도 합니다. 각 실시간 모델은 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에 추가(설정 › 개발자 › 구성 편집). 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.4MB 초과, 임베딩은 1MB 초과, 음성 합성은 100KB 초과) |
| 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)}…).`,
);
검사를 통과하면 알 수 있는 것
그 키와 그 측정된 코드를 가진 엔클레이브가 존재했다는 것, 즉 Tinfoil이 GitHub에 게시하고 서명한 릴리스를 실행하는 진짜 AMD 또는 Intel 기밀 컴퓨팅 하드웨어였다는 것. 그리고 게이트웨이가 요청을 전달하기 전에 그 엔클레이브를 검증하고 요청을 그 키로 암호화했다고 밝히고 있다는 것입니다.
알 수 없는 것
게이트웨이가 실제로 요청을 그 키로 암호화했는지는 알 수 없습니다. 헤더는 게이트웨이의 보고이지 엔클레이브가 서명한 진술이 아니며, 게이트웨이는 API 요청을 확인하기 위해 메모리에서 읽습니다. 직접 수행한 검증에 의존하려면 클라이언트가 엔클레이브를 확인하고 각 요청을 직접 암호화하는 암호화 모드를 사용하세요. 표시된 엔클레이브는 Tinfoil의 모델 라우터로, 요청을 모델 자체의 엔클레이브로 전달합니다.
웹사이트 어시스턴트
스크립트 태그 하나로 내 웹사이트에 어시스턴트를 넣으세요. 채팅 패널을 여는 버튼이 추가되며, 직접 설정한 일일 지출 상한 안에서 선택한 오픈 모델이 답변합니다. 패널은 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 주소 자체가 아닌, 키가 적용된 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 앱은 기반으로 삼을 Tinfoil SDK가 없어 대신 개발자 API를 사용합니다. 게이트웨이가 각 요청을 메모리에서 읽고, 엔클레이브를 직접 검증하며, 검증한 내용을 답변의 헤더에 보고합니다.
| 패키지 | 대상 | 기능 |
|---|---|---|
@undergrove/sdk | Node | 스트리밍, 도구, 임베딩, 음성 텍스트 변환, 음성 합성을 지원하는 OpenAI 방식 클라이언트. |
undergrove | Python | Python 클라이언트로, LangChain(ChatUndergrove)과 LlamaIndex(UndergroveLLM)를 추가 기능으로 제공합니다. |
Go | Go | Tinfoil의 Go 검증기 기반 클라이언트로, 스트리밍과 도구 호출을 지원합니다. |
Undergrove | Swift | Tinfoil의 Swift 검증기 기반의 macOS 14 및 iOS 17용 Swift 패키지. |
@undergrove/ai-sdk-provider | Vercel AI SDK | generateText, streamText, 도구를 지원하며, 검증된 내용을 각 답변의 제공자 메타데이터에 담습니다. |
@undergrove/langchain | LangChain.js | 채팅 모델과 임베딩. |
@undergrove/react | React | 채팅 구성 요소와 훅, 그리고 API 키를 브라우저에서 분리해 두는 서버 경로. |
create-undergrove-app | 스타터 | npm create undergrove-app: 명령 두 개로 Vercel에 배포하는 비공개 채팅 앱. |
@undergrove/agents | 에이전트 | 각 단계에 어떤 검증된 엔클레이브가 답변했는지 기록하는 도구 호출 루프로, 누구나 수정 여부를 확인할 수 있는 기록과 엔클레이브 고정을 제공합니다. |
undergrove | Rust | 스트리밍과 도구 호출을 지원하고 각 답변에 대한 게이트웨이의 증명 자료를 제공하는 개발자 API 클라이언트. 요청을 직접 엔클레이브로 암호화하지는 않습니다. |
dev.undergrove:undergrove | Java 및 Kotlin | 스트리밍, 도구, 임베딩을 지원하고 Kotlin용 코루틴과 Flow를 제공하는 개발자 API 클라이언트. 암호화 모드가 아니며, 게이트웨이가 요청을 메모리에서 읽습니다. |
undergrove[jupyter] | Jupyter | 노트북 변수와 DataFrame을 컨텍스트로 사용하는 %%undergrove 셀 매직과 증명 위젯. |
@undergrove/office | Excel 및 Word | 모든 셀에서 =UNDERGROVE.ASK()를 사용하고, Word에서는 선택 영역을 요약하거나 다시 쓰거나 질문할 수 있습니다. |
@undergrove/office | Outlook | 열려 있는 메시지의 요약과, 보내기 전에 확인하는 답장 초안. |
undergrove-obsidian | Obsidian | 열려 있는 노트에 대해 질문하고, 요약하고, 다시 쓰고, 이어서 쓸 수 있으며, 각 답변 아래에 검증된 엔클레이브가 표시됩니다. 데스크톱 전용. |
Undergrove for Alfred | Alfred | ug와 질문을 입력하면 답변이 Large Type으로 표시되고 클립보드에도 복사됩니다. undergrove CLI를 통해 암호화됩니다. |
n8n-nodes-undergrove | n8n | 커뮤니티 노드: 모델에 질문, 임베딩 생성, 모델 목록 보기를 지원하며, 각 항목에 검증된 내용을 담습니다. |
Undergrove for Zapier | Zapier | 모델에 질문하기와 임베딩 만들기 액션을 제공하며, 각 요청은 Zap의 프로세스에서 엔클레이브로 암호화됩니다. |
Undergrove for Make | Make | 개발자 API 기반의 커스텀 앱: 게이트웨이가 엔클레이브를 검증하고, 검증한 내용을 각 모듈의 출력에 보고합니다. |
Undergrove Enclave Pin | GitHub Actions | 러너에서 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. |
|---|---|
| 다음 | 첫 모델 경로, 비공개 알파 로그인, 공유 사용 한도. |
| 구축 완료, 아직 활성화 전 | 셀프 서비스 키, 유료 요금제, 일일 한도를 넘는 사용을 위한 선불 크레딧. |
| 계획됨 | 팀 액세스. |
| 검증 필요 | 워크스페이스, 릴레이, 게이트웨이에 대한 독립적인 보안 평가. |