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 状态筛选 ### 6.2 API Key 状态筛选
`GET /admin/api-keys?status=active|suspended|auto_disabled|manual_disabled` `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": { "/admin/keys/{id}/access": {
"get": { "get": {
"security": [ "security": [
@@ -1623,7 +1889,7 @@ const docTemplate = `{
"AdminAuth": [] "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": [ "produces": [
"application/json" "application/json"
], ],
@@ -3528,7 +3794,7 @@ const docTemplate = `{
"MasterAuth": [] "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": [ "produces": [
"application/json" "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": { "internal_api.CreateMasterRequest": {
"type": "object", "type": "object",
"required": [ "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": { "internal_api.IssueChildKeyRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -5938,42 +6253,36 @@ const docTemplate = `{
} }
} }
}, },
"internal_api.TrafficBucket": { "internal_api.TrafficChartAxis": {
"type": "object", "type": "object",
"properties": { "properties": {
"breakdown": { "labels": {
"type": "object", "type": "array",
"additionalProperties": { "items": {
"$ref": "#/definitions/internal_api.TrafficMetrics" "type": "string"
} }
}, },
"time": { "timestamps": {
"type": "string" "type": "array",
"items": {
"type": "integer"
}
}, },
"timestamp": { "totals": {
"type": "integer" "$ref": "#/definitions/internal_api.TrafficTotals"
},
"total": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
} }
} }
}, },
"internal_api.TrafficChartResponse": { "internal_api.TrafficChartResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"buckets": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.TrafficBucket"
}
},
"granularity": { "granularity": {
"type": "string" "type": "string"
}, },
"models": { "series": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/internal_api.TrafficSeries"
} }
}, },
"since": { "since": {
@@ -5981,20 +6290,58 @@ const docTemplate = `{
}, },
"until": { "until": {
"type": "integer" "type": "integer"
},
"x": {
"$ref": "#/definitions/internal_api.TrafficChartAxis"
} }
} }
}, },
"internal_api.TrafficMetrics": { "internal_api.TrafficSeries": {
"type": "object", "type": "object",
"properties": { "properties": {
"count": { "data": {
"type": "integer" "type": "array",
"items": {
"type": "integer"
}
},
"name": {
"type": "string"
}, },
"tokens_in": { "tokens_in": {
"type": "integer" "type": "array",
"items": {
"type": "integer"
}
}, },
"tokens_out": { "tokens_out": {
"type": "integer" "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"
}
} }
} }
}, },
@@ -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": { "internal_api.UpdateMasterRequest": {
"type": "object", "type": "object",
"properties": { "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": { "internal_api.WhoamiResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"allow_ips": {
"description": "IP whitelist (for diagnostics)",
"type": "string",
"example": ""
},
"created_at": { "created_at": {
"type": "integer", "type": "integer",
"example": 1703505600 "example": 1703505600
@@ -6126,10 +6527,20 @@ const docTemplate = `{
"type": "string", "type": "string",
"example": "default" "example": "default"
}, },
"deny_ips": {
"description": "IP blacklist (for diagnostics)",
"type": "string",
"example": ""
},
"epoch": { "epoch": {
"type": "integer", "type": "integer",
"example": 1 "example": 1
}, },
"expires_at": {
"description": "Expiration timestamp (0 = never)",
"type": "integer",
"example": 0
},
"global_qps": { "global_qps": {
"type": "integer", "type": "integer",
"example": 100 "example": 100
@@ -6151,15 +6562,35 @@ const docTemplate = `{
"type": "string", "type": "string",
"example": "master" "example": "master"
}, },
"last_accessed_at": {
"description": "Last access timestamp",
"type": "integer",
"example": 0
},
"master_id": { "master_id": {
"description": "Key fields (only present when type is \"key\")", "description": "Key fields (only present when type is \"key\")",
"type": "integer", "type": "integer",
"example": 1 "example": 1
}, },
"master_name": {
"description": "Parent master name (for display)",
"type": "string",
"example": "tenant-a"
},
"max_child_keys": { "max_child_keys": {
"type": "integer", "type": "integer",
"example": 5 "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": { "name": {
"type": "string", "type": "string",
"example": "tenant-a" "example": "tenant-a"
@@ -6168,6 +6599,46 @@ const docTemplate = `{
"type": "string", "type": "string",
"example": "default,ns1" "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": { "role": {
"description": "Admin fields (only present when type is \"admin\")", "description": "Admin fields (only present when type is \"admin\")",
"type": "string", "type": "string",
@@ -6189,6 +6660,11 @@ const docTemplate = `{
"updated_at": { "updated_at": {
"type": "integer", "type": "integer",
"example": 1703505600 "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": { "internal_api.refreshModelRegistryRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -176,17 +176,33 @@
"granularity": "hour", "granularity": "hour",
"since": 1735689600, "since": 1735689600,
"until": 1735776000, "until": 1735776000,
"models": ["gpt-4-turbo", "claude-3.5-sonnet", "llama-3-70b", "other"], "x": {
"buckets": [ "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": [
{ {
"time": "2025-01-01T00:00:00Z", "name": "gpt-4-turbo",
"timestamp": 1735689600, "data": [1200, 900],
"breakdown": { "tokens_in": [50000, 38000],
"gpt-4-turbo": { "count": 1200, "tokens_in": 50000, "tokens_out": 80000 }, "tokens_out": [80000, 62000]
"claude-3.5-sonnet": { "count": 800, "tokens_in": 30000, "tokens_out": 45000 }, },
"other": { "count": 50, "tokens_in": 2000, "tokens_out": 3000 } {
}, "name": "claude-3.5-sonnet",
"total": { "count": 2050, "tokens_in": 82000, "tokens_out": 128000 } "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 轴 | 请求数量 | | Y 轴 | 请求数量 |
| 堆叠系列 | `models` 数组,每个模型一个系列 | | 堆叠系列 | `series` 数组,每个模型一个系列 |
| 系列数据 | `buckets[i].breakdown[model].count` | | 系列数据 | `series[i].data` |
| 总量标签 | `buckets[i].total.count` | | 总量标签 | `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 | | 时间粒度切换 | 选择分钟级范围 | 自动切换 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" | | 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": { "/admin/keys/{id}/access": {
"get": { "get": {
"security": [ "security": [
@@ -1617,7 +1883,7 @@
"AdminAuth": [] "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": [ "produces": [
"application/json" "application/json"
], ],
@@ -3522,7 +3788,7 @@
"MasterAuth": [] "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": [ "produces": [
"application/json" "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": { "internal_api.CreateMasterRequest": {
"type": "object", "type": "object",
"required": [ "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": { "internal_api.IssueChildKeyRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -5932,42 +6247,36 @@
} }
} }
}, },
"internal_api.TrafficBucket": { "internal_api.TrafficChartAxis": {
"type": "object", "type": "object",
"properties": { "properties": {
"breakdown": { "labels": {
"type": "object", "type": "array",
"additionalProperties": { "items": {
"$ref": "#/definitions/internal_api.TrafficMetrics" "type": "string"
} }
}, },
"time": { "timestamps": {
"type": "string" "type": "array",
"items": {
"type": "integer"
}
}, },
"timestamp": { "totals": {
"type": "integer" "$ref": "#/definitions/internal_api.TrafficTotals"
},
"total": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
} }
} }
}, },
"internal_api.TrafficChartResponse": { "internal_api.TrafficChartResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"buckets": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.TrafficBucket"
}
},
"granularity": { "granularity": {
"type": "string" "type": "string"
}, },
"models": { "series": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/internal_api.TrafficSeries"
} }
}, },
"since": { "since": {
@@ -5975,20 +6284,58 @@
}, },
"until": { "until": {
"type": "integer" "type": "integer"
},
"x": {
"$ref": "#/definitions/internal_api.TrafficChartAxis"
} }
} }
}, },
"internal_api.TrafficMetrics": { "internal_api.TrafficSeries": {
"type": "object", "type": "object",
"properties": { "properties": {
"count": { "data": {
"type": "integer" "type": "array",
"items": {
"type": "integer"
}
},
"name": {
"type": "string"
}, },
"tokens_in": { "tokens_in": {
"type": "integer" "type": "array",
"items": {
"type": "integer"
}
}, },
"tokens_out": { "tokens_out": {
"type": "integer" "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"
}
} }
} }
}, },
@@ -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": { "internal_api.UpdateMasterRequest": {
"type": "object", "type": "object",
"properties": { "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": { "internal_api.WhoamiResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"allow_ips": {
"description": "IP whitelist (for diagnostics)",
"type": "string",
"example": ""
},
"created_at": { "created_at": {
"type": "integer", "type": "integer",
"example": 1703505600 "example": 1703505600
@@ -6120,10 +6521,20 @@
"type": "string", "type": "string",
"example": "default" "example": "default"
}, },
"deny_ips": {
"description": "IP blacklist (for diagnostics)",
"type": "string",
"example": ""
},
"epoch": { "epoch": {
"type": "integer", "type": "integer",
"example": 1 "example": 1
}, },
"expires_at": {
"description": "Expiration timestamp (0 = never)",
"type": "integer",
"example": 0
},
"global_qps": { "global_qps": {
"type": "integer", "type": "integer",
"example": 100 "example": 100
@@ -6145,15 +6556,35 @@
"type": "string", "type": "string",
"example": "master" "example": "master"
}, },
"last_accessed_at": {
"description": "Last access timestamp",
"type": "integer",
"example": 0
},
"master_id": { "master_id": {
"description": "Key fields (only present when type is \"key\")", "description": "Key fields (only present when type is \"key\")",
"type": "integer", "type": "integer",
"example": 1 "example": 1
}, },
"master_name": {
"description": "Parent master name (for display)",
"type": "string",
"example": "tenant-a"
},
"max_child_keys": { "max_child_keys": {
"type": "integer", "type": "integer",
"example": 5 "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": { "name": {
"type": "string", "type": "string",
"example": "tenant-a" "example": "tenant-a"
@@ -6162,6 +6593,46 @@
"type": "string", "type": "string",
"example": "default,ns1" "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": { "role": {
"description": "Admin fields (only present when type is \"admin\")", "description": "Admin fields (only present when type is \"admin\")",
"type": "string", "type": "string",
@@ -6183,6 +6654,11 @@
"updated_at": { "updated_at": {
"type": "integer", "type": "integer",
"example": 1703505600 "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": { "internal_api.refreshModelRegistryRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -562,6 +562,17 @@ definitions:
- title - title
- type - type
type: object type: object
internal_api.CreateIPBanRequest:
properties:
cidr:
type: string
expires_at:
type: integer
reason:
type: string
required:
- cidr
type: object
internal_api.CreateMasterRequest: internal_api.CreateMasterRequest:
properties: properties:
global_qps: global_qps:
@@ -661,6 +672,27 @@ definitions:
$ref: '#/definitions/internal_api.GroupedStatsItem' $ref: '#/definitions/internal_api.GroupedStatsItem'
type: array type: array
type: object 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: internal_api.IssueChildKeyRequest:
properties: properties:
allow_ips: allow_ips:
@@ -1070,44 +1102,65 @@ definitions:
tokens: tokens:
type: integer type: integer
type: object type: object
internal_api.TrafficBucket: internal_api.TrafficChartAxis:
properties: properties:
breakdown: labels:
additionalProperties: items:
$ref: '#/definitions/internal_api.TrafficMetrics' type: string
type: object type: array
time: timestamps:
type: string items:
timestamp: type: integer
type: integer type: array
total: totals:
$ref: '#/definitions/internal_api.TrafficMetrics' $ref: '#/definitions/internal_api.TrafficTotals'
type: object type: object
internal_api.TrafficChartResponse: internal_api.TrafficChartResponse:
properties: properties:
buckets:
items:
$ref: '#/definitions/internal_api.TrafficBucket'
type: array
granularity: granularity:
type: string type: string
models: series:
items: items:
type: string $ref: '#/definitions/internal_api.TrafficSeries'
type: array type: array
since: since:
type: integer type: integer
until: until:
type: integer type: integer
x:
$ref: '#/definitions/internal_api.TrafficChartAxis'
type: object type: object
internal_api.TrafficMetrics: internal_api.TrafficSeries:
properties: properties:
count: data:
type: integer items:
type: integer
type: array
name:
type: string
tokens_in: tokens_in:
type: integer items:
type: integer
type: array
tokens_out: tokens_out:
type: integer 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 type: object
internal_api.TrendInfo: internal_api.TrendInfo:
properties: properties:
@@ -1146,6 +1199,15 @@ definitions:
min_tpm_tokens_1m: min_tpm_tokens_1m:
type: integer type: integer
type: object type: object
internal_api.UpdateIPBanRequest:
properties:
expires_at:
$ref: '#/definitions/internal_api.optionalInt64'
reason:
type: string
status:
type: string
type: object
internal_api.UpdateMasterRequest: internal_api.UpdateMasterRequest:
properties: properties:
global_qps: global_qps:
@@ -1186,17 +1248,56 @@ definitions:
description: active/suspended description: active/suspended
type: string type: string
type: object 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: internal_api.WhoamiResponse:
properties: properties:
allow_ips:
description: IP whitelist (for diagnostics)
example: ""
type: string
created_at: created_at:
example: 1703505600 example: 1703505600
type: integer type: integer
default_namespace: default_namespace:
example: default example: default
type: string type: string
deny_ips:
description: IP blacklist (for diagnostics)
example: ""
type: string
epoch: epoch:
example: 1 example: 1
type: integer type: integer
expires_at:
description: Expiration timestamp (0 = never)
example: 0
type: integer
global_qps: global_qps:
example: 100 example: 100
type: integer type: integer
@@ -1213,19 +1314,64 @@ definitions:
issued_by: issued_by:
example: master example: master
type: string type: string
last_accessed_at:
description: Last access timestamp
example: 0
type: integer
master_id: master_id:
description: Key fields (only present when type is "key") description: Key fields (only present when type is "key")
example: 1 example: 1
type: integer type: integer
master_name:
description: Parent master name (for display)
example: tenant-a
type: string
max_child_keys: max_child_keys:
example: 5 example: 5
type: integer 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: name:
example: tenant-a example: tenant-a
type: string type: string
namespaces: namespaces:
example: default,ns1 example: default,ns1
type: string 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: role:
description: Admin fields (only present when type is "admin") description: Admin fields (only present when type is "admin")
example: admin example: admin
@@ -1243,6 +1389,10 @@ definitions:
updated_at: updated_at:
example: 1703505600 example: 1703505600
type: integer type: integer
used_tokens:
description: Total tokens used (from DB)
example: 0
type: integer
type: object type: object
internal_api.apiKeyStatsFlushEntry: internal_api.apiKeyStatsFlushEntry:
properties: properties:
@@ -1260,6 +1410,14 @@ definitions:
$ref: '#/definitions/internal_api.apiKeyStatsFlushEntry' $ref: '#/definitions/internal_api.apiKeyStatsFlushEntry'
type: array type: array
type: object type: object
internal_api.optionalInt64:
properties:
set:
type: boolean
value:
format: int64
type: integer
type: object
internal_api.refreshModelRegistryRequest: internal_api.refreshModelRegistryRequest:
properties: properties:
ref: ref:
@@ -2197,6 +2355,177 @@ paths:
summary: Update feature flags summary: Update feature flags
tags: tags:
- admin - 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: /admin/keys/{id}/access:
get: get:
description: Returns key default_namespace and namespaces description: Returns key default_namespace and namespaces
@@ -2403,7 +2732,8 @@ paths:
/admin/logs/stats/traffic-chart: /admin/logs/stats/traffic-chart:
get: get:
description: Get time × model aggregated data for stacked traffic charts. Returns 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: parameters:
- description: 'Time granularity: hour (default) or minute' - description: 'Time granularity: hour (default) or minute'
enum: enum:
@@ -3615,13 +3945,41 @@ paths:
/auth/whoami: /auth/whoami:
get: get:
description: |- 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. 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: **Response varies by token type:**
- Admin Token: {"type": "admin", "role": "admin"}
- Master Key: {"type": "master", "id": 1, "name": "...", ...} **Admin Token:**
- Child Key: {"type": "key", "id": 5, "master_id": 1, "issued_by": "master", ...} - 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: produces:
- application/json - application/json
responses: responses:

View File

@@ -1183,7 +1183,7 @@ const docTemplate = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "time period: today, week, month, all", "description": "time period: today, week, month, last7d, last30d, all",
"name": "period", "name": "period",
"in": "query" "in": "query"
}, },
@@ -1198,6 +1198,12 @@ const docTemplate = `{
"description": "unix seconds", "description": "unix seconds",
"name": "until", "name": "until",
"in": "query" "in": "query"
},
{
"type": "boolean",
"description": "include trend data comparing to previous period",
"name": "include_trends",
"in": "query"
} }
], ],
"responses": { "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": { "/admin/keys/{id}/access": {
"get": { "get": {
"security": [ "security": [
@@ -1617,7 +1889,7 @@ const docTemplate = `{
"AdminAuth": [] "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": [ "produces": [
"application/json" "application/json"
], ],
@@ -3522,7 +3794,7 @@ const docTemplate = `{
"MasterAuth": [] "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": [ "produces": [
"application/json" "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": { "internal_api.CreateMasterRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -5208,11 +5497,36 @@ const docTemplate = `{
"$ref": "#/definitions/internal_api.TopModelStat" "$ref": "#/definitions/internal_api.TopModelStat"
} }
}, },
"trends": {
"description": "Only present when include_trends=true",
"allOf": [
{
"$ref": "#/definitions/internal_api.DashboardTrends"
}
]
},
"updated_at": { "updated_at": {
"type": "integer" "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": { "internal_api.DeleteLogsRequest": {
"type": "object", "type": "object",
"properties": { "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": { "internal_api.IssueChildKeyRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -5907,42 +6253,36 @@ const docTemplate = `{
} }
} }
}, },
"internal_api.TrafficBucket": { "internal_api.TrafficChartAxis": {
"type": "object", "type": "object",
"properties": { "properties": {
"breakdown": { "labels": {
"type": "object", "type": "array",
"additionalProperties": { "items": {
"$ref": "#/definitions/internal_api.TrafficMetrics" "type": "string"
} }
}, },
"time": { "timestamps": {
"type": "string" "type": "array",
"items": {
"type": "integer"
}
}, },
"timestamp": { "totals": {
"type": "integer" "$ref": "#/definitions/internal_api.TrafficTotals"
},
"total": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
} }
} }
}, },
"internal_api.TrafficChartResponse": { "internal_api.TrafficChartResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"buckets": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.TrafficBucket"
}
},
"granularity": { "granularity": {
"type": "string" "type": "string"
}, },
"models": { "series": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/internal_api.TrafficSeries"
} }
}, },
"since": { "since": {
@@ -5950,20 +6290,71 @@ const docTemplate = `{
}, },
"until": { "until": {
"type": "integer" "type": "integer"
},
"x": {
"$ref": "#/definitions/internal_api.TrafficChartAxis"
} }
} }
}, },
"internal_api.TrafficMetrics": { "internal_api.TrafficSeries": {
"type": "object", "type": "object",
"properties": { "properties": {
"count": { "data": {
"type": "integer" "type": "array",
"items": {
"type": "integer"
}
},
"name": {
"type": "string"
}, },
"tokens_in": { "tokens_in": {
"type": "integer" "type": "array",
"items": {
"type": "integer"
}
}, },
"tokens_out": { "tokens_out": {
"type": "integer" "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"
} }
} }
}, },
@@ -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": { "internal_api.UpdateMasterRequest": {
"type": "object", "type": "object",
"properties": { "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": { "internal_api.WhoamiResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"allow_ips": {
"description": "IP whitelist (for diagnostics)",
"type": "string",
"example": ""
},
"created_at": { "created_at": {
"type": "integer", "type": "integer",
"example": 1703505600 "example": 1703505600
@@ -6082,10 +6527,20 @@ const docTemplate = `{
"type": "string", "type": "string",
"example": "default" "example": "default"
}, },
"deny_ips": {
"description": "IP blacklist (for diagnostics)",
"type": "string",
"example": ""
},
"epoch": { "epoch": {
"type": "integer", "type": "integer",
"example": 1 "example": 1
}, },
"expires_at": {
"description": "Expiration timestamp (0 = never)",
"type": "integer",
"example": 0
},
"global_qps": { "global_qps": {
"type": "integer", "type": "integer",
"example": 100 "example": 100
@@ -6107,15 +6562,35 @@ const docTemplate = `{
"type": "string", "type": "string",
"example": "master" "example": "master"
}, },
"last_accessed_at": {
"description": "Last access timestamp",
"type": "integer",
"example": 0
},
"master_id": { "master_id": {
"description": "Key fields (only present when type is \"key\")", "description": "Key fields (only present when type is \"key\")",
"type": "integer", "type": "integer",
"example": 1 "example": 1
}, },
"master_name": {
"description": "Parent master name (for display)",
"type": "string",
"example": "tenant-a"
},
"max_child_keys": { "max_child_keys": {
"type": "integer", "type": "integer",
"example": 5 "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": { "name": {
"type": "string", "type": "string",
"example": "tenant-a" "example": "tenant-a"
@@ -6124,6 +6599,46 @@ const docTemplate = `{
"type": "string", "type": "string",
"example": "default,ns1" "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": { "role": {
"description": "Admin fields (only present when type is \"admin\")", "description": "Admin fields (only present when type is \"admin\")",
"type": "string", "type": "string",
@@ -6145,6 +6660,11 @@ const docTemplate = `{
"updated_at": { "updated_at": {
"type": "integer", "type": "integer",
"example": 1703505600 "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": { "internal_api.refreshModelRegistryRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -1177,7 +1177,7 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "time period: today, week, month, all", "description": "time period: today, week, month, last7d, last30d, all",
"name": "period", "name": "period",
"in": "query" "in": "query"
}, },
@@ -1192,6 +1192,12 @@
"description": "unix seconds", "description": "unix seconds",
"name": "until", "name": "until",
"in": "query" "in": "query"
},
{
"type": "boolean",
"description": "include trend data comparing to previous period",
"name": "include_trends",
"in": "query"
} }
], ],
"responses": { "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": { "/admin/keys/{id}/access": {
"get": { "get": {
"security": [ "security": [
@@ -1611,7 +1883,7 @@
"AdminAuth": [] "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": [ "produces": [
"application/json" "application/json"
], ],
@@ -3516,7 +3788,7 @@
"MasterAuth": [] "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": [ "produces": [
"application/json" "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": { "internal_api.CreateMasterRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -5202,11 +5491,36 @@
"$ref": "#/definitions/internal_api.TopModelStat" "$ref": "#/definitions/internal_api.TopModelStat"
} }
}, },
"trends": {
"description": "Only present when include_trends=true",
"allOf": [
{
"$ref": "#/definitions/internal_api.DashboardTrends"
}
]
},
"updated_at": { "updated_at": {
"type": "integer" "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": { "internal_api.DeleteLogsRequest": {
"type": "object", "type": "object",
"properties": { "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": { "internal_api.IssueChildKeyRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -5901,42 +6247,36 @@
} }
} }
}, },
"internal_api.TrafficBucket": { "internal_api.TrafficChartAxis": {
"type": "object", "type": "object",
"properties": { "properties": {
"breakdown": { "labels": {
"type": "object", "type": "array",
"additionalProperties": { "items": {
"$ref": "#/definitions/internal_api.TrafficMetrics" "type": "string"
} }
}, },
"time": { "timestamps": {
"type": "string" "type": "array",
"items": {
"type": "integer"
}
}, },
"timestamp": { "totals": {
"type": "integer" "$ref": "#/definitions/internal_api.TrafficTotals"
},
"total": {
"$ref": "#/definitions/internal_api.TrafficMetrics"
} }
} }
}, },
"internal_api.TrafficChartResponse": { "internal_api.TrafficChartResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"buckets": {
"type": "array",
"items": {
"$ref": "#/definitions/internal_api.TrafficBucket"
}
},
"granularity": { "granularity": {
"type": "string" "type": "string"
}, },
"models": { "series": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/internal_api.TrafficSeries"
} }
}, },
"since": { "since": {
@@ -5944,20 +6284,71 @@
}, },
"until": { "until": {
"type": "integer" "type": "integer"
},
"x": {
"$ref": "#/definitions/internal_api.TrafficChartAxis"
} }
} }
}, },
"internal_api.TrafficMetrics": { "internal_api.TrafficSeries": {
"type": "object", "type": "object",
"properties": { "properties": {
"count": { "data": {
"type": "integer" "type": "array",
"items": {
"type": "integer"
}
},
"name": {
"type": "string"
}, },
"tokens_in": { "tokens_in": {
"type": "integer" "type": "array",
"items": {
"type": "integer"
}
}, },
"tokens_out": { "tokens_out": {
"type": "integer" "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"
} }
} }
}, },
@@ -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": { "internal_api.UpdateMasterRequest": {
"type": "object", "type": "object",
"properties": { "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": { "internal_api.WhoamiResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"allow_ips": {
"description": "IP whitelist (for diagnostics)",
"type": "string",
"example": ""
},
"created_at": { "created_at": {
"type": "integer", "type": "integer",
"example": 1703505600 "example": 1703505600
@@ -6076,10 +6521,20 @@
"type": "string", "type": "string",
"example": "default" "example": "default"
}, },
"deny_ips": {
"description": "IP blacklist (for diagnostics)",
"type": "string",
"example": ""
},
"epoch": { "epoch": {
"type": "integer", "type": "integer",
"example": 1 "example": 1
}, },
"expires_at": {
"description": "Expiration timestamp (0 = never)",
"type": "integer",
"example": 0
},
"global_qps": { "global_qps": {
"type": "integer", "type": "integer",
"example": 100 "example": 100
@@ -6101,15 +6556,35 @@
"type": "string", "type": "string",
"example": "master" "example": "master"
}, },
"last_accessed_at": {
"description": "Last access timestamp",
"type": "integer",
"example": 0
},
"master_id": { "master_id": {
"description": "Key fields (only present when type is \"key\")", "description": "Key fields (only present when type is \"key\")",
"type": "integer", "type": "integer",
"example": 1 "example": 1
}, },
"master_name": {
"description": "Parent master name (for display)",
"type": "string",
"example": "tenant-a"
},
"max_child_keys": { "max_child_keys": {
"type": "integer", "type": "integer",
"example": 5 "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": { "name": {
"type": "string", "type": "string",
"example": "tenant-a" "example": "tenant-a"
@@ -6118,6 +6593,46 @@
"type": "string", "type": "string",
"example": "default,ns1" "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": { "role": {
"description": "Admin fields (only present when type is \"admin\")", "description": "Admin fields (only present when type is \"admin\")",
"type": "string", "type": "string",
@@ -6139,6 +6654,11 @@
"updated_at": { "updated_at": {
"type": "integer", "type": "integer",
"example": 1703505600 "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": { "internal_api.refreshModelRegistryRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -562,6 +562,17 @@ definitions:
- title - title
- type - type
type: object type: object
internal_api.CreateIPBanRequest:
properties:
cidr:
type: string
expires_at:
type: integer
reason:
type: string
required:
- cidr
type: object
internal_api.CreateMasterRequest: internal_api.CreateMasterRequest:
properties: properties:
global_qps: global_qps:
@@ -596,9 +607,24 @@ definitions:
items: items:
$ref: '#/definitions/internal_api.TopModelStat' $ref: '#/definitions/internal_api.TopModelStat'
type: array type: array
trends:
allOf:
- $ref: '#/definitions/internal_api.DashboardTrends'
description: Only present when include_trends=true
updated_at: updated_at:
type: integer type: integer
type: object 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: internal_api.DeleteLogsRequest:
properties: properties:
before: before:
@@ -646,6 +672,27 @@ definitions:
$ref: '#/definitions/internal_api.GroupedStatsItem' $ref: '#/definitions/internal_api.GroupedStatsItem'
type: array type: array
type: object 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: internal_api.IssueChildKeyRequest:
properties: properties:
allow_ips: allow_ips:
@@ -1055,44 +1102,74 @@ definitions:
tokens: tokens:
type: integer type: integer
type: object type: object
internal_api.TrafficBucket: internal_api.TrafficChartAxis:
properties: properties:
breakdown: labels:
additionalProperties: items:
$ref: '#/definitions/internal_api.TrafficMetrics' type: string
type: object type: array
time: timestamps:
type: string items:
timestamp: type: integer
type: integer type: array
total: totals:
$ref: '#/definitions/internal_api.TrafficMetrics' $ref: '#/definitions/internal_api.TrafficTotals'
type: object type: object
internal_api.TrafficChartResponse: internal_api.TrafficChartResponse:
properties: properties:
buckets:
items:
$ref: '#/definitions/internal_api.TrafficBucket'
type: array
granularity: granularity:
type: string type: string
models: series:
items: items:
type: string $ref: '#/definitions/internal_api.TrafficSeries'
type: array type: array
since: since:
type: integer type: integer
until: until:
type: integer type: integer
x:
$ref: '#/definitions/internal_api.TrafficChartAxis'
type: object type: object
internal_api.TrafficMetrics: internal_api.TrafficSeries:
properties: properties:
count: data:
type: integer items:
type: integer
type: array
name:
type: string
tokens_in: tokens_in:
type: integer items:
type: integer
type: array
tokens_out: tokens_out:
type: integer 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 type: object
internal_api.UpdateAccessRequest: internal_api.UpdateAccessRequest:
properties: properties:
@@ -1122,6 +1199,15 @@ definitions:
min_tpm_tokens_1m: min_tpm_tokens_1m:
type: integer type: integer
type: object type: object
internal_api.UpdateIPBanRequest:
properties:
expires_at:
$ref: '#/definitions/internal_api.optionalInt64'
reason:
type: string
status:
type: string
type: object
internal_api.UpdateMasterRequest: internal_api.UpdateMasterRequest:
properties: properties:
global_qps: global_qps:
@@ -1162,17 +1248,56 @@ definitions:
description: active/suspended description: active/suspended
type: string type: string
type: object 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: internal_api.WhoamiResponse:
properties: properties:
allow_ips:
description: IP whitelist (for diagnostics)
example: ""
type: string
created_at: created_at:
example: 1703505600 example: 1703505600
type: integer type: integer
default_namespace: default_namespace:
example: default example: default
type: string type: string
deny_ips:
description: IP blacklist (for diagnostics)
example: ""
type: string
epoch: epoch:
example: 1 example: 1
type: integer type: integer
expires_at:
description: Expiration timestamp (0 = never)
example: 0
type: integer
global_qps: global_qps:
example: 100 example: 100
type: integer type: integer
@@ -1189,19 +1314,64 @@ definitions:
issued_by: issued_by:
example: master example: master
type: string type: string
last_accessed_at:
description: Last access timestamp
example: 0
type: integer
master_id: master_id:
description: Key fields (only present when type is "key") description: Key fields (only present when type is "key")
example: 1 example: 1
type: integer type: integer
master_name:
description: Parent master name (for display)
example: tenant-a
type: string
max_child_keys: max_child_keys:
example: 5 example: 5
type: integer 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: name:
example: tenant-a example: tenant-a
type: string type: string
namespaces: namespaces:
example: default,ns1 example: default,ns1
type: string 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: role:
description: Admin fields (only present when type is "admin") description: Admin fields (only present when type is "admin")
example: admin example: admin
@@ -1219,6 +1389,10 @@ definitions:
updated_at: updated_at:
example: 1703505600 example: 1703505600
type: integer type: integer
used_tokens:
description: Total tokens used (from DB)
example: 0
type: integer
type: object type: object
internal_api.apiKeyStatsFlushEntry: internal_api.apiKeyStatsFlushEntry:
properties: properties:
@@ -1236,6 +1410,14 @@ definitions:
$ref: '#/definitions/internal_api.apiKeyStatsFlushEntry' $ref: '#/definitions/internal_api.apiKeyStatsFlushEntry'
type: array type: array
type: object type: object
internal_api.optionalInt64:
properties:
set:
type: boolean
value:
format: int64
type: integer
type: object
internal_api.refreshModelRegistryRequest: internal_api.refreshModelRegistryRequest:
properties: properties:
ref: ref:
@@ -2086,7 +2268,7 @@ paths:
description: Returns aggregated metrics for dashboard display including requests, description: Returns aggregated metrics for dashboard display including requests,
tokens, latency, masters, keys, and provider keys statistics tokens, latency, masters, keys, and provider keys statistics
parameters: parameters:
- description: 'time period: today, week, month, all' - description: 'time period: today, week, month, last7d, last30d, all'
in: query in: query
name: period name: period
type: string type: string
@@ -2098,6 +2280,10 @@ paths:
in: query in: query
name: until name: until
type: integer type: integer
- description: include trend data comparing to previous period
in: query
name: include_trends
type: boolean
produces: produces:
- application/json - application/json
responses: responses:
@@ -2169,6 +2355,177 @@ paths:
summary: Update feature flags summary: Update feature flags
tags: tags:
- admin - 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: /admin/keys/{id}/access:
get: get:
description: Returns key default_namespace and namespaces description: Returns key default_namespace and namespaces
@@ -2375,7 +2732,8 @@ paths:
/admin/logs/stats/traffic-chart: /admin/logs/stats/traffic-chart:
get: get:
description: Get time × model aggregated data for stacked traffic charts. Returns 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: parameters:
- description: 'Time granularity: hour (default) or minute' - description: 'Time granularity: hour (default) or minute'
enum: enum:
@@ -3587,13 +3945,41 @@ paths:
/auth/whoami: /auth/whoami:
get: get:
description: |- 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. 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: **Response varies by token type:**
- Admin Token: {"type": "admin", "role": "admin"}
- Master Key: {"type": "master", "id": 1, "name": "...", ...} **Admin Token:**
- Child Key: {"type": "key", "id": 5, "master_id": 1, "issued_by": "master", ...} - 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: produces:
- application/json - application/json
responses: responses:

View File

@@ -609,34 +609,35 @@ func (h *Handler) logStatsByMinute(c *gin.Context, q *gorm.DB, sinceTime, untilT
c.JSON(http.StatusOK, GroupedStatsResponse{Items: items}) c.JSON(http.StatusOK, GroupedStatsResponse{Items: items})
} }
// TrafficMetrics contains the metrics for a model in a bucket // TrafficSeries contains the metrics for a model aligned to the shared time axis.
type TrafficMetrics struct { type TrafficSeries struct {
Count int64 `json:"count"` Name string `json:"name"`
TokensIn int64 `json:"tokens_in"` Data []int64 `json:"data"`
TokensOut int64 `json:"tokens_out"` TokensIn []int64 `json:"tokens_in"`
TokensOut []int64 `json:"tokens_out"`
} }
// ModelMetricsMap is a map from model name to TrafficMetrics. // TrafficTotals contains aggregated totals aligned to the shared time axis.
// Keys are model names (e.g. "gpt-4", "claude-3-opus") or "other" for aggregated remaining models. type TrafficTotals struct {
// Example: {"gpt-4": {"count": 10, "tokens_in": 1000, "tokens_out": 500}, "other": {"count": 3, "tokens_in": 200, "tokens_out": 100}} Data []int64 `json:"data"`
type ModelMetricsMap map[string]TrafficMetrics TokensIn []int64 `json:"tokens_in"`
TokensOut []int64 `json:"tokens_out"`
}
// TrafficBucket represents one time bucket with model breakdown // TrafficChartAxis defines the shared time axis for chart data.
type TrafficBucket struct { type TrafficChartAxis struct {
Time string `json:"time"` Labels []string `json:"labels"`
Timestamp int64 `json:"timestamp"` Timestamps []int64 `json:"timestamps"`
// Breakdown is a map from model name (e.g. "gpt-4", "claude-3-opus", "other") to its metrics Totals TrafficTotals `json:"totals"`
Breakdown ModelMetricsMap `json:"breakdown"`
Total TrafficMetrics `json:"total"`
} }
// TrafficChartResponse is the response for traffic chart API // TrafficChartResponse is the response for traffic chart API
type TrafficChartResponse struct { type TrafficChartResponse struct {
Granularity string `json:"granularity"` Granularity string `json:"granularity"`
Since int64 `json:"since"` Since int64 `json:"since"`
Until int64 `json:"until"` Until int64 `json:"until"`
Models []string `json:"models"` X TrafficChartAxis `json:"x"`
Buckets []TrafficBucket `json:"buckets"` Series []TrafficSeries `json:"series"`
} }
const ( const (
@@ -644,9 +645,121 @@ const (
maxTrafficTopN = 20 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 // GetTrafficChart godoc
// @Summary Traffic chart data (admin) // @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 // @Tags admin
// @Produce json // @Produce json
// @Security AdminAuth // @Security AdminAuth
@@ -731,14 +844,7 @@ func (h *Handler) GetTrafficChart(c *gin.Context) {
truncFunc = "DATE_TRUNC('hour', created_at)" truncFunc = "DATE_TRUNC('hour', created_at)"
} }
type bucketModelStats struct { var rows []trafficBucketRow
Bucket time.Time
ModelName string
Cnt int64
TokensIn int64
TokensOut int64
}
var rows []bucketModelStats
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"). 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"). Group("bucket, model_name").
@@ -748,88 +854,7 @@ func (h *Handler) GetTrafficChart(c *gin.Context) {
return return
} }
// Calculate global model counts for top_n selection c.JSON(http.StatusOK, buildTrafficChartSeriesResponse(rows, topN, granularity, sinceTime, untilTime))
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,
})
} }
// ListSelfLogs godoc // 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) { func TestTrafficChart_TopNOtherAggregation(t *testing.T) {
// Skip test when running with SQLite (no DATE_TRUNC support) // Skip test when running with SQLite (no DATE_TRUNC support)
// This test requires PostgreSQL for time truncation functions // This test requires PostgreSQL for time truncation functions