---
title: "Rotate an inference key without resetting quota"
description: "Issue a replacement key, verify it, and revoke the old key while preserving access and quota."
---

> Documentation Index
> Fetch the complete documentation index at: https://nexus.microvoid.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Rotate an inference key without resetting quota

## Goal

无损轮换一名用户的 Inference Key。

## Outcome

新 Key 仍绑定同一 WorkspaceAccess；既有 Plan、QuotaPeriod 和历史 Usage 不复制、不归零。验证新 Key 后才撤销旧 Key。

## Prerequisites

- Access ID、旧 Key ID、Management Key。
- 既有 Gateway Base URL 和目标 Provider 接受的模型名。

## Lifecycle note

Key 只负责凭证安全生命周期。轮换不会延长 `WorkspaceAccess.accessEndsAt`，也不会改变 `QuotaPeriod.periodEnd`。

## 1. Issue, verify, and cut over

```js title="rotate-inference-key.mjs"
import { writeFile } from 'node:fs/promises';

const adminBaseUrl = required('NEXUS_ADMIN_BASE_URL').replace(/\/$/, '');
const managementKey = required('NEXUS_MANAGEMENT_KEY');
const accessId = required('NEXUS_ACCESS_ID');
const oldKeyId = required('NEXUS_OLD_KEY_ID');
const gatewayBaseUrl = required('NEXUS_GATEWAY_BASE_URL').replace(/\/$/, '');
const model = required('NEXUS_MODEL');

async function admin(path, { method = 'GET', body } = {}) {
  const response = await fetch(`${adminBaseUrl}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${managementKey}`,
      ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error(`${method} ${path} failed: ${response.status} ${await response.text()}`);
  return response.status === 204 ? undefined : response.json();
}

const before = await admin(`/admin/v1/workspace-accesses/${accessId}/quota`);
const replacement = await admin(`/admin/v1/workspace-accesses/${accessId}/api-keys`, {
  method: 'POST',
  body: { name: 'rotation-replacement', expiresInSeconds: 2592000 },
});
await writeFile('replacement-inference-key.txt', `${replacement.key}\n`, { mode: 0o600, flag: 'wx' });

const smoke = await fetch(`${gatewayBaseUrl}/chat/completions`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${replacement.key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ model, messages: [{ role: 'user', content: 'Key rotation smoke test.' }] }),
});
if (!smoke.ok) throw new Error(`replacement key smoke test failed: ${smoke.status} ${await smoke.text()}`);

await admin(`/admin/v1/workspace-accesses/${accessId}/api-keys/${oldKeyId}`, { method: 'DELETE' });
const after = await admin(`/admin/v1/workspace-accesses/${accessId}/quota`);
if (before.quotaPeriodId !== after.quotaPeriodId || before.grantedUnits !== after.grantedUnits) {
  throw new Error('key rotation unexpectedly changed the quota period');
}
console.log(JSON.stringify({ newKeyId: replacement.id, quotaPeriodId: after.quotaPeriodId }));

function required(name) {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`${name} is required`);
  return value;
}
```

## Check your work

- 新 Key 的烟雾测试成功后旧 Key 才被撤销。
- `quotaPeriodId` 和 `grantedUnits` 不变；烟雾测试本身可以正常增加 `consumedUnits`。
- `replacement-inference-key.txt` 权限为 `0600`，明文不会再次从 Admin API 读取。

## Failure modes

- 新 Key 测试失败：保留旧 Key，不执行撤销步骤。
- `workspace_access_denied`：修复 Access，而不是继续轮换 Key。
- Key 到期时间超出 Access 上限：选择更短 TTL；Key 有效期不能替代 Access 有效期。

## Security notes

切流后从部署 Secret 中移除旧 Key，再撤销服务端记录。不要同时撤销所有可用 Key，除非这是明确的紧急处置。

## Next steps

复核 [Credential model](/docs/give-users-access/credential-model) 和 [Security](/docs/security)。

Source: https://nexus.microvoid.io/cookbook/rotate-inference-key/index.mdx
