refactor(api): update traffic chart response structure

Change the traffic chart API response from bucket-based to series-based
to better support frontend visualization libraries. The new format
provides a shared X-axis and aligned data arrays for each model series.

- Replace `buckets` with `x` and `series` in response
- Implement data alignment and zero-filling for time slots
- Update Swagger documentation including pending definitions

BREAKING CHANGE: The `GET /admin/logs/stats/traffic-chart` response
schema has changed. `buckets` and `models` fields are removed.
This commit is contained in:
zenfun
2026-01-08 18:40:44 +08:00
parent 341b54b185
commit f400ffde95
10 changed files with 3239 additions and 294 deletions

View File

@@ -371,6 +371,49 @@ curl "http://localhost:8080/admin/logs/stats?group_by=minute&since=1704150000&un
}
```
#### 流量图表统计
`GET /admin/logs/stats/traffic-chart`
**参数**
| 参数 | 类型 | 说明 | 约束 |
| :--- | :--- | :--- | :--- |
| `granularity` | string | 时间粒度:`hour` / `minute` | `minute` 必须提供 `since` + `until` 且跨度 ≤ 6 小时 |
| `since` | int | 起始时间 (Unix 秒) | 默认 24 小时前 |
| `until` | int | 结束时间 (Unix 秒) | 默认当前 |
| `top_n` | int | Top 模型数量 | 1-20默认 5其余聚合为 `other` |
**响应示例**
```json
{
"granularity": "hour",
"since": 1735689600,
"until": 1735776000,
"x": {
"labels": ["2025-01-01T00:00:00Z", "2025-01-01T01:00:00Z"],
"timestamps": [1735689600, 1735693200],
"totals": {
"data": [2050, 1800],
"tokens_in": [82000, 70000],
"tokens_out": [128000, 110000]
}
},
"series": [
{
"name": "gpt-4-turbo",
"data": [1200, 900],
"tokens_in": [50000, 38000],
"tokens_out": [80000, 62000]
},
{
"name": "other",
"data": [850, 900],
"tokens_in": [32000, 32000],
"tokens_out": [48000, 48000]
}
]
}
```
### 6.2 API Key 状态筛选
`GET /admin/api-keys?status=active|suspended|auto_disabled|manual_disabled`

View File

@@ -1308,6 +1308,272 @@ const docTemplate = `{
}
}
},
"/admin/ip-bans": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "List all global IP/CIDR ban rules",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "List IP bans",
"parameters": [
{
"type": "string",
"description": "Filter by status (active, expired)",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.IPBanView"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"post": {
"security": [
{
"AdminAuth": []
}
],
"description": "Create a new global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Create an IP ban",
"parameters": [
{
"description": "IP Ban Info",
"name": "ban",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api.CreateIPBanRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
}
},
"/admin/ip-bans/{id}": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "Get a single global IP/CIDR ban rule by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Get an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"put": {
"security": [
{
"AdminAuth": []
}
],
"description": "Update a global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Update an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "IP Ban Update",
"name": "ban",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api.UpdateIPBanRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"delete": {
"security": [
{
"AdminAuth": []
}
],
"description": "Delete a global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Delete an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
}
},
"/admin/keys/{id}/access": {
"get": {
"security": [
@@ -1623,7 +1889,7 @@ const docTemplate = `{
"AdminAuth": []
}
],
"description": "Get time × model aggregated data for stacked traffic charts. Returns time buckets with per-model breakdown.",
"description": "Get time × model aggregated data for stacked traffic charts. Returns a shared time axis under ` + "`" + `x` + "`" + ` and per-model series arrays aligned to that axis. Models outside top_n are aggregated under the series name \"other\".",
"produces": [
"application/json"
],
@@ -3528,7 +3794,7 @@ const docTemplate = `{
"MasterAuth": []
}
],
"description": "Returns the identity of the authenticated user based on the Authorization header.\nSupports Admin Token, Master Key, and Child Key (API Key) authentication.\n\nResponse varies by token type:\n- Admin Token: {\"type\": \"admin\", \"role\": \"admin\"}\n- Master Key: {\"type\": \"master\", \"id\": 1, \"name\": \"...\", ...}\n- Child Key: {\"type\": \"key\", \"id\": 5, \"master_id\": 1, \"issued_by\": \"master\", ...}",
"description": "Returns complete identity and realtime statistics of the authenticated user.\nSupports Admin Token, Master Key, and Child Key (API Key) authentication.\nThis endpoint is designed for frontend initialization - call once after login\nand store the response for subsequent use.\n\n**Response varies by token type:**\n\n**Admin Token:**\n- type: \"admin\"\n- role: \"admin\"\n- permissions: [\"*\"] (full access)\n\n**Master Key:**\n- type: \"master\"\n- Basic info: id, name, group, namespaces, status, epoch, max_child_keys, global_qps\n- Timestamps: created_at, updated_at\n- Realtime stats: requests, tokens, qps, qps_limit, rate_limited\n\n**Child Key (API Key):**\n- type: \"key\"\n- Basic info: id, master_id, master_name, group, scopes, namespaces, status\n- Security: issued_at_epoch, issued_by, allow_ips, deny_ips, expires_at\n- Model limits: model_limits, model_limits_enabled\n- Quota: quota_limit, quota_used, quota_reset_at, quota_reset_type\n- Usage stats: request_count, used_tokens, last_accessed_at\n- Realtime stats: requests, tokens, qps, qps_limit, rate_limited\n\n**Error responses:**\n- 401: authorization header required\n- 401: invalid authorization header format\n- 401: invalid token\n- 401: token is not active\n- 401: token has expired\n- 401: token has been revoked\n- 401: master is not active",
"produces": [
"application/json"
],
@@ -5163,6 +5429,23 @@ const docTemplate = `{
}
}
},
"internal_api.CreateIPBanRequest": {
"type": "object",
"required": [
"cidr"
],
"properties": {
"cidr": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"reason": {
"type": "string"
}
}
},
"internal_api.CreateMasterRequest": {
"type": "object",
"required": [
@@ -5314,6 +5597,38 @@ const docTemplate = `{
}
}
},
"internal_api.IPBanView": {
"type": "object",
"properties": {
"cidr": {
"type": "string"
},
"created_at": {
"type": "integer"
},
"created_by": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"hit_count": {
"type": "integer"
},
"id": {
"type": "integer"
},
"reason": {
"type": "string"
},
"status": {
"type": "string"
},
"updated_at": {
"type": "integer"
}
}
},
"internal_api.IssueChildKeyRequest": {
"type": "object",
"properties": {
@@ -5938,42 +6253,36 @@ const docTemplate = `{
}
}
},
"internal_api.TrafficBucket": {
"internal_api.TrafficChartAxis": {
"type": "object",
"properties": {
"breakdown": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"time": {
"type": "string"
},
"timestamp": {
"timestamps": {
"type": "array",
"items": {
"type": "integer"
}
},
"total": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
"totals": {
"$ref": "#/definitions/internal_api.TrafficTotals"
}
}
},
"internal_api.TrafficChartResponse": {
"type": "object",
"properties": {
"buckets": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.TrafficBucket"
}
},
"granularity": {
"type": "string"
},
"models": {
"series": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/internal_api.TrafficSeries"
}
},
"since": {
@@ -5981,22 +6290,60 @@ const docTemplate = `{
},
"until": {
"type": "integer"
},
"x": {
"$ref": "#/definitions/internal_api.TrafficChartAxis"
}
}
},
"internal_api.TrafficMetrics": {
"internal_api.TrafficSeries": {
"type": "object",
"properties": {
"count": {
"data": {
"type": "array",
"items": {
"type": "integer"
}
},
"name": {
"type": "string"
},
"tokens_in": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_out": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"internal_api.TrafficTotals": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_in": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_out": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"internal_api.TrendInfo": {
"type": "object",
@@ -6054,6 +6401,20 @@ const docTemplate = `{
}
}
},
"internal_api.UpdateIPBanRequest": {
"type": "object",
"properties": {
"expires_at": {
"$ref": "#/definitions/internal_api.optionalInt64"
},
"reason": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"internal_api.UpdateMasterRequest": {
"type": "object",
"properties": {
@@ -6115,9 +6476,49 @@ const docTemplate = `{
}
}
},
"internal_api.WhoamiRealtimeView": {
"type": "object",
"properties": {
"qps": {
"description": "Current QPS",
"type": "integer",
"example": 5
},
"qps_limit": {
"description": "QPS limit",
"type": "integer",
"example": 100
},
"rate_limited": {
"description": "Whether currently rate limited",
"type": "boolean",
"example": false
},
"requests": {
"description": "Total requests",
"type": "integer",
"example": 100
},
"tokens": {
"description": "Total tokens used",
"type": "integer",
"example": 50000
},
"updated_at": {
"description": "Last updated timestamp",
"type": "integer",
"example": 1703505600
}
}
},
"internal_api.WhoamiResponse": {
"type": "object",
"properties": {
"allow_ips": {
"description": "IP whitelist (for diagnostics)",
"type": "string",
"example": ""
},
"created_at": {
"type": "integer",
"example": 1703505600
@@ -6126,10 +6527,20 @@ const docTemplate = `{
"type": "string",
"example": "default"
},
"deny_ips": {
"description": "IP blacklist (for diagnostics)",
"type": "string",
"example": ""
},
"epoch": {
"type": "integer",
"example": 1
},
"expires_at": {
"description": "Expiration timestamp (0 = never)",
"type": "integer",
"example": 0
},
"global_qps": {
"type": "integer",
"example": 100
@@ -6151,15 +6562,35 @@ const docTemplate = `{
"type": "string",
"example": "master"
},
"last_accessed_at": {
"description": "Last access timestamp",
"type": "integer",
"example": 0
},
"master_id": {
"description": "Key fields (only present when type is \"key\")",
"type": "integer",
"example": 1
},
"master_name": {
"description": "Parent master name (for display)",
"type": "string",
"example": "tenant-a"
},
"max_child_keys": {
"type": "integer",
"example": 5
},
"model_limits": {
"description": "Model restrictions",
"type": "string",
"example": "gpt-4,claude"
},
"model_limits_enabled": {
"description": "Whether model limits are active",
"type": "boolean",
"example": false
},
"name": {
"type": "string",
"example": "tenant-a"
@@ -6168,6 +6599,46 @@ const docTemplate = `{
"type": "string",
"example": "default,ns1"
},
"permissions": {
"description": "Admin permissions (always [\"*\"])",
"type": "array",
"items": {
"type": "string"
}
},
"quota_limit": {
"description": "Token quota limit (-1 = unlimited)",
"type": "integer",
"example": -1
},
"quota_reset_at": {
"description": "Quota reset timestamp",
"type": "integer",
"example": 0
},
"quota_reset_type": {
"description": "Quota reset type",
"type": "string",
"example": "monthly"
},
"quota_used": {
"description": "Token quota used",
"type": "integer",
"example": 0
},
"realtime": {
"description": "Realtime stats (for master and key types)",
"allOf": [
{
"$ref": "#/definitions/internal_api.WhoamiRealtimeView"
}
]
},
"request_count": {
"description": "Total request count (from DB)",
"type": "integer",
"example": 0
},
"role": {
"description": "Admin fields (only present when type is \"admin\")",
"type": "string",
@@ -6189,6 +6660,11 @@ const docTemplate = `{
"updated_at": {
"type": "integer",
"example": 1703505600
},
"used_tokens": {
"description": "Total tokens used (from DB)",
"type": "integer",
"example": 0
}
}
},
@@ -6217,6 +6693,18 @@ const docTemplate = `{
}
}
},
"internal_api.optionalInt64": {
"type": "object",
"properties": {
"set": {
"type": "boolean"
},
"value": {
"type": "integer",
"format": "int64"
}
}
},
"internal_api.refreshModelRegistryRequest": {
"type": "object",
"properties": {

View File

@@ -176,17 +176,33 @@
"granularity": "hour",
"since": 1735689600,
"until": 1735776000,
"models": ["gpt-4-turbo", "claude-3.5-sonnet", "llama-3-70b", "other"],
"buckets": [
{
"time": "2025-01-01T00:00:00Z",
"timestamp": 1735689600,
"breakdown": {
"gpt-4-turbo": { "count": 1200, "tokens_in": 50000, "tokens_out": 80000 },
"claude-3.5-sonnet": { "count": 800, "tokens_in": 30000, "tokens_out": 45000 },
"other": { "count": 50, "tokens_in": 2000, "tokens_out": 3000 }
"x": {
"labels": ["2025-01-01T00:00:00Z", "2025-01-01T01:00:00Z"],
"timestamps": [1735689600, 1735693200],
"totals": {
"data": [2050, 1800],
"tokens_in": [82000, 70000],
"tokens_out": [128000, 110000]
}
},
"total": { "count": 2050, "tokens_in": 82000, "tokens_out": 128000 }
"series": [
{
"name": "gpt-4-turbo",
"data": [1200, 900],
"tokens_in": [50000, 38000],
"tokens_out": [80000, 62000]
},
{
"name": "claude-3.5-sonnet",
"data": [800, 700],
"tokens_in": [30000, 26000],
"tokens_out": [45000, 40000]
},
{
"name": "other",
"data": [50, 200],
"tokens_in": [2000, 6000],
"tokens_out": [3000, 8000]
}
]
}
@@ -196,11 +212,11 @@
| 图表类型 | 数据映射 |
|----------|----------|
| X 轴 | `buckets[i].time` (格式化为 00:00, 04:00 等) |
| X 轴 | `x.labels` (格式化为 00:00, 04:00 等) |
| Y 轴 | 请求数量 |
| 堆叠系列 | `models` 数组,每个模型一个系列 |
| 系列数据 | `buckets[i].breakdown[model].count` |
| 总量标签 | `buckets[i].total.count` |
| 堆叠系列 | `series` 数组,每个模型一个系列 |
| 系列数据 | `series[i].data` |
| 总量标签 | `x.totals.data` |
**时间范围选择器映射**:
@@ -226,7 +242,7 @@
| 功能点 | 用户操作 | 前端行为 | 后端请求 | 成功反馈 | 异常处理 | 风险/边界 |
|--------|----------|----------|----------|----------|----------|-----------|
| 图表加载 | 页面加载 | 请求默认 24H 图表 | `GET /admin/logs/stats/traffic-chart` | 渲染堆叠柱状图 | 请求失败 → 显示错误提示 | `buckets` 为空 → 显示 No Data |
| 图表加载 | 页面加载 | 请求默认 24H 图表 | `GET /admin/logs/stats/traffic-chart` | 渲染堆叠柱状图 | 请求失败 → 显示错误提示 | `series` 为空或 `x.labels` 为空 → 显示 No Data |
| 时间粒度切换 | 选择分钟级范围 | 自动切换 granularity=minute | `GET /admin/logs/stats/traffic-chart` | 图表更细粒度 | 400 → 回退到 hour 并提示 | minute 范围 ≤ 6h |
| Top 模型限制 | 修改显示数量 | 约束 top_n ≤ 20 | `GET /admin/logs/stats/traffic-chart` | 图表更新 | 400 → 自动回退到 20 | 模型过多时显示 "other" |

View File

@@ -1302,6 +1302,272 @@
}
}
},
"/admin/ip-bans": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "List all global IP/CIDR ban rules",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "List IP bans",
"parameters": [
{
"type": "string",
"description": "Filter by status (active, expired)",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.IPBanView"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"post": {
"security": [
{
"AdminAuth": []
}
],
"description": "Create a new global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Create an IP ban",
"parameters": [
{
"description": "IP Ban Info",
"name": "ban",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api.CreateIPBanRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
}
},
"/admin/ip-bans/{id}": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "Get a single global IP/CIDR ban rule by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Get an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"put": {
"security": [
{
"AdminAuth": []
}
],
"description": "Update a global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Update an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "IP Ban Update",
"name": "ban",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api.UpdateIPBanRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"delete": {
"security": [
{
"AdminAuth": []
}
],
"description": "Delete a global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Delete an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
}
},
"/admin/keys/{id}/access": {
"get": {
"security": [
@@ -1617,7 +1883,7 @@
"AdminAuth": []
}
],
"description": "Get time × model aggregated data for stacked traffic charts. Returns time buckets with per-model breakdown.",
"description": "Get time × model aggregated data for stacked traffic charts. Returns a shared time axis under `x` and per-model series arrays aligned to that axis. Models outside top_n are aggregated under the series name \"other\".",
"produces": [
"application/json"
],
@@ -3522,7 +3788,7 @@
"MasterAuth": []
}
],
"description": "Returns the identity of the authenticated user based on the Authorization header.\nSupports Admin Token, Master Key, and Child Key (API Key) authentication.\n\nResponse varies by token type:\n- Admin Token: {\"type\": \"admin\", \"role\": \"admin\"}\n- Master Key: {\"type\": \"master\", \"id\": 1, \"name\": \"...\", ...}\n- Child Key: {\"type\": \"key\", \"id\": 5, \"master_id\": 1, \"issued_by\": \"master\", ...}",
"description": "Returns complete identity and realtime statistics of the authenticated user.\nSupports Admin Token, Master Key, and Child Key (API Key) authentication.\nThis endpoint is designed for frontend initialization - call once after login\nand store the response for subsequent use.\n\n**Response varies by token type:**\n\n**Admin Token:**\n- type: \"admin\"\n- role: \"admin\"\n- permissions: [\"*\"] (full access)\n\n**Master Key:**\n- type: \"master\"\n- Basic info: id, name, group, namespaces, status, epoch, max_child_keys, global_qps\n- Timestamps: created_at, updated_at\n- Realtime stats: requests, tokens, qps, qps_limit, rate_limited\n\n**Child Key (API Key):**\n- type: \"key\"\n- Basic info: id, master_id, master_name, group, scopes, namespaces, status\n- Security: issued_at_epoch, issued_by, allow_ips, deny_ips, expires_at\n- Model limits: model_limits, model_limits_enabled\n- Quota: quota_limit, quota_used, quota_reset_at, quota_reset_type\n- Usage stats: request_count, used_tokens, last_accessed_at\n- Realtime stats: requests, tokens, qps, qps_limit, rate_limited\n\n**Error responses:**\n- 401: authorization header required\n- 401: invalid authorization header format\n- 401: invalid token\n- 401: token is not active\n- 401: token has expired\n- 401: token has been revoked\n- 401: master is not active",
"produces": [
"application/json"
],
@@ -5157,6 +5423,23 @@
}
}
},
"internal_api.CreateIPBanRequest": {
"type": "object",
"required": [
"cidr"
],
"properties": {
"cidr": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"reason": {
"type": "string"
}
}
},
"internal_api.CreateMasterRequest": {
"type": "object",
"required": [
@@ -5308,6 +5591,38 @@
}
}
},
"internal_api.IPBanView": {
"type": "object",
"properties": {
"cidr": {
"type": "string"
},
"created_at": {
"type": "integer"
},
"created_by": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"hit_count": {
"type": "integer"
},
"id": {
"type": "integer"
},
"reason": {
"type": "string"
},
"status": {
"type": "string"
},
"updated_at": {
"type": "integer"
}
}
},
"internal_api.IssueChildKeyRequest": {
"type": "object",
"properties": {
@@ -5932,42 +6247,36 @@
}
}
},
"internal_api.TrafficBucket": {
"internal_api.TrafficChartAxis": {
"type": "object",
"properties": {
"breakdown": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"time": {
"type": "string"
},
"timestamp": {
"timestamps": {
"type": "array",
"items": {
"type": "integer"
}
},
"total": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
"totals": {
"$ref": "#/definitions/internal_api.TrafficTotals"
}
}
},
"internal_api.TrafficChartResponse": {
"type": "object",
"properties": {
"buckets": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.TrafficBucket"
}
},
"granularity": {
"type": "string"
},
"models": {
"series": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/internal_api.TrafficSeries"
}
},
"since": {
@@ -5975,22 +6284,60 @@
},
"until": {
"type": "integer"
},
"x": {
"$ref": "#/definitions/internal_api.TrafficChartAxis"
}
}
},
"internal_api.TrafficMetrics": {
"internal_api.TrafficSeries": {
"type": "object",
"properties": {
"count": {
"data": {
"type": "array",
"items": {
"type": "integer"
}
},
"name": {
"type": "string"
},
"tokens_in": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_out": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"internal_api.TrafficTotals": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_in": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_out": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"internal_api.TrendInfo": {
"type": "object",
@@ -6048,6 +6395,20 @@
}
}
},
"internal_api.UpdateIPBanRequest": {
"type": "object",
"properties": {
"expires_at": {
"$ref": "#/definitions/internal_api.optionalInt64"
},
"reason": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"internal_api.UpdateMasterRequest": {
"type": "object",
"properties": {
@@ -6109,9 +6470,49 @@
}
}
},
"internal_api.WhoamiRealtimeView": {
"type": "object",
"properties": {
"qps": {
"description": "Current QPS",
"type": "integer",
"example": 5
},
"qps_limit": {
"description": "QPS limit",
"type": "integer",
"example": 100
},
"rate_limited": {
"description": "Whether currently rate limited",
"type": "boolean",
"example": false
},
"requests": {
"description": "Total requests",
"type": "integer",
"example": 100
},
"tokens": {
"description": "Total tokens used",
"type": "integer",
"example": 50000
},
"updated_at": {
"description": "Last updated timestamp",
"type": "integer",
"example": 1703505600
}
}
},
"internal_api.WhoamiResponse": {
"type": "object",
"properties": {
"allow_ips": {
"description": "IP whitelist (for diagnostics)",
"type": "string",
"example": ""
},
"created_at": {
"type": "integer",
"example": 1703505600
@@ -6120,10 +6521,20 @@
"type": "string",
"example": "default"
},
"deny_ips": {
"description": "IP blacklist (for diagnostics)",
"type": "string",
"example": ""
},
"epoch": {
"type": "integer",
"example": 1
},
"expires_at": {
"description": "Expiration timestamp (0 = never)",
"type": "integer",
"example": 0
},
"global_qps": {
"type": "integer",
"example": 100
@@ -6145,15 +6556,35 @@
"type": "string",
"example": "master"
},
"last_accessed_at": {
"description": "Last access timestamp",
"type": "integer",
"example": 0
},
"master_id": {
"description": "Key fields (only present when type is \"key\")",
"type": "integer",
"example": 1
},
"master_name": {
"description": "Parent master name (for display)",
"type": "string",
"example": "tenant-a"
},
"max_child_keys": {
"type": "integer",
"example": 5
},
"model_limits": {
"description": "Model restrictions",
"type": "string",
"example": "gpt-4,claude"
},
"model_limits_enabled": {
"description": "Whether model limits are active",
"type": "boolean",
"example": false
},
"name": {
"type": "string",
"example": "tenant-a"
@@ -6162,6 +6593,46 @@
"type": "string",
"example": "default,ns1"
},
"permissions": {
"description": "Admin permissions (always [\"*\"])",
"type": "array",
"items": {
"type": "string"
}
},
"quota_limit": {
"description": "Token quota limit (-1 = unlimited)",
"type": "integer",
"example": -1
},
"quota_reset_at": {
"description": "Quota reset timestamp",
"type": "integer",
"example": 0
},
"quota_reset_type": {
"description": "Quota reset type",
"type": "string",
"example": "monthly"
},
"quota_used": {
"description": "Token quota used",
"type": "integer",
"example": 0
},
"realtime": {
"description": "Realtime stats (for master and key types)",
"allOf": [
{
"$ref": "#/definitions/internal_api.WhoamiRealtimeView"
}
]
},
"request_count": {
"description": "Total request count (from DB)",
"type": "integer",
"example": 0
},
"role": {
"description": "Admin fields (only present when type is \"admin\")",
"type": "string",
@@ -6183,6 +6654,11 @@
"updated_at": {
"type": "integer",
"example": 1703505600
},
"used_tokens": {
"description": "Total tokens used (from DB)",
"type": "integer",
"example": 0
}
}
},
@@ -6211,6 +6687,18 @@
}
}
},
"internal_api.optionalInt64": {
"type": "object",
"properties": {
"set": {
"type": "boolean"
},
"value": {
"type": "integer",
"format": "int64"
}
}
},
"internal_api.refreshModelRegistryRequest": {
"type": "object",
"properties": {

View File

@@ -562,6 +562,17 @@ definitions:
- title
- type
type: object
internal_api.CreateIPBanRequest:
properties:
cidr:
type: string
expires_at:
type: integer
reason:
type: string
required:
- cidr
type: object
internal_api.CreateMasterRequest:
properties:
global_qps:
@@ -661,6 +672,27 @@ definitions:
$ref: '#/definitions/internal_api.GroupedStatsItem'
type: array
type: object
internal_api.IPBanView:
properties:
cidr:
type: string
created_at:
type: integer
created_by:
type: string
expires_at:
type: integer
hit_count:
type: integer
id:
type: integer
reason:
type: string
status:
type: string
updated_at:
type: integer
type: object
internal_api.IssueChildKeyRequest:
properties:
allow_ips:
@@ -1070,44 +1102,65 @@ definitions:
tokens:
type: integer
type: object
internal_api.TrafficBucket:
internal_api.TrafficChartAxis:
properties:
breakdown:
additionalProperties:
$ref: '#/definitions/internal_api.TrafficMetrics'
type: object
time:
labels:
items:
type: string
timestamp:
type: array
timestamps:
items:
type: integer
total:
$ref: '#/definitions/internal_api.TrafficMetrics'
type: array
totals:
$ref: '#/definitions/internal_api.TrafficTotals'
type: object
internal_api.TrafficChartResponse:
properties:
buckets:
items:
$ref: '#/definitions/internal_api.TrafficBucket'
type: array
granularity:
type: string
models:
series:
items:
type: string
$ref: '#/definitions/internal_api.TrafficSeries'
type: array
since:
type: integer
until:
type: integer
x:
$ref: '#/definitions/internal_api.TrafficChartAxis'
type: object
internal_api.TrafficMetrics:
internal_api.TrafficSeries:
properties:
count:
data:
items:
type: integer
type: array
name:
type: string
tokens_in:
items:
type: integer
type: array
tokens_out:
items:
type: integer
type: array
type: object
internal_api.TrafficTotals:
properties:
data:
items:
type: integer
type: array
tokens_in:
items:
type: integer
type: array
tokens_out:
items:
type: integer
type: array
type: object
internal_api.TrendInfo:
properties:
@@ -1146,6 +1199,15 @@ definitions:
min_tpm_tokens_1m:
type: integer
type: object
internal_api.UpdateIPBanRequest:
properties:
expires_at:
$ref: '#/definitions/internal_api.optionalInt64'
reason:
type: string
status:
type: string
type: object
internal_api.UpdateMasterRequest:
properties:
global_qps:
@@ -1186,17 +1248,56 @@ definitions:
description: active/suspended
type: string
type: object
internal_api.WhoamiRealtimeView:
properties:
qps:
description: Current QPS
example: 5
type: integer
qps_limit:
description: QPS limit
example: 100
type: integer
rate_limited:
description: Whether currently rate limited
example: false
type: boolean
requests:
description: Total requests
example: 100
type: integer
tokens:
description: Total tokens used
example: 50000
type: integer
updated_at:
description: Last updated timestamp
example: 1703505600
type: integer
type: object
internal_api.WhoamiResponse:
properties:
allow_ips:
description: IP whitelist (for diagnostics)
example: ""
type: string
created_at:
example: 1703505600
type: integer
default_namespace:
example: default
type: string
deny_ips:
description: IP blacklist (for diagnostics)
example: ""
type: string
epoch:
example: 1
type: integer
expires_at:
description: Expiration timestamp (0 = never)
example: 0
type: integer
global_qps:
example: 100
type: integer
@@ -1213,19 +1314,64 @@ definitions:
issued_by:
example: master
type: string
last_accessed_at:
description: Last access timestamp
example: 0
type: integer
master_id:
description: Key fields (only present when type is "key")
example: 1
type: integer
master_name:
description: Parent master name (for display)
example: tenant-a
type: string
max_child_keys:
example: 5
type: integer
model_limits:
description: Model restrictions
example: gpt-4,claude
type: string
model_limits_enabled:
description: Whether model limits are active
example: false
type: boolean
name:
example: tenant-a
type: string
namespaces:
example: default,ns1
type: string
permissions:
description: Admin permissions (always ["*"])
items:
type: string
type: array
quota_limit:
description: Token quota limit (-1 = unlimited)
example: -1
type: integer
quota_reset_at:
description: Quota reset timestamp
example: 0
type: integer
quota_reset_type:
description: Quota reset type
example: monthly
type: string
quota_used:
description: Token quota used
example: 0
type: integer
realtime:
allOf:
- $ref: '#/definitions/internal_api.WhoamiRealtimeView'
description: Realtime stats (for master and key types)
request_count:
description: Total request count (from DB)
example: 0
type: integer
role:
description: Admin fields (only present when type is "admin")
example: admin
@@ -1243,6 +1389,10 @@ definitions:
updated_at:
example: 1703505600
type: integer
used_tokens:
description: Total tokens used (from DB)
example: 0
type: integer
type: object
internal_api.apiKeyStatsFlushEntry:
properties:
@@ -1260,6 +1410,14 @@ definitions:
$ref: '#/definitions/internal_api.apiKeyStatsFlushEntry'
type: array
type: object
internal_api.optionalInt64:
properties:
set:
type: boolean
value:
format: int64
type: integer
type: object
internal_api.refreshModelRegistryRequest:
properties:
ref:
@@ -2197,6 +2355,177 @@ paths:
summary: Update feature flags
tags:
- admin
/admin/ip-bans:
get:
consumes:
- application/json
description: List all global IP/CIDR ban rules
parameters:
- description: Filter by status (active, expired)
in: query
name: status
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/internal_api.IPBanView'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: List IP bans
tags:
- admin
- ip-bans
post:
consumes:
- application/json
description: Create a new global IP/CIDR ban rule
parameters:
- description: IP Ban Info
in: body
name: ban
required: true
schema:
$ref: '#/definitions/internal_api.CreateIPBanRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/internal_api.IPBanView'
"400":
description: Bad Request
schema:
$ref: '#/definitions/gin.H'
"409":
description: Conflict
schema:
$ref: '#/definitions/gin.H'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: Create an IP ban
tags:
- admin
- ip-bans
/admin/ip-bans/{id}:
delete:
consumes:
- application/json
description: Delete a global IP/CIDR ban rule
parameters:
- description: IP Ban ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"204":
description: No Content
"404":
description: Not Found
schema:
$ref: '#/definitions/gin.H'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: Delete an IP ban
tags:
- admin
- ip-bans
get:
consumes:
- application/json
description: Get a single global IP/CIDR ban rule by ID
parameters:
- description: IP Ban ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/internal_api.IPBanView'
"404":
description: Not Found
schema:
$ref: '#/definitions/gin.H'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: Get an IP ban
tags:
- admin
- ip-bans
put:
consumes:
- application/json
description: Update a global IP/CIDR ban rule
parameters:
- description: IP Ban ID
in: path
name: id
required: true
type: integer
- description: IP Ban Update
in: body
name: ban
required: true
schema:
$ref: '#/definitions/internal_api.UpdateIPBanRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/internal_api.IPBanView'
"400":
description: Bad Request
schema:
$ref: '#/definitions/gin.H'
"404":
description: Not Found
schema:
$ref: '#/definitions/gin.H'
"409":
description: Conflict
schema:
$ref: '#/definitions/gin.H'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: Update an IP ban
tags:
- admin
- ip-bans
/admin/keys/{id}/access:
get:
description: Returns key default_namespace and namespaces
@@ -2403,7 +2732,8 @@ paths:
/admin/logs/stats/traffic-chart:
get:
description: Get time × model aggregated data for stacked traffic charts. Returns
time buckets with per-model breakdown.
a shared time axis under `x` and per-model series arrays aligned to that axis.
Models outside top_n are aggregated under the series name "other".
parameters:
- description: 'Time granularity: hour (default) or minute'
enum:
@@ -3615,13 +3945,41 @@ paths:
/auth/whoami:
get:
description: |-
Returns the identity of the authenticated user based on the Authorization header.
Returns complete identity and realtime statistics of the authenticated user.
Supports Admin Token, Master Key, and Child Key (API Key) authentication.
This endpoint is designed for frontend initialization - call once after login
and store the response for subsequent use.
Response varies by token type:
- Admin Token: {"type": "admin", "role": "admin"}
- Master Key: {"type": "master", "id": 1, "name": "...", ...}
- Child Key: {"type": "key", "id": 5, "master_id": 1, "issued_by": "master", ...}
**Response varies by token type:**
**Admin Token:**
- type: "admin"
- role: "admin"
- permissions: ["*"] (full access)
**Master Key:**
- type: "master"
- Basic info: id, name, group, namespaces, status, epoch, max_child_keys, global_qps
- Timestamps: created_at, updated_at
- Realtime stats: requests, tokens, qps, qps_limit, rate_limited
**Child Key (API Key):**
- type: "key"
- Basic info: id, master_id, master_name, group, scopes, namespaces, status
- Security: issued_at_epoch, issued_by, allow_ips, deny_ips, expires_at
- Model limits: model_limits, model_limits_enabled
- Quota: quota_limit, quota_used, quota_reset_at, quota_reset_type
- Usage stats: request_count, used_tokens, last_accessed_at
- Realtime stats: requests, tokens, qps, qps_limit, rate_limited
**Error responses:**
- 401: authorization header required
- 401: invalid authorization header format
- 401: invalid token
- 401: token is not active
- 401: token has expired
- 401: token has been revoked
- 401: master is not active
produces:
- application/json
responses:

View File

@@ -1183,7 +1183,7 @@ const docTemplate = `{
"parameters": [
{
"type": "string",
"description": "time period: today, week, month, all",
"description": "time period: today, week, month, last7d, last30d, all",
"name": "period",
"in": "query"
},
@@ -1198,6 +1198,12 @@ const docTemplate = `{
"description": "unix seconds",
"name": "until",
"in": "query"
},
{
"type": "boolean",
"description": "include trend data comparing to previous period",
"name": "include_trends",
"in": "query"
}
],
"responses": {
@@ -1302,6 +1308,272 @@ const docTemplate = `{
}
}
},
"/admin/ip-bans": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "List all global IP/CIDR ban rules",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "List IP bans",
"parameters": [
{
"type": "string",
"description": "Filter by status (active, expired)",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.IPBanView"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"post": {
"security": [
{
"AdminAuth": []
}
],
"description": "Create a new global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Create an IP ban",
"parameters": [
{
"description": "IP Ban Info",
"name": "ban",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api.CreateIPBanRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
}
},
"/admin/ip-bans/{id}": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "Get a single global IP/CIDR ban rule by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Get an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"put": {
"security": [
{
"AdminAuth": []
}
],
"description": "Update a global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Update an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "IP Ban Update",
"name": "ban",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api.UpdateIPBanRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"delete": {
"security": [
{
"AdminAuth": []
}
],
"description": "Delete a global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Delete an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
}
},
"/admin/keys/{id}/access": {
"get": {
"security": [
@@ -1617,7 +1889,7 @@ const docTemplate = `{
"AdminAuth": []
}
],
"description": "Get time × model aggregated data for stacked traffic charts. Returns time buckets with per-model breakdown.",
"description": "Get time × model aggregated data for stacked traffic charts. Returns a shared time axis under ` + "`" + `x` + "`" + ` and per-model series arrays aligned to that axis. Models outside top_n are aggregated under the series name \"other\".",
"produces": [
"application/json"
],
@@ -3522,7 +3794,7 @@ const docTemplate = `{
"MasterAuth": []
}
],
"description": "Returns the identity of the authenticated user based on the Authorization header.\nSupports Admin Token, Master Key, and Child Key (API Key) authentication.\n\nResponse varies by token type:\n- Admin Token: {\"type\": \"admin\", \"role\": \"admin\"}\n- Master Key: {\"type\": \"master\", \"id\": 1, \"name\": \"...\", ...}\n- Child Key: {\"type\": \"key\", \"id\": 5, \"master_id\": 1, \"issued_by\": \"master\", ...}",
"description": "Returns complete identity and realtime statistics of the authenticated user.\nSupports Admin Token, Master Key, and Child Key (API Key) authentication.\nThis endpoint is designed for frontend initialization - call once after login\nand store the response for subsequent use.\n\n**Response varies by token type:**\n\n**Admin Token:**\n- type: \"admin\"\n- role: \"admin\"\n- permissions: [\"*\"] (full access)\n\n**Master Key:**\n- type: \"master\"\n- Basic info: id, name, group, namespaces, status, epoch, max_child_keys, global_qps\n- Timestamps: created_at, updated_at\n- Realtime stats: requests, tokens, qps, qps_limit, rate_limited\n\n**Child Key (API Key):**\n- type: \"key\"\n- Basic info: id, master_id, master_name, group, scopes, namespaces, status\n- Security: issued_at_epoch, issued_by, allow_ips, deny_ips, expires_at\n- Model limits: model_limits, model_limits_enabled\n- Quota: quota_limit, quota_used, quota_reset_at, quota_reset_type\n- Usage stats: request_count, used_tokens, last_accessed_at\n- Realtime stats: requests, tokens, qps, qps_limit, rate_limited\n\n**Error responses:**\n- 401: authorization header required\n- 401: invalid authorization header format\n- 401: invalid token\n- 401: token is not active\n- 401: token has expired\n- 401: token has been revoked\n- 401: master is not active",
"produces": [
"application/json"
],
@@ -5157,6 +5429,23 @@ const docTemplate = `{
}
}
},
"internal_api.CreateIPBanRequest": {
"type": "object",
"required": [
"cidr"
],
"properties": {
"cidr": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"reason": {
"type": "string"
}
}
},
"internal_api.CreateMasterRequest": {
"type": "object",
"required": [
@@ -5208,11 +5497,36 @@ const docTemplate = `{
"$ref": "#/definitions/internal_api.TopModelStat"
}
},
"trends": {
"description": "Only present when include_trends=true",
"allOf": [
{
"$ref": "#/definitions/internal_api.DashboardTrends"
}
]
},
"updated_at": {
"type": "integer"
}
}
},
"internal_api.DashboardTrends": {
"type": "object",
"properties": {
"error_rate": {
"$ref": "#/definitions/internal_api.TrendInfo"
},
"latency": {
"$ref": "#/definitions/internal_api.TrendInfo"
},
"requests": {
"$ref": "#/definitions/internal_api.TrendInfo"
},
"tokens": {
"$ref": "#/definitions/internal_api.TrendInfo"
}
}
},
"internal_api.DeleteLogsRequest": {
"type": "object",
"properties": {
@@ -5283,6 +5597,38 @@ const docTemplate = `{
}
}
},
"internal_api.IPBanView": {
"type": "object",
"properties": {
"cidr": {
"type": "string"
},
"created_at": {
"type": "integer"
},
"created_by": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"hit_count": {
"type": "integer"
},
"id": {
"type": "integer"
},
"reason": {
"type": "string"
},
"status": {
"type": "string"
},
"updated_at": {
"type": "integer"
}
}
},
"internal_api.IssueChildKeyRequest": {
"type": "object",
"properties": {
@@ -5907,42 +6253,36 @@ const docTemplate = `{
}
}
},
"internal_api.TrafficBucket": {
"internal_api.TrafficChartAxis": {
"type": "object",
"properties": {
"breakdown": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"time": {
"type": "string"
},
"timestamp": {
"timestamps": {
"type": "array",
"items": {
"type": "integer"
}
},
"total": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
"totals": {
"$ref": "#/definitions/internal_api.TrafficTotals"
}
}
},
"internal_api.TrafficChartResponse": {
"type": "object",
"properties": {
"buckets": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.TrafficBucket"
}
},
"granularity": {
"type": "string"
},
"models": {
"series": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/internal_api.TrafficSeries"
}
},
"since": {
@@ -5950,22 +6290,73 @@ const docTemplate = `{
},
"until": {
"type": "integer"
},
"x": {
"$ref": "#/definitions/internal_api.TrafficChartAxis"
}
}
},
"internal_api.TrafficMetrics": {
"internal_api.TrafficSeries": {
"type": "object",
"properties": {
"count": {
"data": {
"type": "array",
"items": {
"type": "integer"
}
},
"name": {
"type": "string"
},
"tokens_in": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_out": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"internal_api.TrafficTotals": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_in": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_out": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"internal_api.TrendInfo": {
"type": "object",
"properties": {
"delta": {
"description": "Percentage change from previous period (nil if no baseline)",
"type": "number"
},
"direction": {
"description": "\"up\", \"down\", \"stable\", or \"new\" (no baseline)",
"type": "string"
}
}
},
"internal_api.UpdateAccessRequest": {
"type": "object",
@@ -6010,6 +6401,20 @@ const docTemplate = `{
}
}
},
"internal_api.UpdateIPBanRequest": {
"type": "object",
"properties": {
"expires_at": {
"$ref": "#/definitions/internal_api.optionalInt64"
},
"reason": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"internal_api.UpdateMasterRequest": {
"type": "object",
"properties": {
@@ -6071,9 +6476,49 @@ const docTemplate = `{
}
}
},
"internal_api.WhoamiRealtimeView": {
"type": "object",
"properties": {
"qps": {
"description": "Current QPS",
"type": "integer",
"example": 5
},
"qps_limit": {
"description": "QPS limit",
"type": "integer",
"example": 100
},
"rate_limited": {
"description": "Whether currently rate limited",
"type": "boolean",
"example": false
},
"requests": {
"description": "Total requests",
"type": "integer",
"example": 100
},
"tokens": {
"description": "Total tokens used",
"type": "integer",
"example": 50000
},
"updated_at": {
"description": "Last updated timestamp",
"type": "integer",
"example": 1703505600
}
}
},
"internal_api.WhoamiResponse": {
"type": "object",
"properties": {
"allow_ips": {
"description": "IP whitelist (for diagnostics)",
"type": "string",
"example": ""
},
"created_at": {
"type": "integer",
"example": 1703505600
@@ -6082,10 +6527,20 @@ const docTemplate = `{
"type": "string",
"example": "default"
},
"deny_ips": {
"description": "IP blacklist (for diagnostics)",
"type": "string",
"example": ""
},
"epoch": {
"type": "integer",
"example": 1
},
"expires_at": {
"description": "Expiration timestamp (0 = never)",
"type": "integer",
"example": 0
},
"global_qps": {
"type": "integer",
"example": 100
@@ -6107,15 +6562,35 @@ const docTemplate = `{
"type": "string",
"example": "master"
},
"last_accessed_at": {
"description": "Last access timestamp",
"type": "integer",
"example": 0
},
"master_id": {
"description": "Key fields (only present when type is \"key\")",
"type": "integer",
"example": 1
},
"master_name": {
"description": "Parent master name (for display)",
"type": "string",
"example": "tenant-a"
},
"max_child_keys": {
"type": "integer",
"example": 5
},
"model_limits": {
"description": "Model restrictions",
"type": "string",
"example": "gpt-4,claude"
},
"model_limits_enabled": {
"description": "Whether model limits are active",
"type": "boolean",
"example": false
},
"name": {
"type": "string",
"example": "tenant-a"
@@ -6124,6 +6599,46 @@ const docTemplate = `{
"type": "string",
"example": "default,ns1"
},
"permissions": {
"description": "Admin permissions (always [\"*\"])",
"type": "array",
"items": {
"type": "string"
}
},
"quota_limit": {
"description": "Token quota limit (-1 = unlimited)",
"type": "integer",
"example": -1
},
"quota_reset_at": {
"description": "Quota reset timestamp",
"type": "integer",
"example": 0
},
"quota_reset_type": {
"description": "Quota reset type",
"type": "string",
"example": "monthly"
},
"quota_used": {
"description": "Token quota used",
"type": "integer",
"example": 0
},
"realtime": {
"description": "Realtime stats (for master and key types)",
"allOf": [
{
"$ref": "#/definitions/internal_api.WhoamiRealtimeView"
}
]
},
"request_count": {
"description": "Total request count (from DB)",
"type": "integer",
"example": 0
},
"role": {
"description": "Admin fields (only present when type is \"admin\")",
"type": "string",
@@ -6145,6 +6660,11 @@ const docTemplate = `{
"updated_at": {
"type": "integer",
"example": 1703505600
},
"used_tokens": {
"description": "Total tokens used (from DB)",
"type": "integer",
"example": 0
}
}
},
@@ -6173,6 +6693,18 @@ const docTemplate = `{
}
}
},
"internal_api.optionalInt64": {
"type": "object",
"properties": {
"set": {
"type": "boolean"
},
"value": {
"type": "integer",
"format": "int64"
}
}
},
"internal_api.refreshModelRegistryRequest": {
"type": "object",
"properties": {

View File

@@ -1177,7 +1177,7 @@
"parameters": [
{
"type": "string",
"description": "time period: today, week, month, all",
"description": "time period: today, week, month, last7d, last30d, all",
"name": "period",
"in": "query"
},
@@ -1192,6 +1192,12 @@
"description": "unix seconds",
"name": "until",
"in": "query"
},
{
"type": "boolean",
"description": "include trend data comparing to previous period",
"name": "include_trends",
"in": "query"
}
],
"responses": {
@@ -1296,6 +1302,272 @@
}
}
},
"/admin/ip-bans": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "List all global IP/CIDR ban rules",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "List IP bans",
"parameters": [
{
"type": "string",
"description": "Filter by status (active, expired)",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.IPBanView"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"post": {
"security": [
{
"AdminAuth": []
}
],
"description": "Create a new global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Create an IP ban",
"parameters": [
{
"description": "IP Ban Info",
"name": "ban",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api.CreateIPBanRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
}
},
"/admin/ip-bans/{id}": {
"get": {
"security": [
{
"AdminAuth": []
}
],
"description": "Get a single global IP/CIDR ban rule by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Get an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"put": {
"security": [
{
"AdminAuth": []
}
],
"description": "Update a global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Update an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "IP Ban Update",
"name": "ban",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api.UpdateIPBanRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api.IPBanView"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
},
"delete": {
"security": [
{
"AdminAuth": []
}
],
"description": "Delete a global IP/CIDR ban rule",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin",
"ip-bans"
],
"summary": "Delete an IP ban",
"parameters": [
{
"type": "integer",
"description": "IP Ban ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/gin.H"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
}
}
},
"/admin/keys/{id}/access": {
"get": {
"security": [
@@ -1611,7 +1883,7 @@
"AdminAuth": []
}
],
"description": "Get time × model aggregated data for stacked traffic charts. Returns time buckets with per-model breakdown.",
"description": "Get time × model aggregated data for stacked traffic charts. Returns a shared time axis under `x` and per-model series arrays aligned to that axis. Models outside top_n are aggregated under the series name \"other\".",
"produces": [
"application/json"
],
@@ -3516,7 +3788,7 @@
"MasterAuth": []
}
],
"description": "Returns the identity of the authenticated user based on the Authorization header.\nSupports Admin Token, Master Key, and Child Key (API Key) authentication.\n\nResponse varies by token type:\n- Admin Token: {\"type\": \"admin\", \"role\": \"admin\"}\n- Master Key: {\"type\": \"master\", \"id\": 1, \"name\": \"...\", ...}\n- Child Key: {\"type\": \"key\", \"id\": 5, \"master_id\": 1, \"issued_by\": \"master\", ...}",
"description": "Returns complete identity and realtime statistics of the authenticated user.\nSupports Admin Token, Master Key, and Child Key (API Key) authentication.\nThis endpoint is designed for frontend initialization - call once after login\nand store the response for subsequent use.\n\n**Response varies by token type:**\n\n**Admin Token:**\n- type: \"admin\"\n- role: \"admin\"\n- permissions: [\"*\"] (full access)\n\n**Master Key:**\n- type: \"master\"\n- Basic info: id, name, group, namespaces, status, epoch, max_child_keys, global_qps\n- Timestamps: created_at, updated_at\n- Realtime stats: requests, tokens, qps, qps_limit, rate_limited\n\n**Child Key (API Key):**\n- type: \"key\"\n- Basic info: id, master_id, master_name, group, scopes, namespaces, status\n- Security: issued_at_epoch, issued_by, allow_ips, deny_ips, expires_at\n- Model limits: model_limits, model_limits_enabled\n- Quota: quota_limit, quota_used, quota_reset_at, quota_reset_type\n- Usage stats: request_count, used_tokens, last_accessed_at\n- Realtime stats: requests, tokens, qps, qps_limit, rate_limited\n\n**Error responses:**\n- 401: authorization header required\n- 401: invalid authorization header format\n- 401: invalid token\n- 401: token is not active\n- 401: token has expired\n- 401: token has been revoked\n- 401: master is not active",
"produces": [
"application/json"
],
@@ -5151,6 +5423,23 @@
}
}
},
"internal_api.CreateIPBanRequest": {
"type": "object",
"required": [
"cidr"
],
"properties": {
"cidr": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"reason": {
"type": "string"
}
}
},
"internal_api.CreateMasterRequest": {
"type": "object",
"required": [
@@ -5202,11 +5491,36 @@
"$ref": "#/definitions/internal_api.TopModelStat"
}
},
"trends": {
"description": "Only present when include_trends=true",
"allOf": [
{
"$ref": "#/definitions/internal_api.DashboardTrends"
}
]
},
"updated_at": {
"type": "integer"
}
}
},
"internal_api.DashboardTrends": {
"type": "object",
"properties": {
"error_rate": {
"$ref": "#/definitions/internal_api.TrendInfo"
},
"latency": {
"$ref": "#/definitions/internal_api.TrendInfo"
},
"requests": {
"$ref": "#/definitions/internal_api.TrendInfo"
},
"tokens": {
"$ref": "#/definitions/internal_api.TrendInfo"
}
}
},
"internal_api.DeleteLogsRequest": {
"type": "object",
"properties": {
@@ -5277,6 +5591,38 @@
}
}
},
"internal_api.IPBanView": {
"type": "object",
"properties": {
"cidr": {
"type": "string"
},
"created_at": {
"type": "integer"
},
"created_by": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"hit_count": {
"type": "integer"
},
"id": {
"type": "integer"
},
"reason": {
"type": "string"
},
"status": {
"type": "string"
},
"updated_at": {
"type": "integer"
}
}
},
"internal_api.IssueChildKeyRequest": {
"type": "object",
"properties": {
@@ -5901,42 +6247,36 @@
}
}
},
"internal_api.TrafficBucket": {
"internal_api.TrafficChartAxis": {
"type": "object",
"properties": {
"breakdown": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"time": {
"type": "string"
},
"timestamp": {
"timestamps": {
"type": "array",
"items": {
"type": "integer"
}
},
"total": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
"totals": {
"$ref": "#/definitions/internal_api.TrafficTotals"
}
}
},
"internal_api.TrafficChartResponse": {
"type": "object",
"properties": {
"buckets": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.TrafficBucket"
}
},
"granularity": {
"type": "string"
},
"models": {
"series": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/internal_api.TrafficSeries"
}
},
"since": {
@@ -5944,22 +6284,73 @@
},
"until": {
"type": "integer"
},
"x": {
"$ref": "#/definitions/internal_api.TrafficChartAxis"
}
}
},
"internal_api.TrafficMetrics": {
"internal_api.TrafficSeries": {
"type": "object",
"properties": {
"count": {
"data": {
"type": "array",
"items": {
"type": "integer"
}
},
"name": {
"type": "string"
},
"tokens_in": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_out": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"internal_api.TrafficTotals": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_in": {
"type": "array",
"items": {
"type": "integer"
}
},
"tokens_out": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"internal_api.TrendInfo": {
"type": "object",
"properties": {
"delta": {
"description": "Percentage change from previous period (nil if no baseline)",
"type": "number"
},
"direction": {
"description": "\"up\", \"down\", \"stable\", or \"new\" (no baseline)",
"type": "string"
}
}
},
"internal_api.UpdateAccessRequest": {
"type": "object",
@@ -6004,6 +6395,20 @@
}
}
},
"internal_api.UpdateIPBanRequest": {
"type": "object",
"properties": {
"expires_at": {
"$ref": "#/definitions/internal_api.optionalInt64"
},
"reason": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"internal_api.UpdateMasterRequest": {
"type": "object",
"properties": {
@@ -6065,9 +6470,49 @@
}
}
},
"internal_api.WhoamiRealtimeView": {
"type": "object",
"properties": {
"qps": {
"description": "Current QPS",
"type": "integer",
"example": 5
},
"qps_limit": {
"description": "QPS limit",
"type": "integer",
"example": 100
},
"rate_limited": {
"description": "Whether currently rate limited",
"type": "boolean",
"example": false
},
"requests": {
"description": "Total requests",
"type": "integer",
"example": 100
},
"tokens": {
"description": "Total tokens used",
"type": "integer",
"example": 50000
},
"updated_at": {
"description": "Last updated timestamp",
"type": "integer",
"example": 1703505600
}
}
},
"internal_api.WhoamiResponse": {
"type": "object",
"properties": {
"allow_ips": {
"description": "IP whitelist (for diagnostics)",
"type": "string",
"example": ""
},
"created_at": {
"type": "integer",
"example": 1703505600
@@ -6076,10 +6521,20 @@
"type": "string",
"example": "default"
},
"deny_ips": {
"description": "IP blacklist (for diagnostics)",
"type": "string",
"example": ""
},
"epoch": {
"type": "integer",
"example": 1
},
"expires_at": {
"description": "Expiration timestamp (0 = never)",
"type": "integer",
"example": 0
},
"global_qps": {
"type": "integer",
"example": 100
@@ -6101,15 +6556,35 @@
"type": "string",
"example": "master"
},
"last_accessed_at": {
"description": "Last access timestamp",
"type": "integer",
"example": 0
},
"master_id": {
"description": "Key fields (only present when type is \"key\")",
"type": "integer",
"example": 1
},
"master_name": {
"description": "Parent master name (for display)",
"type": "string",
"example": "tenant-a"
},
"max_child_keys": {
"type": "integer",
"example": 5
},
"model_limits": {
"description": "Model restrictions",
"type": "string",
"example": "gpt-4,claude"
},
"model_limits_enabled": {
"description": "Whether model limits are active",
"type": "boolean",
"example": false
},
"name": {
"type": "string",
"example": "tenant-a"
@@ -6118,6 +6593,46 @@
"type": "string",
"example": "default,ns1"
},
"permissions": {
"description": "Admin permissions (always [\"*\"])",
"type": "array",
"items": {
"type": "string"
}
},
"quota_limit": {
"description": "Token quota limit (-1 = unlimited)",
"type": "integer",
"example": -1
},
"quota_reset_at": {
"description": "Quota reset timestamp",
"type": "integer",
"example": 0
},
"quota_reset_type": {
"description": "Quota reset type",
"type": "string",
"example": "monthly"
},
"quota_used": {
"description": "Token quota used",
"type": "integer",
"example": 0
},
"realtime": {
"description": "Realtime stats (for master and key types)",
"allOf": [
{
"$ref": "#/definitions/internal_api.WhoamiRealtimeView"
}
]
},
"request_count": {
"description": "Total request count (from DB)",
"type": "integer",
"example": 0
},
"role": {
"description": "Admin fields (only present when type is \"admin\")",
"type": "string",
@@ -6139,6 +6654,11 @@
"updated_at": {
"type": "integer",
"example": 1703505600
},
"used_tokens": {
"description": "Total tokens used (from DB)",
"type": "integer",
"example": 0
}
}
},
@@ -6167,6 +6687,18 @@
}
}
},
"internal_api.optionalInt64": {
"type": "object",
"properties": {
"set": {
"type": "boolean"
},
"value": {
"type": "integer",
"format": "int64"
}
}
},
"internal_api.refreshModelRegistryRequest": {
"type": "object",
"properties": {

View File

@@ -562,6 +562,17 @@ definitions:
- title
- type
type: object
internal_api.CreateIPBanRequest:
properties:
cidr:
type: string
expires_at:
type: integer
reason:
type: string
required:
- cidr
type: object
internal_api.CreateMasterRequest:
properties:
global_qps:
@@ -596,9 +607,24 @@ definitions:
items:
$ref: '#/definitions/internal_api.TopModelStat'
type: array
trends:
allOf:
- $ref: '#/definitions/internal_api.DashboardTrends'
description: Only present when include_trends=true
updated_at:
type: integer
type: object
internal_api.DashboardTrends:
properties:
error_rate:
$ref: '#/definitions/internal_api.TrendInfo'
latency:
$ref: '#/definitions/internal_api.TrendInfo'
requests:
$ref: '#/definitions/internal_api.TrendInfo'
tokens:
$ref: '#/definitions/internal_api.TrendInfo'
type: object
internal_api.DeleteLogsRequest:
properties:
before:
@@ -646,6 +672,27 @@ definitions:
$ref: '#/definitions/internal_api.GroupedStatsItem'
type: array
type: object
internal_api.IPBanView:
properties:
cidr:
type: string
created_at:
type: integer
created_by:
type: string
expires_at:
type: integer
hit_count:
type: integer
id:
type: integer
reason:
type: string
status:
type: string
updated_at:
type: integer
type: object
internal_api.IssueChildKeyRequest:
properties:
allow_ips:
@@ -1055,44 +1102,74 @@ definitions:
tokens:
type: integer
type: object
internal_api.TrafficBucket:
internal_api.TrafficChartAxis:
properties:
breakdown:
additionalProperties:
$ref: '#/definitions/internal_api.TrafficMetrics'
type: object
time:
labels:
items:
type: string
timestamp:
type: array
timestamps:
items:
type: integer
total:
$ref: '#/definitions/internal_api.TrafficMetrics'
type: array
totals:
$ref: '#/definitions/internal_api.TrafficTotals'
type: object
internal_api.TrafficChartResponse:
properties:
buckets:
items:
$ref: '#/definitions/internal_api.TrafficBucket'
type: array
granularity:
type: string
models:
series:
items:
type: string
$ref: '#/definitions/internal_api.TrafficSeries'
type: array
since:
type: integer
until:
type: integer
x:
$ref: '#/definitions/internal_api.TrafficChartAxis'
type: object
internal_api.TrafficMetrics:
internal_api.TrafficSeries:
properties:
count:
data:
items:
type: integer
type: array
name:
type: string
tokens_in:
items:
type: integer
type: array
tokens_out:
items:
type: integer
type: array
type: object
internal_api.TrafficTotals:
properties:
data:
items:
type: integer
type: array
tokens_in:
items:
type: integer
type: array
tokens_out:
items:
type: integer
type: array
type: object
internal_api.TrendInfo:
properties:
delta:
description: Percentage change from previous period (nil if no baseline)
type: number
direction:
description: '"up", "down", "stable", or "new" (no baseline)'
type: string
type: object
internal_api.UpdateAccessRequest:
properties:
@@ -1122,6 +1199,15 @@ definitions:
min_tpm_tokens_1m:
type: integer
type: object
internal_api.UpdateIPBanRequest:
properties:
expires_at:
$ref: '#/definitions/internal_api.optionalInt64'
reason:
type: string
status:
type: string
type: object
internal_api.UpdateMasterRequest:
properties:
global_qps:
@@ -1162,17 +1248,56 @@ definitions:
description: active/suspended
type: string
type: object
internal_api.WhoamiRealtimeView:
properties:
qps:
description: Current QPS
example: 5
type: integer
qps_limit:
description: QPS limit
example: 100
type: integer
rate_limited:
description: Whether currently rate limited
example: false
type: boolean
requests:
description: Total requests
example: 100
type: integer
tokens:
description: Total tokens used
example: 50000
type: integer
updated_at:
description: Last updated timestamp
example: 1703505600
type: integer
type: object
internal_api.WhoamiResponse:
properties:
allow_ips:
description: IP whitelist (for diagnostics)
example: ""
type: string
created_at:
example: 1703505600
type: integer
default_namespace:
example: default
type: string
deny_ips:
description: IP blacklist (for diagnostics)
example: ""
type: string
epoch:
example: 1
type: integer
expires_at:
description: Expiration timestamp (0 = never)
example: 0
type: integer
global_qps:
example: 100
type: integer
@@ -1189,19 +1314,64 @@ definitions:
issued_by:
example: master
type: string
last_accessed_at:
description: Last access timestamp
example: 0
type: integer
master_id:
description: Key fields (only present when type is "key")
example: 1
type: integer
master_name:
description: Parent master name (for display)
example: tenant-a
type: string
max_child_keys:
example: 5
type: integer
model_limits:
description: Model restrictions
example: gpt-4,claude
type: string
model_limits_enabled:
description: Whether model limits are active
example: false
type: boolean
name:
example: tenant-a
type: string
namespaces:
example: default,ns1
type: string
permissions:
description: Admin permissions (always ["*"])
items:
type: string
type: array
quota_limit:
description: Token quota limit (-1 = unlimited)
example: -1
type: integer
quota_reset_at:
description: Quota reset timestamp
example: 0
type: integer
quota_reset_type:
description: Quota reset type
example: monthly
type: string
quota_used:
description: Token quota used
example: 0
type: integer
realtime:
allOf:
- $ref: '#/definitions/internal_api.WhoamiRealtimeView'
description: Realtime stats (for master and key types)
request_count:
description: Total request count (from DB)
example: 0
type: integer
role:
description: Admin fields (only present when type is "admin")
example: admin
@@ -1219,6 +1389,10 @@ definitions:
updated_at:
example: 1703505600
type: integer
used_tokens:
description: Total tokens used (from DB)
example: 0
type: integer
type: object
internal_api.apiKeyStatsFlushEntry:
properties:
@@ -1236,6 +1410,14 @@ definitions:
$ref: '#/definitions/internal_api.apiKeyStatsFlushEntry'
type: array
type: object
internal_api.optionalInt64:
properties:
set:
type: boolean
value:
format: int64
type: integer
type: object
internal_api.refreshModelRegistryRequest:
properties:
ref:
@@ -2086,7 +2268,7 @@ paths:
description: Returns aggregated metrics for dashboard display including requests,
tokens, latency, masters, keys, and provider keys statistics
parameters:
- description: 'time period: today, week, month, all'
- description: 'time period: today, week, month, last7d, last30d, all'
in: query
name: period
type: string
@@ -2098,6 +2280,10 @@ paths:
in: query
name: until
type: integer
- description: include trend data comparing to previous period
in: query
name: include_trends
type: boolean
produces:
- application/json
responses:
@@ -2169,6 +2355,177 @@ paths:
summary: Update feature flags
tags:
- admin
/admin/ip-bans:
get:
consumes:
- application/json
description: List all global IP/CIDR ban rules
parameters:
- description: Filter by status (active, expired)
in: query
name: status
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/internal_api.IPBanView'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: List IP bans
tags:
- admin
- ip-bans
post:
consumes:
- application/json
description: Create a new global IP/CIDR ban rule
parameters:
- description: IP Ban Info
in: body
name: ban
required: true
schema:
$ref: '#/definitions/internal_api.CreateIPBanRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/internal_api.IPBanView'
"400":
description: Bad Request
schema:
$ref: '#/definitions/gin.H'
"409":
description: Conflict
schema:
$ref: '#/definitions/gin.H'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: Create an IP ban
tags:
- admin
- ip-bans
/admin/ip-bans/{id}:
delete:
consumes:
- application/json
description: Delete a global IP/CIDR ban rule
parameters:
- description: IP Ban ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"204":
description: No Content
"404":
description: Not Found
schema:
$ref: '#/definitions/gin.H'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: Delete an IP ban
tags:
- admin
- ip-bans
get:
consumes:
- application/json
description: Get a single global IP/CIDR ban rule by ID
parameters:
- description: IP Ban ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/internal_api.IPBanView'
"404":
description: Not Found
schema:
$ref: '#/definitions/gin.H'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: Get an IP ban
tags:
- admin
- ip-bans
put:
consumes:
- application/json
description: Update a global IP/CIDR ban rule
parameters:
- description: IP Ban ID
in: path
name: id
required: true
type: integer
- description: IP Ban Update
in: body
name: ban
required: true
schema:
$ref: '#/definitions/internal_api.UpdateIPBanRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/internal_api.IPBanView'
"400":
description: Bad Request
schema:
$ref: '#/definitions/gin.H'
"404":
description: Not Found
schema:
$ref: '#/definitions/gin.H'
"409":
description: Conflict
schema:
$ref: '#/definitions/gin.H'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/gin.H'
security:
- AdminAuth: []
summary: Update an IP ban
tags:
- admin
- ip-bans
/admin/keys/{id}/access:
get:
description: Returns key default_namespace and namespaces
@@ -2375,7 +2732,8 @@ paths:
/admin/logs/stats/traffic-chart:
get:
description: Get time × model aggregated data for stacked traffic charts. Returns
time buckets with per-model breakdown.
a shared time axis under `x` and per-model series arrays aligned to that axis.
Models outside top_n are aggregated under the series name "other".
parameters:
- description: 'Time granularity: hour (default) or minute'
enum:
@@ -3587,13 +3945,41 @@ paths:
/auth/whoami:
get:
description: |-
Returns the identity of the authenticated user based on the Authorization header.
Returns complete identity and realtime statistics of the authenticated user.
Supports Admin Token, Master Key, and Child Key (API Key) authentication.
This endpoint is designed for frontend initialization - call once after login
and store the response for subsequent use.
Response varies by token type:
- Admin Token: {"type": "admin", "role": "admin"}
- Master Key: {"type": "master", "id": 1, "name": "...", ...}
- Child Key: {"type": "key", "id": 5, "master_id": 1, "issued_by": "master", ...}
**Response varies by token type:**
**Admin Token:**
- type: "admin"
- role: "admin"
- permissions: ["*"] (full access)
**Master Key:**
- type: "master"
- Basic info: id, name, group, namespaces, status, epoch, max_child_keys, global_qps
- Timestamps: created_at, updated_at
- Realtime stats: requests, tokens, qps, qps_limit, rate_limited
**Child Key (API Key):**
- type: "key"
- Basic info: id, master_id, master_name, group, scopes, namespaces, status
- Security: issued_at_epoch, issued_by, allow_ips, deny_ips, expires_at
- Model limits: model_limits, model_limits_enabled
- Quota: quota_limit, quota_used, quota_reset_at, quota_reset_type
- Usage stats: request_count, used_tokens, last_accessed_at
- Realtime stats: requests, tokens, qps, qps_limit, rate_limited
**Error responses:**
- 401: authorization header required
- 401: invalid authorization header format
- 401: invalid token
- 401: token is not active
- 401: token has expired
- 401: token has been revoked
- 401: master is not active
produces:
- application/json
responses:

View File

@@ -609,25 +609,26 @@ func (h *Handler) logStatsByMinute(c *gin.Context, q *gorm.DB, sinceTime, untilT
c.JSON(http.StatusOK, GroupedStatsResponse{Items: items})
}
// TrafficMetrics contains the metrics for a model in a bucket
type TrafficMetrics struct {
Count int64 `json:"count"`
TokensIn int64 `json:"tokens_in"`
TokensOut int64 `json:"tokens_out"`
// TrafficSeries contains the metrics for a model aligned to the shared time axis.
type TrafficSeries struct {
Name string `json:"name"`
Data []int64 `json:"data"`
TokensIn []int64 `json:"tokens_in"`
TokensOut []int64 `json:"tokens_out"`
}
// ModelMetricsMap is a map from model name to TrafficMetrics.
// Keys are model names (e.g. "gpt-4", "claude-3-opus") or "other" for aggregated remaining models.
// Example: {"gpt-4": {"count": 10, "tokens_in": 1000, "tokens_out": 500}, "other": {"count": 3, "tokens_in": 200, "tokens_out": 100}}
type ModelMetricsMap map[string]TrafficMetrics
// TrafficTotals contains aggregated totals aligned to the shared time axis.
type TrafficTotals struct {
Data []int64 `json:"data"`
TokensIn []int64 `json:"tokens_in"`
TokensOut []int64 `json:"tokens_out"`
}
// TrafficBucket represents one time bucket with model breakdown
type TrafficBucket struct {
Time string `json:"time"`
Timestamp int64 `json:"timestamp"`
// Breakdown is a map from model name (e.g. "gpt-4", "claude-3-opus", "other") to its metrics
Breakdown ModelMetricsMap `json:"breakdown"`
Total TrafficMetrics `json:"total"`
// TrafficChartAxis defines the shared time axis for chart data.
type TrafficChartAxis struct {
Labels []string `json:"labels"`
Timestamps []int64 `json:"timestamps"`
Totals TrafficTotals `json:"totals"`
}
// TrafficChartResponse is the response for traffic chart API
@@ -635,8 +636,8 @@ type TrafficChartResponse struct {
Granularity string `json:"granularity"`
Since int64 `json:"since"`
Until int64 `json:"until"`
Models []string `json:"models"`
Buckets []TrafficBucket `json:"buckets"`
X TrafficChartAxis `json:"x"`
Series []TrafficSeries `json:"series"`
}
const (
@@ -644,9 +645,121 @@ const (
maxTrafficTopN = 20
)
type trafficBucketRow struct {
Bucket time.Time
ModelName string
Cnt int64
TokensIn int64
TokensOut int64
}
func buildTrafficChartSeriesResponse(rows []trafficBucketRow, topN int, granularity string, sinceTime, untilTime time.Time) TrafficChartResponse {
bucketLabels := make(map[int64]string)
bucketOrder := make([]int64, 0)
for _, r := range rows {
ts := r.Bucket.Unix()
if _, exists := bucketLabels[ts]; !exists {
bucketLabels[ts] = r.Bucket.UTC().Format(time.RFC3339)
bucketOrder = append(bucketOrder, ts)
}
}
modelCounts := make(map[string]int64)
for _, r := range rows {
modelCounts[r.ModelName] += r.Cnt
}
type modelCount struct {
name string
count int64
}
modelList := make([]modelCount, 0, len(modelCounts))
for name, cnt := range modelCounts {
modelList = append(modelList, modelCount{name, cnt})
}
for i := 0; i < len(modelList)-1; i++ {
for j := i + 1; j < len(modelList); j++ {
if modelList[j].count > modelList[i].count {
modelList[i], modelList[j] = modelList[j], modelList[i]
}
}
}
topModels := make(map[string]bool, topN)
seriesNames := make([]string, 0, topN+1)
for i := 0; i < len(modelList) && i < topN; i++ {
topModels[modelList[i].name] = true
seriesNames = append(seriesNames, modelList[i].name)
}
if len(modelList) > topN {
seriesNames = append(seriesNames, "other")
}
bucketIndex := make(map[int64]int, len(bucketOrder))
labels := make([]string, len(bucketOrder))
timestamps := make([]int64, len(bucketOrder))
for i, ts := range bucketOrder {
bucketIndex[ts] = i
labels[i] = bucketLabels[ts]
timestamps[i] = ts
}
series := make([]TrafficSeries, len(seriesNames))
seriesIndex := make(map[string]int, len(seriesNames))
for i, name := range seriesNames {
series[i] = TrafficSeries{
Name: name,
Data: make([]int64, len(bucketOrder)),
TokensIn: make([]int64, len(bucketOrder)),
TokensOut: make([]int64, len(bucketOrder)),
}
seriesIndex[name] = i
}
totals := TrafficTotals{
Data: make([]int64, len(bucketOrder)),
TokensIn: make([]int64, len(bucketOrder)),
TokensOut: make([]int64, len(bucketOrder)),
}
for _, r := range rows {
ts := r.Bucket.Unix()
idx, ok := bucketIndex[ts]
if !ok {
continue
}
modelKey := r.ModelName
if !topModels[modelKey] {
modelKey = "other"
}
if seriesIdx, exists := seriesIndex[modelKey]; exists {
series[seriesIdx].Data[idx] += r.Cnt
series[seriesIdx].TokensIn[idx] += r.TokensIn
series[seriesIdx].TokensOut[idx] += r.TokensOut
}
totals.Data[idx] += r.Cnt
totals.TokensIn[idx] += r.TokensIn
totals.TokensOut[idx] += r.TokensOut
}
return TrafficChartResponse{
Granularity: granularity,
Since: sinceTime.Unix(),
Until: untilTime.Unix(),
X: TrafficChartAxis{
Labels: labels,
Timestamps: timestamps,
Totals: totals,
},
Series: series,
}
}
// GetTrafficChart godoc
// @Summary Traffic chart data (admin)
// @Description Get time × model aggregated data for stacked traffic charts. Returns time buckets with per-model breakdown. The 'breakdown' field in each bucket is a map where keys are model names (e.g. "gpt-4", "claude-3-opus") and values are TrafficMetrics objects. Models outside top_n are aggregated under the key "other". Example: {"gpt-4": {"count": 10, "tokens_in": 1000, "tokens_out": 500}, "other": {"count": 3, "tokens_in": 200, "tokens_out": 100}}
// @Description Get time × model aggregated data for stacked traffic charts. Returns a shared time axis under `x` and per-model series arrays aligned to that axis. Models outside top_n are aggregated under the series name "other".
// @Tags admin
// @Produce json
// @Security AdminAuth
@@ -731,14 +844,7 @@ func (h *Handler) GetTrafficChart(c *gin.Context) {
truncFunc = "DATE_TRUNC('hour', created_at)"
}
type bucketModelStats struct {
Bucket time.Time
ModelName string
Cnt int64
TokensIn int64
TokensOut int64
}
var rows []bucketModelStats
var rows []trafficBucketRow
if err := q.Select(truncFunc + " as bucket, model_name, COUNT(*) as cnt, COALESCE(SUM(tokens_in),0) as tokens_in, COALESCE(SUM(tokens_out),0) as tokens_out").
Group("bucket, model_name").
@@ -748,88 +854,7 @@ func (h *Handler) GetTrafficChart(c *gin.Context) {
return
}
// Calculate global model counts for top_n selection
modelCounts := make(map[string]int64)
for _, r := range rows {
modelCounts[r.ModelName] += r.Cnt
}
// Get top N models
type modelCount struct {
name string
count int64
}
var modelList []modelCount
for name, cnt := range modelCounts {
modelList = append(modelList, modelCount{name, cnt})
}
// Sort by count descending
for i := 0; i < len(modelList)-1; i++ {
for j := i + 1; j < len(modelList); j++ {
if modelList[j].count > modelList[i].count {
modelList[i], modelList[j] = modelList[j], modelList[i]
}
}
}
topModels := make(map[string]bool)
var modelNames []string
for i := 0; i < len(modelList) && i < topN; i++ {
topModels[modelList[i].name] = true
modelNames = append(modelNames, modelList[i].name)
}
hasOther := len(modelList) > topN
if hasOther {
modelNames = append(modelNames, "other")
}
// Build buckets with breakdown
bucketMap := make(map[int64]*TrafficBucket)
var bucketOrder []int64
for _, r := range rows {
ts := r.Bucket.Unix()
bucket, exists := bucketMap[ts]
if !exists {
bucket = &TrafficBucket{
Time: r.Bucket.UTC().Format(time.RFC3339),
Timestamp: ts,
Breakdown: make(map[string]TrafficMetrics),
}
bucketMap[ts] = bucket
bucketOrder = append(bucketOrder, ts)
}
modelKey := r.ModelName
if !topModels[modelKey] {
modelKey = "other"
}
existing := bucket.Breakdown[modelKey]
bucket.Breakdown[modelKey] = TrafficMetrics{
Count: existing.Count + r.Cnt,
TokensIn: existing.TokensIn + r.TokensIn,
TokensOut: existing.TokensOut + r.TokensOut,
}
bucket.Total.Count += r.Cnt
bucket.Total.TokensIn += r.TokensIn
bucket.Total.TokensOut += r.TokensOut
}
// Build response buckets in order
buckets := make([]TrafficBucket, 0, len(bucketOrder))
for _, ts := range bucketOrder {
buckets = append(buckets, *bucketMap[ts])
}
c.JSON(http.StatusOK, TrafficChartResponse{
Granularity: granularity,
Since: sinceTime.Unix(),
Until: untilTime.Unix(),
Models: modelNames,
Buckets: buckets,
})
c.JSON(http.StatusOK, buildTrafficChartSeriesResponse(rows, topN, granularity, sinceTime, untilTime))
}
// ListSelfLogs godoc

View File

@@ -355,6 +355,83 @@ func TestLogStats_DefaultBehavior(t *testing.T) {
}
}
func TestBuildTrafficChartSeriesResponse(t *testing.T) {
bucket1 := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
bucket2 := time.Date(2025, 1, 1, 1, 0, 0, 0, time.UTC)
rows := []trafficBucketRow{
{Bucket: bucket1, ModelName: "a", Cnt: 5, TokensIn: 10, TokensOut: 20},
{Bucket: bucket1, ModelName: "b", Cnt: 3, TokensIn: 6, TokensOut: 12},
{Bucket: bucket2, ModelName: "a", Cnt: 2, TokensIn: 4, TokensOut: 8},
{Bucket: bucket2, ModelName: "c", Cnt: 8, TokensIn: 16, TokensOut: 32},
}
resp := buildTrafficChartSeriesResponse(rows, 2, "hour", bucket1, bucket2)
if len(resp.X.Labels) != 2 || len(resp.X.Timestamps) != 2 {
t.Fatalf("expected 2 time buckets, got labels=%d timestamps=%d", len(resp.X.Labels), len(resp.X.Timestamps))
}
if resp.X.Labels[0] != bucket1.Format(time.RFC3339) || resp.X.Labels[1] != bucket2.Format(time.RFC3339) {
t.Fatalf("unexpected labels: %+v", resp.X.Labels)
}
if resp.X.Timestamps[0] != bucket1.Unix() || resp.X.Timestamps[1] != bucket2.Unix() {
t.Fatalf("unexpected timestamps: %+v", resp.X.Timestamps)
}
if resp.X.Totals.Data[0] != 8 || resp.X.Totals.Data[1] != 10 {
t.Fatalf("unexpected totals data: %+v", resp.X.Totals.Data)
}
if resp.X.Totals.TokensIn[0] != 16 || resp.X.Totals.TokensIn[1] != 20 {
t.Fatalf("unexpected totals tokens_in: %+v", resp.X.Totals.TokensIn)
}
if resp.X.Totals.TokensOut[0] != 32 || resp.X.Totals.TokensOut[1] != 40 {
t.Fatalf("unexpected totals tokens_out: %+v", resp.X.Totals.TokensOut)
}
seriesByName := make(map[string]TrafficSeries, len(resp.Series))
for _, s := range resp.Series {
seriesByName[s.Name] = s
}
for _, name := range []string{"a", "c", "other"} {
if _, ok := seriesByName[name]; !ok {
t.Fatalf("missing series %q", name)
}
}
aSeries := seriesByName["a"]
if aSeries.Data[0] != 5 || aSeries.Data[1] != 2 {
t.Fatalf("unexpected series a data: %+v", aSeries.Data)
}
if aSeries.TokensIn[0] != 10 || aSeries.TokensIn[1] != 4 {
t.Fatalf("unexpected series a tokens_in: %+v", aSeries.TokensIn)
}
if aSeries.TokensOut[0] != 20 || aSeries.TokensOut[1] != 8 {
t.Fatalf("unexpected series a tokens_out: %+v", aSeries.TokensOut)
}
cSeries := seriesByName["c"]
if cSeries.Data[0] != 0 || cSeries.Data[1] != 8 {
t.Fatalf("unexpected series c data: %+v", cSeries.Data)
}
if cSeries.TokensIn[0] != 0 || cSeries.TokensIn[1] != 16 {
t.Fatalf("unexpected series c tokens_in: %+v", cSeries.TokensIn)
}
if cSeries.TokensOut[0] != 0 || cSeries.TokensOut[1] != 32 {
t.Fatalf("unexpected series c tokens_out: %+v", cSeries.TokensOut)
}
otherSeries := seriesByName["other"]
if otherSeries.Data[0] != 3 || otherSeries.Data[1] != 0 {
t.Fatalf("unexpected series other data: %+v", otherSeries.Data)
}
if otherSeries.TokensIn[0] != 6 || otherSeries.TokensIn[1] != 0 {
t.Fatalf("unexpected series other tokens_in: %+v", otherSeries.TokensIn)
}
if otherSeries.TokensOut[0] != 12 || otherSeries.TokensOut[1] != 0 {
t.Fatalf("unexpected series other tokens_out: %+v", otherSeries.TokensOut)
}
}
func TestTrafficChart_TopNOtherAggregation(t *testing.T) {
// Skip test when running with SQLite (no DATE_TRUNC support)
// This test requires PostgreSQL for time truncation functions