> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sunra.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 预算子密钥

预算子密钥是从普通 Sunra API key 创建的短期子密钥，带有 USD 支出上限和独立的 call receipt。为每个 agent turn 或 run 创建一把预算子密钥，可以限制 agent 的 LLM 访问，并分别核算每位终端用户的用量。该上限通过估算成本控制请求准入：实际结算支出可能超过上限，因此请在您自己的 authorization hold 中预留余量。Sunra 仍向父密钥所属的 organization 计费；预算子密钥提供额度限制和用量归属，并不是 wallet。

## 生命周期概览

1. **Mint**：使用普通密钥创建预算子密钥。保存子密钥 `id`、业务 `ref` 和仅返回一次的 secret。
2. **调用**：在到期前，使用子密钥 secret 发起一次或多次 LLM 调用，显式指定输出上限并设置 call tag。
3. **查看**：使用普通密钥读取实时支出和 call receipt。
4. **关闭**：turn 结束后关闭子密钥。轮询直到 `status: closed`，再根据最终 `spent_usd` 结算用户的 authorization hold。

所有管理请求均使用 `https://api.sunra.ai` 和 `Authorization: Key $SUNRA_KEY`，其中 `SUNRA_KEY` 必须是有效的普通密钥。预算子密钥不能调用管理端点。参见[鉴权](/zh-Hans/platform/authentication)。

## 创建预算子密钥

向 `POST /v1/budget-keys` 发送 JSON body：

| 字段                      | 必填 | 规则                                                                                                                                     |
| ----------------------- | -- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cap_usd`               | 是  | 正数十进制 USD **字符串**，匹配 `^\d+(?:\.\d{1,8})?$`。范围为 `0.00000001`–`90071992.54740991`，含边界。拒绝 JSON number、零、负数、指数表示法和超过 8 位小数的值。返回值固定为 8 位小数。 |
| `expires_at`            | 是  | 带 `Z` 或 timezone offset 的 ISO datetime 字符串。校验时必须在未来，且距离当前时间最多 24 小时。请为请求处理留出时间。                                                        |
| `ref`                   | 否  | 最多 256 个字符的字符串，用于关联您的 turn 或授权记录。如果需要通过 `ref` 找回密钥，请使用非空值。它不是 idempotency key。                                                         |
| `metadata`              | 否  | JSON object，序列化后的 UTF-8 JSON 不超过 8,192 bytes。                                                                                          |
| `metadata.allowed_tags` | 否  | 最多 32 个字符串组成的 array，每项最多 128 个 printable ASCII 字符。设置后，每次调用都必须提供集合内的 tag。空 array 会拒绝所有调用。                                               |
| `models`                | 否  | 包含 1–64 个精确、小写的公共 `owner/model` ID 的 array，必须在父密钥的 allowlist 内；不支持 wildcard。每一部分长 1–64 个字符，首尾必须是小写字母或数字，中间可包含 `.`, `_` 或 `-`。          |

可选字段请直接省略，不要发送 `null`。未知的顶层字段会被拒绝。省略 `models` 时，子密钥继承父密钥的模型限制；设置后，每次调用都必须同时满足该列表和父密钥当前的 allowlist。

以下示例使用 `jq` 将到期时间设为一小时后，并创建上限为 \$0.05 的密钥：

```bash theme={null}
EXPIRES_AT=$(jq -nr 'now + 3600 | strftime("%Y-%m-%dT%H:%M:%SZ")')
curl --fail-with-body -sS https://api.sunra.ai/v1/budget-keys \
  -H "Authorization: Key $SUNRA_KEY" \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --arg expires "$EXPIRES_AT" '{
    cap_usd: "0.05",
    expires_at: $expires,
    ref: "turn-123",
    metadata: {allowed_tags: ["summary"]},
    models: ["google/gemini-2.5-flash"]
  }')"
```

`201` response 直接返回资源，不包含 `data` 包装层。示例值如下：

```json theme={null}
{
  "object": "budget_key",
  "id": "child_example",
  "parent_api_key_id": "parent_example",
  "status": "open",
  "cap_usd": "0.05000000",
  "expires_at": "2026-09-11T13:00:00.000Z",
  "ref": "turn-123",
  "metadata": {"allowed_tags": ["summary"]},
  "models": ["google/gemini-2.5-flash"],
  "spent_usd": "0.00000000",
  "calls": 0,
  "reserved_usd": "0.00000000",
  "in_flight": 0,
  "closed_at": null,
  "secret_key": "replace-with-the-returned-child-secret"
}
```

**请立即保存 `secret_key`：mint 仅返回一次。** GET、list、receipt 和 close 均不会返回它。**Mint 不具备幂等性**：重复请求即使使用相同的 `ref`，也会创建不同的子密钥。如果 response 丢失，请先使用下文的恢复端点找到并关闭遗留密钥，再决定是否重新 mint。

## 使用子密钥调用 LLM API

使用 `https://api-llm.sunra.ai` 和 `Authorization: Bearer <child secret>`。仅支持以下端点：

| 端点                          | 支持的调用                                                            |
| --------------------------- | ---------------------------------------------------------------- |
| `POST /v1/chat/completions` | Streaming 和 non-streaming [Chat Completions](/zh-Hans/llm/chat)。 |
| `POST /v1/messages`         | Streaming 和 non-streaming [Messages](/zh-Hans/llm/messages)。     |
| `POST /v1/responses`        | Streaming 和 non-streaming [Responses](/zh-Hans/llm/responses)。   |
| `POST /v1/embeddings`       | 仅支持 non-streaming [text embeddings](/zh-Hans/llm/embeddings)。    |

其他 API 操作，包括预算子密钥管理和 native/media embeddings，均以 `403 budget_key_route_forbidden` 拒绝子密钥。已过期或已撤销的凭证可能先在鉴权阶段失败；无法识别的 URL 仍可能返回 `404`。

使用 Chat Completions 时，**每次子密钥请求都必须发送 `max_tokens` 或 `max_completion_tokens`**，并选择模型支持的字段。两者都缺失时返回 `400 max_tokens_required`。值必须是正的 safe integer；如果同时提供两个字段，它们必须相等。Messages 使用 `max_tokens`，Responses 使用 `max_output_tokens`。后两种 API 可以根据模型配置的输出上限补充缺失值；显式发送上限可以明确本次调用的输出额度。

`n` 可以省略，但提供时必须为数字 `1`。`best_of` 即使为 `1` 也会被拒绝。

子密钥请求只能携带下列 portable 生成参数，以及所调用端点对应的输出上限字段。其他字段——provider extension bag 和其他写法的 ceiling，例如 `extra_body`、`generation_config`、`generationConfig`、`max_new_tokens`——一律返回 `400 invalid_input`：它们可能在转换后放大真实输出上限，使 reservation 失效。允许的顶层字段为：`model`, `provider`, `stream`, `stream_options`, `messages`, `input`, `system`, `instructions`, `tools`, `tool_choice`, `parallel_tool_calls`, `response_format`, `text`, `temperature`, `top_p`, `top_k`, `stop`, `stop_sequences`, `seed`, `presence_penalty`, `frequency_penalty`, `logit_bias`, `logprobs`, `top_logprobs`, `user`, `metadata`, `store`, `previous_response_id`, `include`, `truncation`, `reasoning`, `reasoning_effort`, `thinking`, `service_tier`, `safety_identifier`, `prompt_cache_key`, `prompt_cache_retention`, `verbosity`, `n`, `encoding_format`, `dimensions`。使用普通密钥发起的请求不受此限制。

使用 `x-sunra-call-tag` 标记调用，值最多为 128 个 printable ASCII 字符。它会作为 `tag` 出现在 receipt 中。如果密钥设置了 `metadata.allowed_tags`，缺失 tag 或 tag 不在集合内会返回 `403 budget_call_tag_forbidden`；否则该 header 为可选。普通密钥完全忽略该 header。

将返回的 secret 赋给 `CHILD`。`-i` 会显示 response header：

```bash theme={null}
curl --fail-with-body -sS -i https://api-llm.sunra.ai/v1/chat/completions \
  -H "Authorization: Bearer $CHILD" \
  -H 'Content-Type: application/json' \
  -H 'x-sunra-call-tag: summary' \
  -d '{
    "model": "google/gemini-2.5-flash",
    "messages": [{"role": "user", "content": "Say hello in one sentence."}],
    "max_tokens": 128,
    "n": 1
  }'

# Example response header (alongside the JSON response body):
# x-sunra-prediction-id: chatcmpl_example
```

保存 `x-sunra-prediction-id`，用于将本次调用关联到对应的 receipt。Receipt 的 `id` 与该 header 的值逐字节一致；不要用 provider response ID 替代它。

## 跟踪支出

### 读取密钥

将 mint 返回的子密钥 `id` 赋给 `ID`：

```bash theme={null}
curl --fail-with-body -sS "https://api.sunra.ai/v1/budget-keys/$ID" \
  -H "Authorization: Key $SUNRA_KEY"
```

Response 包含与 mint 相同的资源字段，但不含 `secret_key`：

| 字段             | 含义                                                                                 |
| -------------- | ---------------------------------------------------------------------------------- |
| `spent_usd`    | Open 或 finalizing 时为实时支出；closed 后为权威且不可变的最终支出。值为 8 位小数的 USD 字符串；实时值不可用时可能为 `null`。 |
| `reserved_usd` | 为在途调用预留的估算成本；未知时为 `null`。Closed 密钥返回 `0.00000000`。                                 |
| `in_flight`    | 仍有未释放 reservation 的调用数；未知时为 `null`。Closed 密钥返回 `0`。                                |
| `calls`        | 调用尝试数，包含未付费的尝试。Closed 前为临时值，不可用时可能为 `null`；对账请使用 closed 后的计数。                      |
| `status`       | `open`、`finalizing` 或 `closed`。                                                    |
| `closed_at`    | Closed 前为 `null`，之后为最终 snapshot 的时间。                                               |

准入会在调用前预留**估算售价**。实际结算成本可能因在途调用的估算误差而超过上限。请在您自己的 authorization hold 中留出余量；不要根据 open 密钥的支出或 LLM response 已结束就释放 hold。未知的实时值不等于零。

### 读取逐调用 receipt

```bash theme={null}
curl --fail-with-body -sS --get \
  "https://api.sunra.ai/v1/budget-keys/$ID/calls" \
  -H "Authorization: Key $SUNRA_KEY" \
  --data-urlencode 'limit=100'
```

`limit` 默认为 100，允许 1–500。只要 `has_more` 为 true，就将 `next_cursor` 作为 `after` 请求下一页；该值是本页最后一行的 `id`。请原样传回并进行 URL encoding。无效 cursor 或属于其他子密钥的 cursor 会返回 `400 invalid_input`。

以下为包含一次已付费调用的密钥在关闭后的示例页面：

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "id": "chatcmpl_example",
      "model": "google/gemini-2.5-flash",
      "status": "PAID",
      "cost_usd": "0.00012000",
      "created_at": "2026-09-11T12:00:00.000Z",
      "completed_at": "2026-09-11T12:00:01.000Z",
      "usage": {"input_tokens": 10, "output_tokens": 20},
      "tag": "summary"
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "receipt_version": 1,
  "calls_count": 1,
  "complete": true
}
```

Receipt 包含 `PAID`、`VOID`、`PENDING` 和 `UNCOLLECTIBLE` 尝试。只有 `PAID` 行具有结算成本；其他状态的 `cost_usd` 均为 `"0.00000000"`。在创建调用记录之前被拒绝的请求不计入。`completed_at` 和 `tag` 可以为 `null`；`usage` 可包含 cache token 计数，`wire_id` 为可选字段。

`calls_count` 是 receipt 所有分页中的尝试总数，不只是当前页的行数。**只有 closed 密钥的 `complete` 才为 true**；open 和 finalizing 状态的 receipt 仍可能变化。最终对账时，请先关闭密钥，从第一页重新读取全部 receipt，并使用 decimal arithmetic 对 `status: "PAID"` 行的 `cost_usd` 求和。该总和等于 closed 后的 `spent_usd`，总行数等于 closed 后的 `calls` 和 receipt 的 `calls_count`。仅有 `has_more: false` 并不表示密钥已关闭。

## 关闭密钥

```bash theme={null}
curl --fail-with-body -sS -i -X POST \
  "https://api.sunra.ai/v1/budget-keys/$ID/close" \
  -H "Authorization: Key $SUNRA_KEY"
```

Close 不需要 body，并会阻止新调用。使用已关闭的子密钥调用会返回 `401 budget_key_revoked`。Close 不会取消已经准入的调用，它们仍可能继续结算。

| Response                       | 后续操作                                                                           |
| ------------------------------ | ------------------------------------------------------------------------------ |
| `200` 且 `status: "closed"`     | 使用 `spent_usd` 作为最终金额。Snapshot 此后不会再变化。                                        |
| `202` 且 `status: "finalizing"` | 仍有在途调用，或 finalization 尚未完成。保留 authorization hold，继续轮询 close，直到返回 `200 closed`。 |
| `503 budget_store_unavailable` | 尚未确认关闭成功。保留 hold，并使用 backoff 重试。                                               |

Close 具有幂等性。密钥关闭后，重复请求返回相同的最终 snapshot；没有调用的密钥也会返回支出为零的 snapshot。也可以轮询 GET by ID，但即使 `status` 为 `finalizing`，GET 也返回 HTTP `200`：请始终检查 body。不保证 finalization 在固定时间内完成。

## 端到端示例

使用 Bash、支持 `--fail-with-body` 的 `curl` 和 `jq` 运行以下示例。按照[鉴权](/zh-Hans/platform/authentication)中的说明将普通密钥赋给 `SUNRA_KEY`。示例使用 `google/gemini-2.5-flash`，父密钥必须允许该模型。它会创建一把上限为 \$0.05、有效期为一小时的密钥，发起一次调用、读取 receipt、关闭密钥并打印最终支出。Close 前读取的 receipt 仍可能处于 pending 状态。

如果调用或 receipt 读取失败，exit handler 也会尝试关闭密钥。请在应用中保存打印出的 `ref` 和子密钥 ID，以便在执行中断后恢复处理。

```bash theme={null}
set -euo pipefail
: "${SUNRA_KEY:?Set SUNRA_KEY to an active ordinary Sunra API key}"
WORK_DIR=$(mktemp -d)
ID=''

close_key() {
  while true; do
    if ! HTTP_STATUS=$(curl -sS -o "$WORK_DIR/close.json" -w '%{http_code}' \
      -X POST "https://api.sunra.ai/v1/budget-keys/$ID/close" \
      -H "Authorization: Key $SUNRA_KEY"); then
      sleep 2
      continue
    fi
    case "$HTTP_STATUS" in
      200)
        jq -e '.status == "closed"' "$WORK_DIR/close.json" >/dev/null || return 1
        jq -r '"Final spend (USD): " + .spent_usd' "$WORK_DIR/close.json"
        return 0
        ;;
      202) sleep 2 ;;
      503)
        jq -e '.error.details.retryable == true' "$WORK_DIR/close.json" >/dev/null || return 1
        sleep 2
        ;;
      *) cat "$WORK_DIR/close.json" >&2; return 1 ;;
    esac
  done
}

cleanup() {
  RESULT=$?
  trap - EXIT
  if [[ -n "$ID" ]]; then
    close_key || RESULT=1
  fi
  rm -rf "$WORK_DIR"
  exit "$RESULT"
}
trap cleanup EXIT

REF="turn-$(jq -nr 'now * 1000000 | floor')"
printf 'Recovery ref: %s\n' "$REF"
EXPIRES_AT=$(jq -nr 'now + 3600 | strftime("%Y-%m-%dT%H:%M:%SZ")')
MINT=$(curl --fail-with-body -sS https://api.sunra.ai/v1/budget-keys \
  -H "Authorization: Key $SUNRA_KEY" \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --arg expires "$EXPIRES_AT" --arg ref "$REF" '{
    cap_usd: "0.05",
    expires_at: $expires,
    ref: $ref,
    metadata: {allowed_tags: ["summary"]},
    models: ["google/gemini-2.5-flash"]
  }')")
ID=$(jq -er '.id' <<<"$MINT")
printf 'Child ID: %s\n' "$ID"
CHILD=$(jq -er '.secret_key' <<<"$MINT")
unset MINT

curl --fail-with-body -sS -D "$WORK_DIR/chat.headers" \
  -o "$WORK_DIR/chat.json" https://api-llm.sunra.ai/v1/chat/completions \
  -H "Authorization: Bearer $CHILD" \
  -H 'Content-Type: application/json' \
  -H 'x-sunra-call-tag: summary' \
  -d '{
    "model": "google/gemini-2.5-flash",
    "messages": [{"role": "user", "content": "Say hello in one sentence."}],
    "max_tokens": 128,
    "n": 1
  }'
CALL_ID=$(awk 'tolower($1) == "x-sunra-prediction-id:" {gsub("\r", "", $2); print $2}' "$WORK_DIR/chat.headers")
printf 'x-sunra-prediction-id: %s\n' "$CALL_ID"

curl --fail-with-body -sS "https://api.sunra.ai/v1/budget-keys/$ID/calls?limit=100" \
  -H "Authorization: Key $SUNRA_KEY" \
  | jq --arg id "$CALL_ID" '.data[] | select(.id == $id)'

close_key
ID=''
unset CHILD
```

Mint 结果不明确时，不要自动重试 mint。应用需要最终的逐调用账目时，请按上文说明，在 `closed` 后重新读取全部 receipt 分页。重复发送 LLM 请求不会被去重，可能再次产生费用。

## 找回丢失的 ticket

如果 mint response 或本地 ticket 记录丢失，可使用已保存的 `ref` 找回子密钥 ID：

```bash theme={null}
curl --fail-with-body -sS --get https://api.sunra.ai/v1/budget-keys \
  -H "Authorization: Key $SUNRA_KEY" \
  --data-urlencode 'ref=turn-123' \
  --data-urlencode 'limit=100'
```

Response 形如 `{ "data": [...], "has_more": false, "next_cursor": null }`，`data` 中包含预算子密钥资源，不含 secret。`ref` 必填、非空且最多 256 个字符。`limit` 默认为 100，允许 1–100。如果 `has_more` 为 true，将不透明的 `next_cursor` 进行 URL encoding 后作为 `after` 请求下一页。多个密钥可以共用同一个 `ref`；请检查所有匹配项并关闭遗留密钥。

List 用于找回密钥。请使用 GET by ID 或 close 获取最终账目状态，不要根据 open 状态的 list 结果结算 authorization hold。Secret 无法找回。

**同一 organization 内任何有效的普通密钥都可以对预算子密钥执行 get、list、close 和读取 calls。** 不必使用最初 mint 它的密钥。在轮换或撤销父密钥后，请使用该 organization 内另一把有效的普通密钥完成未结 ticket。撤销原父密钥会使新的子密钥调用返回 `401 budget_key_revoked`，但不会自动完成子密钥的最终账目处理。

## 错误

请同时检查 HTTP status 和 `error.details.reason`；下表说明预算相关错误的处理方式。已有的 model-access、parameter-validation、organization-balance 和 upstream 错误可能保留原有的 response 格式。

| HTTP | `reason`                     | 后续操作                                                                                         |
| ---- | ---------------------------- | -------------------------------------------------------------------------------------------- |
| 400  | `max_tokens_required`        | 提供对应的输出上限。Chat 要求 `max_tokens` 或 `max_completion_tokens`。                                    |
| 400  | `invalid_input`              | 修正字段类型、限制值、模型限制、tag 格式、cursor 或不支持的输出参数后再重试。                                                 |
| 401  | `budget_key_expired`         | 停止使用该子密钥，并 close 以获取最终账目。只有获得新的授权后才 mint 新密钥。                                                |
| 401  | `budget_key_revoked`         | 停止使用该子密钥。使用同一 organization 内有效的普通密钥 close 或查询它。                                              |
| 402  | `budget_exhausted`           | 即使没有其他 reservation，本次调用的估算也超过剩余预算。降低请求的估算成本或获取新授权；原样重试无效。                                    |
| 402  | `budget_reserved`            | 可重试。其他调用暂时预留了所需预算。等待它们结算后，使用 backoff 重试。                                                     |
| 402  | `budget_state_unknown`       | 停止使用该子密钥，close 并等待最终账目。不要根据缺失值推断可用预算。后续工作需要新授权的密钥。                                           |
| 403  | `budget_key_route_forbidden` | 管理操作使用普通密钥；子密钥仅用于受支持的 LLM 路由。                                                                |
| 403  | `budget_call_tag_forbidden`  | 发送 `x-sunra-call-tag`，其值必须在密钥的 `metadata.allowed_tags` 内。                                    |
| 404  | `budget_key_not_found`       | 检查子密钥 ID 和用于管理的普通密钥所属的 organization。                                                         |
| 503  | `budget_store_unavailable`   | 可重试。使用 backoff 重试，并保留 authorization hold 直到 closed。Mint 失败且结果不明确时，先通过 `ref` 恢复，再决定是否重新 mint。 |

`402 budget_reserved` body 示例：

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "INSUFFICIENT_CREDIT",
    "message": "budget_reserved",
    "details": {
      "code": "INSUFFICIENT_CREDIT",
      "message": "budget_reserved",
      "reason": "budget_reserved",
      "retryable": true,
      "spent_usd": "0.00000000",
      "reserved_usd": "0.04000000",
      "cap_usd": "0.05000000",
      "estimate_usd": "0.02000000"
    }
  }
}
```

`budget_exhausted` 和 `budget_reserved` 包含准入决策时的全部四个 USD 字符串。对于这些金额字段，`budget_state_unknown` 只返回 `cap_usd`；不能将缺失的 spend 和 reservation 值视为零。

## 限制与保证

* **有效期：** 从 mint 校验时起最多 24 小时。到期后停止新调用；最终账目仍需获取 closed snapshot。
* **Best-effort cap：** 准入预留的是估算售价。实际支出可能因在途调用的估算误差而超过 `cap_usd`。请在自己的 authorization hold 中预留余量。
* **实时值与最终值：** Open 和 finalizing 状态的数值可能变化。只有 `status: closed` 提供权威且不可变的 snapshot；其 receipt 标记为 `complete: true`。Snapshot 覆盖已记录的结算，因此从未记录或在 snapshot 之后才到达的结算可能不包含在最终金额中。
* **计费：** 父密钥所属的 organization 仍按原有方式计费，并继续受现有余额和访问限制约束。预算子密钥不会为 wallet 注资，也不会将终端用户的资金转入 Sunra；终端用户的授权和账目由您的应用管理。
