Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ env/

# Logs
logs/
!app/static/logs/
*.log

# Data
Expand Down
66 changes: 66 additions & 0 deletions app/api/v1/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1781,3 +1781,69 @@ async def _on_item(item: str, res: dict):
"task_id": task.id,
"total": len(token_list),
}


# ==================== Usage Logs ====================


@router.get("/admin/logs", response_class=HTMLResponse, include_in_schema=False)
async def admin_logs_page():
"""使用记录页"""
return await render_template("logs/logs.html")


@router.get("/api/v1/admin/logs", dependencies=[Depends(verify_api_key)])
async def get_logs_api(
type: str = Query(default=None),
model: str = Query(default=None),
status: str = Query(default=None),
token_hash: str = Query(default=None),
start_time: int = Query(default=None),
end_time: int = Query(default=None),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
):
"""查询使用记录"""
from app.services.usage_log import UsageLogService

try:
logs, total = await UsageLogService.query(
type=type,
model=model,
status=status,
token_hash=token_hash,
start_time=start_time,
end_time=end_time,
page=page,
page_size=page_size,
)
return {"data": logs, "total": total, "page": page, "page_size": page_size}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.delete("/api/v1/admin/logs", dependencies=[Depends(verify_api_key)])
async def delete_logs_api(data: dict):
"""清理使用记录"""
from app.services.usage_log import UsageLogService

before = data.get("before_timestamp")
if not before:
raise HTTPException(status_code=400, detail="Missing before_timestamp")
try:
deleted = await UsageLogService.delete(int(before))
return {"status": "success", "deleted": deleted}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.get("/api/v1/admin/logs/stats", dependencies=[Depends(verify_api_key)])
async def get_logs_stats_api():
"""使用记录统计"""
from app.services.usage_log import UsageLogService

try:
stats = await UsageLogService.stats()
return stats
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
11 changes: 9 additions & 2 deletions app/api/v1/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import Any, Dict, List, Optional, Union

from fastapi import APIRouter
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse, JSONResponse
from pydantic import BaseModel, Field, field_validator

Expand Down Expand Up @@ -248,13 +248,18 @@ def validate_request(request: ChatCompletionRequest):


@router.post("/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
async def chat_completions(request: ChatCompletionRequest, raw_request: Request):
"""Chat Completions API - 兼容 OpenAI"""
from app.core.logger import logger

# 参数验证
validate_request(request)

# 获取客户端 IP
client_ip = raw_request.headers.get("x-forwarded-for", "").split(",")[0].strip()
if not client_ip:
client_ip = raw_request.client.host if raw_request.client else ""

logger.debug(f"Chat request: model={request.model}, stream={request.stream}")

# 检测视频模型
Expand All @@ -274,13 +279,15 @@ async def chat_completions(request: ChatCompletionRequest):
video_length=v_conf.video_length,
resolution=v_conf.resolution_name,
preset=v_conf.preset,
client_ip=client_ip,
)
else:
result = await ChatService.completions(
model=request.model,
messages=[msg.model_dump() for msg in request.messages],
stream=request.stream,
thinking=request.thinking,
client_ip=client_ip,
)

if isinstance(result, dict):
Expand Down
55 changes: 38 additions & 17 deletions app/api/v1/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import random
import re
import time
import time as _time
from pathlib import Path
from typing import List, Optional, Union

from fastapi import APIRouter, File, Form, UploadFile
from fastapi import APIRouter, File, Form, Request, UploadFile
from fastapi.responses import StreamingResponse, JSONResponse
from pydantic import BaseModel, Field, ValidationError

Expand Down Expand Up @@ -289,17 +290,10 @@ async def call_grok(


@router.post("/images/generations")
async def create_image(request: ImageGenerationRequest):
"""
Image Generation API
async def create_image(request: ImageGenerationRequest, raw_request: Request):
"""Image Generation API"""
start_time = _time.time()

流式响应格式:
- event: image_generation.partial_image
- event: image_generation.completed

非流式响应格式:
- {"created": ..., "data": [{"b64_json": "..."}], "usage": {...}}
"""
# stream 默认为 false
if request.stream is None:
request.stream = False
Expand Down Expand Up @@ -481,9 +475,23 @@ async def _fetch_batch(call_target: int):
"input_tokens_details": {"text_tokens": 0, "image_tokens": 0},
}

# 记录使用日志
client_ip = raw_request.headers.get("x-forwarded-for", "").split(",")[0].strip()
if not client_ip:
client_ip = raw_request.client.host if raw_request.client else ""
try:
from app.services.usage_log import UsageLogService
use_time = int((_time.time() - start_time) * 1000)
await UsageLogService.record(
type="image", model=request.model, is_stream=False,
use_time=use_time, status="success", ip=client_ip,
)
except Exception:
pass

return JSONResponse(
content={
"created": int(time.time()),
"created": int(_time.time()),
"data": data,
"usage": usage,
}
Expand All @@ -492,6 +500,7 @@ async def _fetch_batch(call_target: int):

@router.post("/images/edits")
async def edit_image(
raw_request: Request,
prompt: str = Form(...),
image: List[UploadFile] = File(...),
model: Optional[str] = Form("grok-imagine-1.0-edit"),
Expand All @@ -502,11 +511,9 @@ async def edit_image(
style: Optional[str] = Form(None),
stream: Optional[bool] = Form(False),
):
"""
Image Edits API
"""Image Edits API"""
start_time = _time.time()

同官方 API 格式,仅支持 multipart/form-data 文件上传
"""
if response_format is None:
response_format = resolve_response_format(None)

Expand Down Expand Up @@ -733,9 +740,23 @@ async def _call_edit():

data = [{response_field: img} for img in selected_images]

# 记录使用日志
client_ip = raw_request.headers.get("x-forwarded-for", "").split(",")[0].strip()
if not client_ip:
client_ip = raw_request.client.host if raw_request.client else ""
try:
from app.services.usage_log import UsageLogService
use_time = int((_time.time() - start_time) * 1000)
await UsageLogService.record(
type="image", model=edit_request.model, is_stream=False,
use_time=use_time, status="success", ip=client_ip,
)
except Exception:
pass

return JSONResponse(
content={
"created": int(time.time()),
"created": int(_time.time()),
"data": data,
"usage": {
"total_tokens": 0,
Expand Down
Loading
Loading