Low Balance: ¥{{ (user.balance / 1e6).toFixed(4) }} — your balance is below the alert threshold of ¥{{ (balanceThreshold / 1e6).toFixed(2) }}.
Total Tokens
{{ (usageSummary.total_tokens || 0).toLocaleString() }}
Total Cost
¥{{ (usageSummary.total_cost / 1e6 || 0).toFixed(4) }}
Requests
{{ usageSummary.requests || 0 }}
Loading chart data...
API Calls by Model ({{ chartRange }}d)
Total Tokens ({{ chartRange }}d)
Balance Usage
Total Requests ({{ chartRange }}d)
Access Monitor
Active Now
{{ mActive || 0 }}
Today PV
{{ mTodayPV || 0 }}
Today UV
{{ mTodayUV || 0 }}
Today Peak
{{ mTodayPeak || 0 }}
Hist Peak
{{ mHistPeak || 0 }}
Yesterday PV
{{ mYestPV || 0 }}
Yes UV/Peak
{{ mYestUV }}/{{ mYestPeak }}
Web PV: {{ mWebPV }} dashboard / site
API PV: {{ mApiPV }} model API calls
Last: {{ monitorLastRefresh }}
Access Trend (24h)
Waiting for data...
Geo Distribution (Top 10)
Waiting for data...
Page Ranking
Waiting for data...
Top IPs ({{ monitorRange }}h)
#{{ i+1 }}
{{ ip.ip_address }}
{{ ip.count }} req
{{ ip.errors }} err
Abnormal
No IP data yet.
Recent Visitors
| IP | Location | Path | Time | Duration | UA | Type | Status |
|---|---|---|---|---|---|---|---|
| {{ v.ip_address }} | {{ v.country || '—' }} {{ v.province || '' }} | {{ fmtTime(v.created_at) }} | {{ v.duration_ms }}ms | {{ v.request_type }} | {{ v.status_code }} | ||
| No visitor data yet. | |||||||
Total: {{ monitorVisitorTotal }}
Security & Risk
Total Errors
{{ monitorErrors }}
{{ b.code }}
{{ b.count }}
Suspicious IPs (≥50 req or ≥10 errors)
{{ ip.ip_address }}
{{ ip.count }} req
{{ ip.errors }} err
Blocked
No suspicious activity detected.
Models
Loading models...
No models available yet. Admin needs to enable models in Settings → Chat Models.
{{ m.id.includes('/') ? m.id.split('/')[1] : m.id }}
{{ m.id.includes('/') ? m.id.split('/')[1] : m.id }}
¥{{ (m.price.input/1e6).toFixed(2) }} in / ¥{{ (m.price.output/1e6).toFixed(2) }} out
{{ detailModel.id.includes('/') ? detailModel.id.split('/')[1] : detailModel.id }}
Model: {{ detailModel.id }}
Provider: {{ detailModel.provider || '—' }}
Pricing (per 1M tokens)
Input: ¥{{ (detailModel.price.input/1e6).toFixed(4) }}
Output: ¥{{ (detailModel.price.output/1e6).toFixed(4) }}
Create API Key
API Usage
Base URL:
{{ siteBaseUrl }}
Example curl:
curl {{ siteBaseUrl }}/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "{{ detailModel.id }}",
"messages": [{"role": "user", "content": "Hello!"}]
}'
Python:
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="{{ siteBaseUrl }}"
)
response = client.chat.completions.create(
model="{{ detailModel.id }}",
messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
Usage Log
Total Requests
{{ (usageOverviewTotals.requests||0).toLocaleString() }}
Total Tokens
{{ (usageOverviewTotals.tokens||0).toLocaleString() }}
Total Cost
¥{{ ((usageOverviewTotals.cost||0)/1e6).toFixed(4) }}
Models Used
{{ usageOverviewByModel.length }}
Daily Usage Trend ({{ usageDays }}d)
By Model
| Model | Requests | Tokens | Cost (¥) | Avg Latency |
|---|---|---|---|---|
| {{ m.model }} | {{ m.requests.toLocaleString() }} | {{ m.tokens.toLocaleString() }} | {{ (m.cost/1e6).toFixed(6) }} | {{ m.avg_latency_ms ? (m.avg_latency_ms/1000).toFixed(2)+'s' : '—' }} |
By Customer
| User | Requests | Tokens | Cost (¥) |
|---|---|---|---|
| {{ u.username }} {{ u.email }} | {{ u.requests.toLocaleString() }} | {{ u.tokens.toLocaleString() }} | {{ (u.cost/1e6).toFixed(6) }} |
By Country
| Country | Requests | Tokens | Cost (¥) |
|---|---|---|---|
| {{ c.country }} | {{ c.requests.toLocaleString() }} | {{ c.tokens.toLocaleString() }} | {{ (c.cost/1e6).toFixed(6) }} |
Group by:
| {{ usageBreakdownBy==='model'?'Model':usageBreakdownBy==='key'?'API Key':usageBreakdownBy==='user'?'Customer':usageBreakdownBy==='country'?'Country':'Day' }} | Requests | Tokens | Cost (¥) | Avg Latency |
|---|---|---|---|---|
| {{ row.label }} | {{ row.requests.toLocaleString() }} | {{ row.tokens.toLocaleString() }} | {{ (row.cost/1e6).toFixed(6) }} | {{ row.avg_latency_ms ? (row.avg_latency_ms/1000).toFixed(2)+'s' : '—' }} |
| No data in this period. |
—
| Date/Time | User | Client | Upstream | IP | Country | Model | 1st Resp (s) | Flow (t/s) | Out Tokens | In Tokens | Price (¥) | Status |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ fmtDateLog(row.created_at) }} | {{ row.username || row.api_key_name }} | {{ row.api_key_name }} | {{ row.upstream_name || '—' }} | {{ row.client_ip || '—' }} | {{ row.country || '—' }} | {{ row.model }} | {{ row.latency_ms ? (row.latency_ms/1000).toFixed(2) : '—' }} | {{ row.throughput_tps }} | {{ row.completion_tokens }} | {{ row.prompt_tokens }} | {{ (row.cost / 1e6).toFixed(6) }} | {{ row.status_code }} |
| No usage records yet. |
Total records: {{ usageLogTotal }}
Export Usage Data
Period:
—
Generating...
Exports usage data grouped by the selected period. Empty date range exports all data.
The CSV includes: period label, request count, total tokens, prompt tokens, completion tokens, and cost in CNY.
Upstream Counter Total
{{ (recSummary.total_counter_tokens||0).toLocaleString() }}
UsageLog Sum Total
{{ (recSummary.total_log_tokens||0).toLocaleString() }}
Delta
{{ (recSummary.total_delta||0)>=0?'+':''}}{{ (recSummary.total_delta||0).toLocaleString() }}
Drift %
{{ (recSummary.total_delta_pct||0).toFixed(4) }}%
Compares
UpstreamAccount.total_tokens (perpetual counter) against SUM(UsageLog.total_tokens) (all-time). A non-zero delta means the two data sources have drifted apart. The time selector controls the recent-activity context columns only.
| Upstream | Provider | Status | Counter (all-time) | Log Sum (all-time) | Delta | Log Reqs (all) | Reqs ({{recDays}}d) | First Log | Last Log |
|---|---|---|---|---|---|---|---|---|---|
| {{ u.name }} | {{ u.provider }} | {{ u.status }} | {{ u.counter_tokens.toLocaleString() }} | {{ u.log_tokens.toLocaleString() }} | {{ u.delta>=0?'+':'' }}{{ u.delta.toLocaleString() }} | {{ u.log_requests.toLocaleString() }} | {{ u.log_requests_recent.toLocaleString() }} | {{ u.first_log_at ? fmtDateLog(u.first_log_at) : '—' }} | {{ u.last_log_at ? fmtDateLog(u.last_log_at) : '—' }} |
| No reconciliation data available. | |||||||||
API Management
{{ adminKeysSelected.length }} / {{ allAdminKeys.length }} selected
| k.id)" style="accent-color:var(--brand);width:14px;height:14px" /> | Name | Key | Owner | Tag | Status | Scope | Tokens | Cost (¥) | Created | Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| {{ k.name }} | {{ k.key_masked }} | {{ k.username }}{{ k.email }} | {{ k.tag }}— | {{ k.is_active ? 'Active' : 'Disabled' }} | {{ k.scope || 'All' }} | {{ k.total_tokens.toLocaleString() }} | {{ (k.total_cost/1e6).toFixed(4) }} | {{ fmtDate(k.created_at) }} |
No API keys found matching filters.
{{ k.name }}
{{ k.tag }}
{{ k.key.slice(0, 2) }}••••••••••••••••••••••••••••••••{{ k.key.slice(-4) }}
{{ k.is_active ? 'Active' : 'Disabled' }}
Monthly budget: ¥{{ (k.monthly_budget / 1e6).toFixed(2) }}
Used: ¥{{ ((k.monthly_spent || 0) / 1e6).toFixed(4) }} ({{ budgetPct(k) }}%)
Models: {{ k.allowed_models }}
IPs: {{ k.allowed_ips }}
No API keys yet. Click "+ New Key" to create one.
No API keys in the system yet.
{{ keyReport.key_name }}
Total Requests
{{ keyReport.totals.requests.toLocaleString() }}
Total Tokens
{{ keyReport.totals.tokens.toLocaleString() }}
Total Cost
¥{{ (keyReport.totals.cost / 1e6).toFixed(4) }}
Lifetime Cost
¥{{ (keyReport.total_cost / 1e6).toFixed(4) }}
Daily Usage ({{ chartRange }}d)
By Model
Model Breakdown
{{ m.model.split('/').pop() }}
{{ m.requests }} req · ¥{{ (m.cost / 1e6).toFixed(4) }}
No model data.
Recent Usage
| Time | Model | Tokens | Cost | Latency | Status |
|---|---|---|---|---|---|
| {{ l.day }} | {{ l.day }} | {{ l.tokens.toLocaleString() }} | ¥{{ (l.cost / 1e6).toFixed(6) }} | — | OK |
| No usage data in this period. | |||||
Loading report...
Billing Records
| Type | Note | Amount (¥) | Balance After (¥) | Time |
|---|---|---|---|---|
| {{ t.type === 'usage' ? 'Usage' : t.type === 'admin_topup' ? 'Top Up' : t.type }} | {{ t.note }} | {{ t.amount > 0 ? '+' : '' }}{{ (t.amount / 1e6).toFixed(6) }} | {{ (t.balance_after / 1e6).toFixed(4) }} | {{ fmtDate(t.created_at) }} |
| No billing records. | ||||
API Reference
Try Playground
Quick Start
- Get an API key from API Management
- Set the base URL to
{{ apiBase }}/v1 - Make your first request using any OpenAI-compatible SDK
- Monitor usage in the Usage Log
Authentication
All API requests require an API key passed via the
Authorization header:
Authorization: Bearer sk-your-api-key
API keys are managed in API Management.
Each key supports IP whitelisting, model access control, RPM limits, monthly budgets, and expiry dates.
Base URL
{{ apiBase }}/v1
All endpoints are prefixed with this URL. The API is fully compatible with the OpenAI API format.
Available Models
List all models available to your account. Additional models may appear when upstream providers are added.
{{ m }}
No models available.
Endpoint:
GET /v1/models
curl {{ apiBase }}/v1/models \
-H "Authorization: Bearer sk-your-api-key"
POST /v1/chat/completions
Create a chat completion. Compatible with the OpenAI Chat Completions API format.
Supports streaming via SSE (Server-Sent Events).
Request Body
| Parameter | Type | Default | Description |
|---|---|---|---|
| model | string | — | Required. Model ID to use |
| messages | array | — | Required. Array of {role, content} objects |
| stream | boolean | false | Enable SSE streaming |
| temperature | number | 1.0 | Sampling temperature (0–2) |
| max_tokens | integer | 4096 | Maximum tokens in response |
| top_p | number | 1.0 | Nucleus sampling parameter |
cURL
curl {{ apiBase }}/v1/chat/completions \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-6",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
],
"stream": false
}'
Python (OpenAI SDK)
from openai import OpenAI
client = OpenAI(
api_key="sk-your-api-key",
base_url="{{ apiBase }}/v1"
)
response = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
]
)
print(response.choices[0].message.content)
Node.js (OpenAI SDK)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "sk-your-api-key",
baseURL: "{{ apiBase }}/v1",
});
const response = await client.chat.completions.create({
model: "claude-sonnet-4-6",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hello!" },
],
});
console.log(response.choices[0].message.content);
Streaming (Python)
from openai import OpenAI
client = OpenAI(
api_key="sk-your-api-key",
base_url="{{ apiBase }}/v1"
)
stream = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "Hello!"}],
stream=True
)
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
print(delta, end="", flush=True)
Account & Usage APIs
These endpoints require your JWT session token (from the dashboard login), not your API key.
GET /api/user/balance
curl {{ apiBase }}/api/user/balance \
-H "Authorization: Bearer your-jwt-token"
GET /api/user/usage-logs?limit=50&offset=0
curl "{{ apiBase }}/api/user/usage-logs?limit=10" \
-H "Authorization: Bearer your-jwt-token"
GET /api/user/transactions?limit=20
curl "{{ apiBase }}/api/user/transactions?limit=20" \
-H "Authorization: Bearer your-jwt-token"
Error Codes
| Code | Meaning | Description |
|---|---|---|
| 401 | Unauthorized | Missing or invalid API key |
| 402 | Insufficient Balance | Account balance is too low. Top up in Billing |
| 403 | Forbidden | IP not whitelisted, key expired, or model not allowed |
| 429 | Rate Limited | RPM limit exceeded. Wait and retry |
| 502 | Upstream Error | Upstream provider returned an error. Try again later |
Rate Limits
Each API key has an RPM (Requests Per Minute) limit configurable in API Management. Default is 60 RPM.
When exceeded, the API returns 429 Too Many Requests.
To increase your limit, contact support.
Pricing
CHAOTOKEN uses a pre-paid credit system. You top up your account and usage is deducted per-token at model-specific rates.
View full pricing in Billing →
Interactive Playground
Test models interactively in our browser-based API Playground. No coding required.
Open Playground →
Security
Change Password
{{ pwdSuccess }}
{{ pwdError }}
Balance Alert
When your balance drops below this threshold, you'll receive a notification and see a banner warning.
Alert me when balance is below
¥
Current: ¥{{ (balanceThreshold / 1e6).toFixed(2) }} ·
{{ user?.balance <= balanceThreshold ? '⚠ Below threshold' : '✓ Above threshold' }}
Threshold disabled
{{ ticketDetail ? 'Ticket #' + ticketDetail.ticket_no : 'Help Desk' }}
Loading...
No tickets found.
{{ t.ticket_no }}
{{ t.status }}
{{ t.priority }}
{{ t.title }}
{{ ticketDetail.title }}
{{ ticketDetail.description }}
Contact: {{ ticketDetail.contact }}
Close reason: {{ ticketDetail.closed_reason }}
No messages yet.
{{ m.sender_name }} · {{ m.sender_role }}
[Internal note]
{{ m.content }}
{{ fmtDate(m.created_at) }}
Ticket closed.
Organization
Organization
{{ orgInfo?.tenant?.name || '—' }}
Your Role
{{ orgInfo?.tenant_role || '—' }}
Group
{{ orgInfo.group.name }}
Your Credits
{{ orgInfo.balance.available.toLocaleString() }}
Used
{{ orgInfo.balance.used.toLocaleString() }}
Locked
{{ orgInfo.balance.locked.toLocaleString() }}
Total
{{ orgInfo.balance.total.toLocaleString() }}
No credit balance allocated yet.
User must already have an account.
| Username | Role | Group | Keys | Credits | Status | Actions | |
|---|---|---|---|---|---|---|---|
| {{ m.email }} | {{ m.username }} | {{ m.keys_count || 0 }} | {{ (m.credits_available || 0).toLocaleString() }} | {{ m.is_active ? 'active' : 'disabled' }} |
{{ g.name }}
{{ g.member_count }} members
Credits: {{ (g.total_credits - g.used_credits).toLocaleString() }}
No groups yet.
Tenant Balance
| Type | Total | Used | Locked | Available |
|---|---|---|---|---|
| {{ b.credit_type }} | {{ b.total.toLocaleString() }} | {{ b.used.toLocaleString() }} | {{ b.locked.toLocaleString() }} | {{ b.available.toLocaleString() }} |
Group Balances
| Group | Type | Total | Used | Locked | Available |
|---|---|---|---|---|---|
| {{ b.group_name }} | {{ b.credit_type }} | {{ b.total.toLocaleString() }} | {{ b.used.toLocaleString() }} | {{ b.locked.toLocaleString() }} | {{ b.available.toLocaleString() }} |
| No group balances | |||||
Admin
{{ adminStatLabels[key] || key }}
{{ key.includes('cost') ? '¥'+(val/1e6).toFixed(2) : val?.toLocaleString() }}
| Name | Status | Members | Credits | Used | Note | Created | Actions |
|---|---|---|---|---|---|---|---|
| {{ t.name }} | {{ t.status }} | {{ t.member_count }} | {{ (t.total_credits - t.used_credits).toLocaleString() }} | {{ t.used_credits.toLocaleString() }} | {{ fmtDate(t.created_at) }} |
No enterprises yet.
{{ adminTenantDetail.name }}
{{ adminTenantDetail.status }} {{ adminTenantDetail.members?.length || 0 }} members {{ adminTenantDetail.groups?.length || 0 }} groupsMembers
User must already be registered.
| Username | Role | Group | Keys | Credits | Active | Joined | ||
|---|---|---|---|---|---|---|---|---|
| {{ m.email }} | {{ m.username }} | {{ m.keys_count || 0 }} | {{ (m.credits_available || 0).toLocaleString() }} | {{ m.is_active ? 'yes' : 'no' }} | {{ fmtDate(m.created_at) }} |
Search & Add Users
{{ u.username }}
{{ u.email }}
(already in another enterprise)
No users found.
Bulk Import Users
Paste one email per line. Accounts will be auto-created with random passwords.
✅ Imported {{ adminBulkImportResults.length }} users ({{ adminBulkImportErrors.length }} skipped)
Login URL:
{{ adminBulkImportSiteUrl }}
| Username | Password | |
|---|---|---|
| {{ r.email }} | {{ r.username }} | {{ r.password }} |
{{ e.email }}: {{ e.error }}
Groups
| Name | Status | Members |
|---|---|---|
| {{ g.name }} | {{ g.status }} | {{ g.member_count }} |
Balances
| Type | Total | Used | Locked | Available |
|---|---|---|---|---|
| {{ b.credit_type }} | {{ b.total_credits.toLocaleString() }} | {{ b.used_credits.toLocaleString() }} | {{ b.locked_credits.toLocaleString() }} | {{ b.available.toLocaleString() }} |
Allowed Models
Leave empty = allow all models.
| Username | Registered | Balance (¥) | Role | Status | Actions | |
|---|---|---|---|---|---|---|
| {{ u.username }} | {{ u.email }} | {{ fmtDate(u.created_at) }} | {{ (u.balance / 1e6).toFixed(4) }} | {{ u.role }} | {{ u.is_active ? 'Active' : 'Disabled' }} |
|
{{ adminUserDetail.username }}
Email: {{ adminUserDetail.email }}
Role: {{ adminUserDetail.role }}
Status: {{ adminUserDetail.is_active ? 'Active' : 'Disabled' }}
Balance: ¥{{ (adminUserDetail.balance / 1e6).toFixed(4) }}
Created: {{ fmtDate(adminUserDetail.created_at) }}
Internal Note
Allowed Models (comma-separated, empty = all)
API Keys ({{ adminUserDetailKeys.length }})
No API keys.
{{ k.name }}
{{ k.key_masked }}
{{ k.is_active ? 'Active' : 'Disabled' }}
Expires: {{ fmtDate(k.expires_at) }}
{{ (k.total_tokens || 0).toLocaleString() }} tok
Total Tokens
{{ (adminUserDetailUsage.total_tokens || 0).toLocaleString() }}
Total Cost
¥{{ (adminUserDetailUsage.total_cost / 1e6 || 0).toFixed(4) }}
Requests
{{ adminUserDetailUsage.total_requests || 0 }}
{{ a.name }}
{{ m }}
All models
Tokens: {{ a.total_tokens?.toLocaleString() }}
Errors: {{ a.error_count }}
Last used: {{ fmtDate(a.last_used_at) }}
· {{ healthCache[a.id].health === 'ok' ? '✓ Online' : healthCache[a.id].health === 'exhausted' ? '✗ No quota' : healthCache[a.id].health === 'forbidden' || healthCache[a.id].health === 'unauthorized' ? '✗ Key invalid' : healthCache[a.id].reachable ? '⚠ Reachable' : healthCache[a.id].error || 'Unknown' }}
(used: ${{ healthCache[a.id].quota.used?.toFixed(2) || '?' }} / ${{ healthCache[a.id].quota.limit?.toFixed(2) }})
{{ a.status }}
No upstreams configured.
No upstreams configured.
{{ pool.model }}
{{ pool.activeCount }} / {{ pool.keys.length }} keys active
{{ k.name }}
p={{ k.priority || 1 }} w={{ k.weight }}
{{ healthCache[k.id].health === 'ok' ? '✓' : '✗' }}
Model Management
Loading model pools...
No credential models found. Configure upstream accounts first.
{{ pool.canonical_name }}
{{ pool.total_upstreams }} key{{ pool.total_upstreams !== 1 ? 's' : '' }}
↓ Primary exhausted → fallback to next base_url pool
Pricing Configuration
Unsaved drafts
{{ modelsMissingPrices.length }} model(s) from Model Management have no price configured.
All prices in USD / 1M tokens
Last published: {{ fmtDate(lastPublishedAt) }}
| Model | Prov | Source | Link | $ Base In (edit) |
$ Base Out (edit) |
Cost % (off) |
$ Cost (In/Out) | Sell % (off) |
$ Selling (In/Out) | Status | Show | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ p.model }} L D | {{ p.provider || '—' }} | — | ${{ ((p.cost_input || 0)).toFixed(2) }} / ${{ ((p.cost_output || 0)).toFixed(2) }} | ${{ ((p.selling_input || 0)).toFixed(2) }} / ${{ ((p.selling_output || 0)).toFixed(2) }} | Live Draft Default |
• Base = official model price (USD/1M). Discount % is "off" style: 75 = 75% off → cost = base × 25%.
• Cost (internal, admin only). Selling (customer-facing). Only selling price affects billing.
• Show toggle controls whether model appears on the public subscription page (#models).
• Changes are drafts until "Publish All" is clicked. Published prices become live for billing, home page, and tenant views.
Publish History
Loading...
No logs yet.
| Time | Action | Detail |
|---|---|---|
| {{ fmtDate(l.created_at) }} | {{ l.action }} | {{ l.detail }} |
Chat Model Configuration
Select which models appear in the Chat page model selector.
Loading available models...
No upstream models found
Configure upstream accounts first, then return here to pick which models appear in Chat.
{{ chatSelectedModels.length }} / {{ chatAllModels.length }} selected
{{ group }}
Help Desk — Admin
{{ k }}
{{ v }}
Loading...
No tickets.
{{ t.ticket_no }}
{{ t.status }}
{{ t.priority }}
{{ t.title }}