わかりやすく、作る。
モデルのルートを公開し、データの扱いが明快な、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
ここまでの回答
1行で OpenAI から切り替え
Undergrove は OpenAI の API に対応しているため、公式 SDK をそのまま使えます。クライアントを作る1行で Undergrove のベースURLとキーを渡し、ここで提供しているオープンモデルを1つ指定するだけです。
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 にある3つの小さなウェブアプリで、それぞれページと、キーを持つ Node サーバーで構成されています。キーがなければローカルの代替環境に対して台本どおりの回答で動き、何もあなたのコンピューターの外に出ません。UNDERGROVE_API_KEY を設定すれば、同じコードがエンクレーブを検証し、各リクエストをそこに向けて暗号化します。
ヘルプセンターのファイルをもとに答えるチャット。Server-Sent Events でページにストリーミングします。
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 SDKundergrove:Tinfoil の Python SDK をもとにし、同じくデフォルトで暗号化します。
packages/sdk-python - CLIundergrove ask、models、verify。キーはシステムのキーチェーンに保管します。エディター向けに、--events は JSON Lines をストリーミングし、acp はエージェントとして、mcp はツールとして動かします。
packages/cli - VS Codeチャットパネルと、手元のコードに対する「選択範囲を説明」「修正」(「修正を適用」付き)「テストを書く」。
packages/vscode - JetBrains IDEsIntelliJ IDEA、PyCharm などで使えるツールウィンドウのチャットと、同じ3つのアクション。キーは IDE のパスワードセーフに保管します。
packages/jetbrains - ZedAgent Panel のエージェントとして /explain、/fix、/tests、/review を使えるほか、Zed 自身のエージェントのツールとしても使えます。どちらも CLI を実行し、あなたのマシン上で暗号化します。
packages/zed - Neovim:UndergroveAsk と、ビジュアル選択に対する :UndergroveExplain。回答はフローティングウィンドウにストリーミングされます。
packages/neovim - Raycastどのアプリでも使える「Undergrove に質問」と「選択したテキストについて質問」。
packages/raycast - GitHub ActionUndergrove Review:プルリクエストごとに1件のレビューを、行コメントと提案付きで投稿し、フッターにアテステーションを記載します。
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つまで作成でき、各キーはそのアカウントの利用枠とクレジットを消費します。保存するのは各キーの一方向ハッシュだけなので、紛失したキーは復元できず、作り直すことになります。
セルフサービスのキーには、作成時またはあとから設定で、個別の上限を付けられます。1日の支出上限(超えると UTC の午前0時までリクエストを拒否)と、使えるモデルの一覧です。上限付きのキーも、アカウントの利用枠に計上されます。上限の機能ができる前に作ったキーには上限がなく、これまでどおり使えます。
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.4 MBのマルチパートアップロードで、言語とプロンプトは任意。1メガバイトごと(端数切り上げ)に $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 の data URL(PNG、JPEG、GIF、WebP)として image_url を最大16個含められます。リンクは取得しません。画像1枚は、モデルが別の値を報告しない限り、入力3,000トークンとして数えます。 |
tools, tool_choice, parallel_tool_calls | array, string | object, boolean | — | /v1/models で tools が有効なモデルの関数呼び出し:最大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 を送ると、回答を Server-Sent Events でストリーミングします。 |
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 のアノテーションとして届きます。検索リクエストは1件あたり利用枠から $0.05 です。 |
stop | string | string[] | — | 文字列1つ、または最大4つの文字列。 |
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 は Model Context Protocol のサーバーでもあり、https://undergrove.vercel.app/mcp で提供しています。各ライブモデルは ask_<model> のようなツールで、Claude Code、Cursor、Claude Desktop などの MCP クライアントから API キーで呼び出せます。ツールは prompt と、任意で system、max_tokens、検索できるモデルでは web_search を受け取り、モデルの回答をテキストで返します。
ツールの呼び出しは、ほかのAPIリクエストと同じです。ゲートウェイはメモリ上でそれを読み、モデルの公開ルートで送り、コピーは保持せず、キーの上限、利用枠、クレジットから差し引きます。サーバーは Streamable 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"
}
}
}
}その他のクライアント
または直接呼び出せます。Streamable 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(キーが自身の1日の上限に達した場合。メッセージにリセット時刻が示されます)。Retry-After ヘッダーの時間まで待つか、ワークスペースの設定でクレジットを追加するか、そこでキーの上限を引き上げてください。 |
| 502 | model_unavailable, stream_interrupted |
| 503 | capacity_exhausted(Retry-After 付き)、inference_not_configured、realtime_not_available(音声の経路が証明されていない) |
| 504 | model_timeout |
アルファ期間中、各キーまたはアカウントには、1分あたり、1日のリクエスト数、1日の支出の利用枠があります。現在の値は /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 のハードウェア・エンクレーブが、公開されたルートでモデルを実行します。リクエストを読める
ゲートウェイはキーを確認し、上限を適用して、上記のフィールドだけをモデルのルートに転送します。プロンプトや回答のコピーは保持せず、記録もしません。ログに残るのは、ルート名、ステータスコード、所要時間だけです。レート制限のカウンターはキーまたはアカウントの一方向ハッシュを使い、2日以内に失効します。
各モデルのルートの運営者、所在地、保持期間は、/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 のモデルルーターで、リクエストをモデル自身のエンクレーブに渡します。
ウェブサイトアシスタント
script タグ1つで、自分のウェブサイトにアシスタントを置けます。チャットパネルを開くボタンが追加され、選んだオープンモデルが、設定した1日の支出上限の範囲で答えます。パネルは Undergrove が配信するフレームなので、あなたのページから訪問者の入力内容を読むことはできません。
- ワークスペースで設定を開き、ウェブサイトアシスタントで公開可能キーを作成します。名前、モデル、利用を許可するウェブサイト、1日の上限を指定します。
- 表示されたコードをコピーし、各ページの
</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 | 必須 | キーに登録されたモデルの1つ。 |
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 が拒否します。
- フレームが、指定したウェブサイトのいずれかに埋め込まれているときだけ使えます。どのページがフレームを埋め込んでいるかをブラウザが伝え、サーバーが確認します。
- 必ず1日の支出上限とモデルの一覧が設定されます。ブラウザ外のプログラムはどのウェブサイトにもなりすませるため、キーは公開されたものとして扱い、1日に使ってもよい金額を上限に設定してください。
- 訪問者ごとに1分あたりと1日あたりの質問数の上限があり、/api/status で公開しています。 訪問者は、IPアドレスそのものではなく、鍵付きの一方向ハッシュで数え、カウントは2日以内に失効します。
回答は、ほかのキーと同じく、アカウントの利用枠とクレジットを消費します。エンクレーブで動くモデルでは、ワークスペースと同じように、フレームが訪問者のブラウザで各メッセージを検証済みのエンクレーブに向けて暗号化します。ほかのモデルにはゲートウェイ経由で届き、ゲートウェイはメモリ上でリクエストを読みますが、コピーは保持しません。あなたのウェブサイトからはどちらも見えません。
「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 | macOS 14 と iOS 17 向けの Swift パッケージ。Tinfoil の 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:2つのコマンドで 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 | ランナー上で 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 では、1 つのステップで起動し、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. |
|---|---|
| 次へ | 最初のモデルのルート、プライベートアルファのサインイン、共通の利用の上限。 |
| 構築済み、未有効化 | セルフサービスのキー、有料プラン、1日の利用枠を超えた分のプリペイドクレジット。 |
| 予定 | チームでのアクセス。 |
| 検証が必要 | ワークスペース、リレー、ゲートウェイの独立したセキュリティ評価。 |