Skip to content

Commit 2be4813

Browse files
authored
feat: multi-user API keys with per-key usage metering (#22)
* feat: multi-user API keys with per-key usage metering and /admin/keys management - API_KEYS (config.json array of {name, key}) / OPENCODE_API_KEYS env (comma-separated name:key, bare keys auto-named key-N); duplicate names or keys fail at startup - Auth: any API_KEYS hit tags req.apiKeyName; requests otherwise fall back to the legacy API_KEY, so single-key deployments are unchanged - In-memory per-key usage registry: requests (including failures), prompt/completion/total tokens for successful business requests on /v1/chat/completions and /v1/responses (chars/4 estimation, reasoning included in completion), lastUsedAt/createdAt - Admin endpoints GET/POST/DELETE /admin/keys, mounted only when API_KEYS is configured; admin auth = Bearer API_KEY (user keys 401, no PUBLIC_DIAGNOSTICS bypass, 501 when API_KEY empty); keyHint shows only the last 4 characters - /health/details gains usage {byKey, total} and /metrics emits opencode_proxy_key_requests_total / opencode_proxy_key_tokens_total in multi-user mode only; 501 admin behavior covered by tests - 35 new tests in tests/unit/multiuser.test.js (192 total) * fix(test): mock http in multiuser suites so ensureBackend never hits the network checkHealth() probes OPENCODE_SERVER_URL via http.get; the new suites mocked only the SDK, so on any machine without a passwordless 1.x backend on 127.0.0.1:10001 every business request hung in ensureBackend() until the 25s lock timeout (13 tests failed / suite hung ~5min). With http mocked: 40/40 in ~3s, matching app.test.js. * fix(multiuser): validate key names and scope diagnostics per caller role * fix(multiuser): constant-time user key lookup, reject API_KEY collisions, and enforce auth when API_KEY is empty * test(multiuser): reset sessionPrompt mock in okSession helper
1 parent 5700e50 commit 2be4813

11 files changed

Lines changed: 1695 additions & 61 deletions

File tree

‎.env.example‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
API_KEY=change-me
44
OPENCODE_SERVER_PASSWORD=change-me-too
55

6+
# Optional multi-user keys (comma-separated "name:key"; a bare key without a
7+
# name is auto-named key-N). Requests matching any entry authenticate and are
8+
# metered per name; everything else still falls back to API_KEY above.
9+
# API_KEY doubles as the admin key for GET/POST/DELETE /admin/keys.
10+
# OPENCODE_API_KEYS=alice:sk-alice-xxxx,bob:sk-bob-xxxx,sk-bare-key-xxxx
11+
612
# Safer default. Keep true unless you really want model tool access.
713
OPENCODE_DISABLE_TOOLS=true
814
# Reuse one backend session per multi-turn chat conversation (appends only new

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **Multi-User API Keys & Per-Key Usage (`API_KEYS` / `OPENCODE_API_KEYS`)**: configure a list of named user keys — a `[{name, key}]` array in config.json or a comma-separated `name:key` string in the environment (bare keys are auto-named `key-N`; duplicates fail at startup). Requests matching any user key authenticate like `API_KEY` and are metered per name in an in-memory registry (`requests`, prompt/completion/total tokens using the existing chars/4 estimation, `lastUsedAt`; failed requests count toward `requests` but never tokens). New admin endpoints `GET`/`POST`/`DELETE /admin/keys` (mounted once `API_KEYS` is set, admin-authenticated with the legacy `API_KEY` — user keys get 401, no `PUBLIC_DIAGNOSTICS` bypass, `501` when `API_KEY` is empty) let operators hot-add or revoke keys at runtime; `GET` responses expose only a `keyHint` with the key's last 4 characters. `/health/details` gains a `usage: {byKey, total}` section and `/metrics` emits `opencode_proxy_key_requests_total{key}` / `opencode_proxy_key_tokens_total{key,type}` in multi-user mode only — single-key deployments keep their exact previous behavior and snapshot shapes.
1213
- **Anthropic Messages API**: `POST /v1/messages` implements the Anthropic Messages protocol (Claude SDK compatible) on top of the same OpenCode backend — `x-api-key` or Bearer auth, `system` (string or blocks), text/image/tool_use/tool_result content blocks, `tools` with `input_schema`, `tool_choice`, `stop_sequences`, and Anthropic-shaped errors (`{type:'error',error:{type,message}}`). Tool calls surface as `tool_use` content blocks with the original client tool names (no `external__` namespace). Streaming (`stream: true`) emits the Anthropic SSE sequence — `message_start`, `content_block_start/delta/stop` (`thinking_delta`, `text_delta`, `input_json_delta`), `message_delta`, `message_stop` — with keepalives and polling fallback, mirroring the chat-completions pipeline including transient-error retry. Documented in both READMEs; covered by `tests/unit/anthropic.test.js`.
1314
- **Built-in Web Panel (`WEB_PANEL_ENABLED` / `OPENCODE_WEB_PANEL_ENABLED`, default off)**: An optional read-only dashboard served by the proxy itself. `GET /panel` returns a self-contained dark-themed HTML page (zero external assets, CSP locked to same-origin, inline style/script only) showing runtime status (status, uptime, backend URL), the model list, and backend diagnostics from `/health/details` (tool-mode counters, tool ID cache). The page fetches `GET /panel/data.json` — a JSON feed assembled from the models list, the `/health/details` payload and `process.uptime()` — with a Bearer key kept in the browser's localStorage. Auth matches the diagnostics endpoints: with `API_KEY` set, `data.json` requires the Bearer key (the HTML shell is public but carries no data), and `PUBLIC_DIAGNOSTICS=true` or an empty `API_KEY` lifts auth. When disabled, both routes 404 and cost nothing. The feed never returns `OPENCODE_SERVER_PASSWORD`, `ZEN_API_KEY`, `API_KEY` or `API_KEYS` values, and responses carry `X-Content-Type-Options: nosniff` plus `Cache-Control: no-store`. Added `tests/unit/panel.test.js` (10 tests: disabled 404s, headers, 401 gating, secret-leak assertions, public/no-key modes) and full configuration docs.
1415

‎README.en.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ The response contains a `{"type":"tool_use","id":"…","name":"web_fetch","input
152152
| Environment variable | Default | Description |
153153
|:--------|:-------|:-----|
154154
| `API_KEY` | (empty) | Proxy Bearer auth key |
155+
| `OPENCODE_API_KEYS` | (empty) | Multi-user API key list, comma-separated `name:key` (see [Configuration](./docs/en/configuration.md)) |
155156
| `OPENCODE_SERVER_PASSWORD` | (empty) | OpenCode backend password |
156157
| `OPENCODE_PROXY_PORT` / `PORT` | `10000` | Proxy port |
157158
| `OPENCODE_SERVER_PORT` | `10001` | Backend port (applies when `OPENCODE_SERVER_URL` is not set) |
@@ -190,10 +191,15 @@ OPENCODE_PROXY_AUTO_CLEANUP_CONVERSATIONS=true
190191
| `POST` | `/v1/chat/completions` | Chat Completions |
191192
| `POST` | `/v1/responses` | Responses API |
192193
| `POST` | `/v1/messages` | Anthropic Messages API (Claude SDK compatible) |
194+
| `GET` | `/admin/keys` | Multi-user key list and usage (admin auth, available once `API_KEYS` is configured) |
195+
| `POST` | `/admin/keys` | Add a user key at runtime (admin auth) |
196+
| `DELETE` | `/admin/keys/:name` | Remove a user key (admin auth) |
193197

194198
Model names: `opencode/big-pickle`, `gpt5-nano` (auto-resolved to `gpt-5-nano`), `opencode/gpt5-nano`.
195199

196200
> 📖 See [API Reference](./docs/en/api-reference.md)
201+
>
202+
> 👥 Full details on multi-user keys, per-key usage metering, and the admin endpoints: [Configuration](./docs/en/configuration.md#-multi-user-api-keys).
197203
198204
## 🖥️ Web Panel
199205

‎README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ curl -X POST http://127.0.0.1:10000/v1/messages \
152152
| 环境变量 | 默认值 | 说明 |
153153
|:--------|:-------|:-----|
154154
| `API_KEY` | (空) | 代理的 Bearer 认证密钥 |
155+
| `OPENCODE_API_KEYS` | (空) | 多用户 API key 列表,`name:key` 逗号分隔(详见 [配置详解](./docs/zh/configuration.md)) |
155156
| `OPENCODE_SERVER_PASSWORD` | (空) | OpenCode 后端密码 |
156157
| `OPENCODE_PROXY_PORT` / `PORT` | `10000` | 代理端口 |
157158
| `OPENCODE_SERVER_PORT` | `10001` | 后端端口(未显式配置 `OPENCODE_SERVER_URL` 时生效) |
@@ -190,10 +191,15 @@ OPENCODE_PROXY_AUTO_CLEANUP_CONVERSATIONS=true
190191
| `POST` | `/v1/chat/completions` | Chat Completions |
191192
| `POST` | `/v1/responses` | Responses API |
192193
| `POST` | `/v1/messages` | Anthropic Messages API(兼容 Claude SDK) |
194+
| `GET` | `/admin/keys` | 多用户 key 列表与用量(管理员认证,配置 `API_KEYS` 后可用) |
195+
| `POST` | `/admin/keys` | 运行时新增用户 key(管理员认证) |
196+
| `DELETE` | `/admin/keys/:name` | 移除用户 key(管理员认证) |
193197

194198
模型名称写法:`opencode/big-pickle`、`gpt5-nano`(自动解析为 `gpt-5-nano`)、`opencode/gpt5-nano`。
195199

196200
> 📖 详见 [API 参考](./docs/zh/api-reference.md)
201+
>
202+
> 👥 多用户 key、分 key 用量统计与管理端点的完整说明见 [配置详解](./docs/zh/configuration.md#-多用户-api-keys)。
197203
198204
## 🖥️ Web Panel
199205

‎config.json.example‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
{
22
"PORT": 10000,
33
"API_KEY": "your-secret-api-key",
4+
"API_KEYS": [
5+
{ "name": "alice", "key": "sk-alice-..." },
6+
{ "key": "sk-bare-key-..." }
7+
],
48
"BIND_HOST": "0.0.0.0",
59
"DISABLE_TOOLS": true,
610
"CHAT_SESSION_AFFINITY": false,

‎docs/en/configuration.md‎

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ Env vars use the `OPENCODE_` prefix; config.json uses the short names (see table
1515
| `OPENCODE_SERVER_PORT` | - | `10001` | Backend port, only used to build default `OPENCODE_SERVER_URL` |
1616
| `OPENCODE_SERVER_URL` | `OPENCODE_SERVER_URL` | `http://127.0.0.1:10001` | OpenCode backend address |
1717
| `OPENCODE_SERVER_PASSWORD` | `OPENCODE_SERVER_PASSWORD` | (empty) | Backend auth password |
18-
| `API_KEY` | `API_KEY` | (empty) | Proxy Bearer key, no auth when unset |
18+
| `API_KEY` | `API_KEY` | (empty) | Proxy Bearer key (also the admin credential for `/admin/keys`), no auth when unset |
19+
| `OPENCODE_API_KEYS` | `API_KEYS` | (empty) | Multi-user API key list: a comma-separated `name:key` string for env, a `[{name, key}]` array for config.json. See [Multi-user API Keys](#-multi-user-api-keys) |
1920
| `OPENCODE_PROXY_MANAGE_BACKEND` | `MANAGE_BACKEND` | `true` | Proxy starts and manages the OpenCode backend process, which loads the tool-lock plugin |
2021
| `OPENCODE_PATH` | `OPENCODE_PATH` | `opencode` | OpenCode binary path |
2122
| `OPENCODE_ZEN_API_KEY` | `ZEN_API_KEY` | (empty) | Zen API key, passed to the managed backend as `OPENCODE_API_KEY` for paid models |
@@ -57,6 +58,10 @@ Env vars use the `OPENCODE_` prefix; config.json uses the short names (see table
5758
{
5859
"PORT": 10000,
5960
"API_KEY": "your-secret-api-key",
61+
"API_KEYS": [
62+
{ "name": "alice", "key": "sk-alice-..." },
63+
{ "key": "sk-bare-key-..." }
64+
],
6065
"BIND_HOST": "0.0.0.0",
6166
"DISABLE_TOOLS": true,
6267
"INTERNAL_ALLOWED_TOOLS": ["web_fetch"],
@@ -107,6 +112,115 @@ When a request has no `tools`, `opencode.internal_allowed_tools` in the request
107112
}
108113
```
109114

115+
## 👥 Multi-user API Keys
116+
117+
Besides the single `API_KEY`, you can configure a set of **named user keys** (`API_KEYS`) for multi-user distribution with per-key usage metering. Both can be configured at once:
118+
119+
- A request matching any `API_KEYS` entry authenticates and is metered under that key's name;
120+
- Otherwise the request falls back to `API_KEY` (existing single-key deployments are unchanged);
121+
- Both empty → no auth (current behavior).
122+
123+
### Configuration formats
124+
125+
`OPENCODE_API_KEYS` (env) is a **comma-separated `name:key`** string (`=` also works as the separator):
126+
127+
```env
128+
OPENCODE_API_KEYS=alice:sk-alice-xxxx,bob:sk-bob-xxxx,sk-bare-key-xxxx
129+
```
130+
131+
config.json uses an `API_KEYS` array of `{name, key}` entries (`name` optional):
132+
133+
```json
134+
{
135+
"API_KEYS": [
136+
{ "name": "alice", "key": "sk-alice-xxxx" },
137+
{ "key": "sk-bare-key-xxxx" }
138+
]
139+
}
140+
```
141+
142+
**Name semantics**:
143+
144+
- Bare keys without a name are auto-named `key-N` (N = 1-based position of the entry in the list);
145+
- Names are the primary key for usage metering and `/admin/keys` and must be unique; key values must be unique too;
146+
- Duplicate names or duplicate keys **fail at startup** with an error instead of running misconfigured.
147+
148+
### Authentication behavior
149+
150+
- User keys carry exactly the same business-endpoint access (`/v1/chat/completions`, `/v1/responses`, ...) as `API_KEY`;
151+
- `/health/details` and `/metrics` auth semantics are unchanged (Bearer by default, public with `OPENCODE_PUBLIC_DIAGNOSTICS=true`; user keys and `API_KEY` both pass). In multi-user mode, the admin `API_KEY` sees all keys in `usage.byKey` / per-key metrics, a user key sees only its own entry, and unauthenticated `OPENCODE_PUBLIC_DIAGNOSTICS=true` requests see only the aggregate `usage.total` (no key names are exposed publicly);
152+
- Deleting (revoking) a user key makes that key's requests fail with 401 immediately.
153+
154+
### Admin endpoints (/admin/keys)
155+
156+
Mounted only when at least one `API_KEYS` entry is configured. Every endpoint requires the **admin credential = `Bearer API_KEY`** (the legacy single admin key — user keys always get 401), and there is no `OPENCODE_PUBLIC_DIAGNOSTICS` bypass. With an empty `API_KEY` the admin surface answers `501` (unavailable).
157+
158+
| Method | Path | Description |
159+
|:-----|:-----|:-----|
160+
| `GET` | `/admin/keys` | List all user keys with their usage |
161+
| `POST` | `/admin/keys` | Hot-add a key at runtime, body `{"name": "carol", "key": "sk-..."}`; duplicate name or key → `400` |
162+
| `DELETE` | `/admin/keys/:name` | Remove a key; unknown name → `404`, success → `204` |
163+
164+
`GET /admin/keys` response example (`keyHint` shows only the last 4 characters; the full key is never retrievable):
165+
166+
```json
167+
[
168+
{
169+
"name": "alice",
170+
"keyHint": "…xxxx",
171+
"requests": 12,
172+
"promptTokens": 8451,
173+
"completionTokens": 2210,
174+
"totalTokens": 10661,
175+
"lastUsedAt": 1789000000000,
176+
"createdAt": 1788900000000
177+
}
178+
]
179+
```
180+
181+
### Usage accounting
182+
183+
Usage lives in memory and resets on process restart (no persistence, by design):
184+
185+
- **requests**: requests that authenticated and entered a business handler (`/v1/chat/completions`, `/v1/responses`); failed requests (4xx/5xx) also count;
186+
- **promptTokens / completionTokens**: accumulated for successful requests only, using the same `Math.ceil(chars / 4)` estimation as the response usage field; completionTokens include reasoning tokens;
187+
- **lastUsedAt / createdAt**: millisecond timestamps of the last use and of key creation (process start or runtime add).
188+
189+
`/health/details` gains a `usage` field in multi-user mode (absent for single-key deployments, preserving the legacy snapshot shape):
190+
191+
```json
192+
{
193+
"status": "ok",
194+
"proxy": true,
195+
"usage": {
196+
"byKey": {
197+
"alice": {
198+
"requests": 12,
199+
"promptTokens": 8451,
200+
"completionTokens": 2210,
201+
"totalTokens": 10661,
202+
"lastUsedAt": 1789000000000,
203+
"createdAt": 1788900000000
204+
}
205+
},
206+
"total": {
207+
"requests": 12,
208+
"promptTokens": 8451,
209+
"completionTokens": 2210,
210+
"totalTokens": 10661
211+
}
212+
}
213+
}
214+
```
215+
216+
`/metrics` emits per-key Prometheus counters in multi-user mode:
217+
218+
```text
219+
opencode_proxy_key_requests_total{key="alice"}
220+
opencode_proxy_key_tokens_total{key="alice",type="prompt"}
221+
opencode_proxy_key_tokens_total{key="alice",type="completion"}
222+
```
223+
110224
## 📊 Health Diagnostics & Metrics
111225

112226
- `/health` is always a lightweight check.

‎docs/zh/configuration.md‎

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
| `OPENCODE_SERVER_PORT` | - | `10001` | 后端端口,仅用于生成默认 `OPENCODE_SERVER_URL` |
1616
| `OPENCODE_SERVER_URL` | `OPENCODE_SERVER_URL` | `http://127.0.0.1:10001` | OpenCode 后端地址 |
1717
| `OPENCODE_SERVER_PASSWORD` | `OPENCODE_SERVER_PASSWORD` | (空) | 后端认证密码 |
18-
| `API_KEY` | `API_KEY` | (空) | 代理的 Bearer 认证密钥,未配置则不鉴权 |
18+
| `API_KEY` | `API_KEY` | (空) | 代理的 Bearer 认证密钥(管理端点也用它做管理员认证),未配置则不鉴权 |
19+
| `OPENCODE_API_KEYS` | `API_KEYS` | (空) | 多用户 API key 列表:env 用逗号分隔的 `name:key` 字符串,config.json 用 `[{name, key}]` 数组。详见[多用户 API Keys](#-多用户-api-keys) |
1920
| `OPENCODE_PROXY_MANAGE_BACKEND` | `MANAGE_BACKEND` | `true` | 由代理拉起并管理 OpenCode 后端进程,后端会加载工具锁插件 |
2021
| `OPENCODE_PATH` | `OPENCODE_PATH` | `opencode` | OpenCode 可执行文件路径 |
2122
| `OPENCODE_ZEN_API_KEY` | `ZEN_API_KEY` | (空) | Zen API Key,以 `OPENCODE_API_KEY` 传给代理拉起的后端,用于付费模型 |
@@ -57,6 +58,10 @@
5758
{
5859
"PORT": 10000,
5960
"API_KEY": "your-secret-api-key",
61+
"API_KEYS": [
62+
{ "name": "alice", "key": "sk-alice-..." },
63+
{ "key": "sk-bare-key-..." }
64+
],
6065
"BIND_HOST": "0.0.0.0",
6166
"DISABLE_TOOLS": true,
6267
"INTERNAL_ALLOWED_TOOLS": ["web_fetch"],
@@ -107,6 +112,115 @@ OpenCode 官方服务会拒绝工具列表被改动的请求(报错 `free tier
107112
}
108113
```
109114

115+
## 👥 多用户 API Keys
116+
117+
除了单一 `API_KEY`,还可以配置一组**具名用户 key**(`API_KEYS`),实现多用户分发与按 key 用量统计。两者可以同时配置:
118+
119+
- 请求命中 `API_KEYS` 中任意一个 key → 认证通过,并记到该 key 名下;
120+
- 未命中用户 key → 回落到 `API_KEY`(老部署零改动);
121+
- 两者都为空 → 不鉴权(现状不变)。
122+
123+
### 配置格式
124+
125+
环境变量 `OPENCODE_API_KEYS` 使用**逗号分隔的 `name:key`** 字符串(env 友好),`=` 也可作分隔符:
126+
127+
```env
128+
OPENCODE_API_KEYS=alice:sk-alice-xxxx,bob:sk-bob-xxxx,sk-bare-key-xxxx
129+
```
130+
131+
config.json 顶层使用 `API_KEYS` 数组,每项 `{name, key}`(`name` 可省略):
132+
133+
```json
134+
{
135+
"API_KEYS": [
136+
{ "name": "alice", "key": "sk-alice-xxxx" },
137+
{ "key": "sk-bare-key-xxxx" }
138+
]
139+
}
140+
```
141+
142+
**name 语义**:
143+
144+
- 省略 name 的裸 key 自动命名为 `key-N`(N 为该 entry 在列表中的 1-based 位置);
145+
- name 是用量统计与 `/admin/keys` 的主键,必须唯一;key 值也必须唯一;
146+
- 出现重复 name 或重复 key 时**启动直接报错退出**,避免带病运行。
147+
148+
### 认证行为
149+
150+
- 命中用户 key 的请求对业务端点(`/v1/chat/completions`、`/v1/responses` 等)与 `API_KEY` 完全同权;
151+
- `/health/details`、`/metrics` 的鉴权语义不变(默认要求 Bearer,`OPENCODE_PUBLIC_DIAGNOSTICS=true` 时免认证,用户 key 与 `API_KEY` 均可通过)。多用户模式下,管理员 `API_KEY` 可见全部 key 的 `usage.byKey` 与按 key 指标,普通用户 key 仅可见自身条目,未携带密钥的 `OPENCODE_PUBLIC_DIAGNOSTICS=true` 请求仅返回汇总 `usage.total`(不公开任何 key 名称);
152+
- 删除(revoke)某个用户 key 后,用该 key 发出的请求立即返回 401。
153+
154+
### 管理端点(/admin/keys)
155+
156+
仅当配置了至少 1 个 `API_KEYS` 条目时才挂载。全部端点要求**管理员认证 = `Bearer API_KEY`**(即老的单一管理密钥,用户 key 一律 401),且不受 `OPENCODE_PUBLIC_DIAGNOSTICS` 影响(无免认证旁路)。`API_KEY` 为空时管理端点返回 `501`(管理面不可用)。
157+
158+
| 方法 | 路径 | 说明 |
159+
|:-----|:-----|:-----|
160+
| `GET` | `/admin/keys` | 列出全部用户 key 及其用量 |
161+
| `POST` | `/admin/keys` | 运行时热加 key,body 为 `{"name": "carol", "key": "sk-..."}`;重名或 key 重复返回 `400` |
162+
| `DELETE` | `/admin/keys/:name` | 移除 key;不存在返回 `404`,成功返回 `204` |
163+
164+
`GET /admin/keys` 响应示例(keyHint 只露末 4 位,完整 key 不可回取):
165+
166+
```json
167+
[
168+
{
169+
"name": "alice",
170+
"keyHint": "…xxxx",
171+
"requests": 12,
172+
"promptTokens": 8451,
173+
"completionTokens": 2210,
174+
"totalTokens": 10661,
175+
"lastUsedAt": 1789000000000,
176+
"createdAt": 1788900000000
177+
}
178+
]
179+
```
180+
181+
### 用量口径
182+
183+
用量保存在内存中,随进程重启清零(不做持久化):
184+
185+
- **requests**:认证通过后进入业务 handler 的请求数(`/v1/chat/completions`、`/v1/responses`),失败的请求(4xx/5xx)同样计入;
186+
- **promptTokens / completionTokens**:仅成功请求累计,沿用 `Math.ceil(字符数 / 4)` 的估算口径,completionTokens 包含 reasoning tokens(与响应 usage 字段一致);
187+
- **lastUsedAt / createdAt**:最后一次使用时间与 key 创建(进程启动或运行时新增)时间的毫秒时间戳。
188+
189+
`/health/details` 在多用户模式下额外返回 `usage` 字段(单 key 部署不出现,保持兼容):
190+
191+
```json
192+
{
193+
"status": "ok",
194+
"proxy": true,
195+
"usage": {
196+
"byKey": {
197+
"alice": {
198+
"requests": 12,
199+
"promptTokens": 8451,
200+
"completionTokens": 2210,
201+
"totalTokens": 10661,
202+
"lastUsedAt": 1789000000000,
203+
"createdAt": 1788900000000
204+
}
205+
},
206+
"total": {
207+
"requests": 12,
208+
"promptTokens": 8451,
209+
"completionTokens": 2210,
210+
"totalTokens": 10661
211+
}
212+
}
213+
}
214+
```
215+
216+
`/metrics` 在多用户模式下输出按 key 的 Prometheus 计数器:
217+
218+
```text
219+
opencode_proxy_key_requests_total{key="alice"}
220+
opencode_proxy_key_tokens_total{key="alice",type="prompt"}
221+
opencode_proxy_key_tokens_total{key="alice",type="completion"}
222+
```
223+
110224
## 📊 健康诊断与指标
111225

112226
- `/health` 始终是轻量健康检查。

0 commit comments

Comments
 (0)