This commit is contained in:
2026-06-10 07:55:16 +08:00
parent 821eca9f0c
commit aaac7e9d98
31 changed files with 548 additions and 149 deletions
+96 -62
View File
@@ -99,6 +99,9 @@ func (h *Handler) Mount(r chi.Router) {
// User usage.
r.Get("/users/me/usage", h.userDailyUsage)
// Public (non-admin) models list for chat composers.
r.Get("/models", h.listModelsPublic)
// Admin-only routes.
r.Group(func(r chi.Router) {
r.Use(h.adminGuard)
@@ -246,7 +249,13 @@ func (h *Handler) getConversation(w http.ResponseWriter, r *http.Request) {
writeErr(w, r, err)
return
}
conv, msgs, err := h.convSvc.Get(r.Context(), uid, id)
limit := 50
if v := r.URL.Query().Get("limit"); v != "" {
if n, e := strconv.Atoi(v); e == nil && n > 0 {
limit = n
}
}
conv, msgs, err := h.convSvc.Get(r.Context(), uid, id, limit)
if err != nil {
writeErr(w, r, err)
return
@@ -398,12 +407,15 @@ func (h *Handler) retryMessage(w http.ResponseWriter, r *http.Request) {
writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "invalid message id"))
return
}
newID, err := h.msgSvc.Retry(r.Context(), uid, msgID)
newID, convID, err := h.msgSvc.Retry(r.Context(), uid, msgID)
if err != nil {
writeErr(w, r, err)
return
}
writeJSON(w, http.StatusOK, map[string]int64{"assistant_message_id": newID})
writeJSON(w, http.StatusOK, map[string]any{
"assistant_message_id": newID,
"stream_url": fmt.Sprintf("/api/v1/conversations/%s/stream", convID),
})
}
// ===== Template handlers =====
@@ -641,6 +653,22 @@ func (h *Handler) listModels(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, out)
}
// listModelsPublic mirrors listModels but is reachable by any authenticated
// user (non-admin), so chat composers can list available models.
func (h *Handler) listModelsPublic(w http.ResponseWriter, r *http.Request) {
onlyEnabled := r.URL.Query().Get("only_enabled") == "true"
models, err := h.epSvc.ListModels(r.Context(), onlyEnabled)
if err != nil {
writeErr(w, r, err)
return
}
out := make([]ModelDTO, 0, len(models))
for i := range models {
out = append(out, toModelDTO(&models[i]))
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) createModel(w http.ResponseWriter, r *http.Request) {
var req CreateModelReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -648,18 +676,18 @@ func (h *Handler) createModel(w http.ResponseWriter, r *http.Request) {
return
}
m, err := h.epSvc.CreateModel(r.Context(), CreateModelInput{
EndpointID: req.EndpointID,
ModelID: req.ModelID,
DisplayName: req.DisplayName,
Capabilities: req.Capabilities,
ContextWindow: req.ContextWindow,
MaxOutputTokens: req.MaxOutputTokens,
PromptPrice: req.PromptPricePerMillionUSD,
CompletionPrice: req.CompletionPricePerMillionUSD,
ThinkingPrice: req.ThinkingPricePerMillionUSD,
EndpointID: req.EndpointID,
ModelID: req.ModelID,
DisplayName: req.DisplayName,
Capabilities: req.Capabilities,
ContextWindow: req.ContextWindow,
MaxOutputTokens: req.MaxOutputTokens,
PromptPrice: req.PromptPricePerMillionUSD,
CompletionPrice: req.CompletionPricePerMillionUSD,
ThinkingPrice: req.ThinkingPricePerMillionUSD,
IsTitleGenerator: req.IsTitleGenerator,
Enabled: req.Enabled,
SortOrder: req.SortOrder,
Enabled: req.Enabled,
SortOrder: req.SortOrder,
})
if err != nil {
writeErr(w, r, err)
@@ -727,56 +755,62 @@ func (h *Handler) adminUsage(w http.ResponseWriter, r *http.Request) {
}
}
byUser, err := h.usageSvc.SummarizeByUser(r.Context(), from, to)
if err != nil {
writeErr(w, r, err)
return
groupBy := q.Get("group_by")
if groupBy == "" {
groupBy = "day"
}
byModel, err := h.usageSvc.SummarizeByModel(r.Context(), from, to)
if err != nil {
writeErr(w, r, err)
return
}
byDay, err := h.usageSvc.SummarizeByDay(r.Context(), from, to)
if err != nil {
writeErr(w, r, err)
items := make([]AdminUsageItem, 0)
switch groupBy {
case "user":
rows, err := h.usageSvc.SummarizeByUser(r.Context(), from, to)
if err != nil {
writeErr(w, r, err)
return
}
for _, row := range rows {
items = append(items, AdminUsageItem{
Key: row.UserID.String(),
PromptTokens: row.PromptTokens,
CompletionTokens: row.CompletionTokens,
ThinkingTokens: row.ThinkingTokens,
CostUSD: row.CostUSD,
})
}
case "model":
rows, err := h.usageSvc.SummarizeByModel(r.Context(), from, to)
if err != nil {
writeErr(w, r, err)
return
}
for _, row := range rows {
items = append(items, AdminUsageItem{
Key: row.ModelID.String(),
PromptTokens: row.PromptTokens,
CompletionTokens: row.CompletionTokens,
ThinkingTokens: row.ThinkingTokens,
CostUSD: row.CostUSD,
})
}
case "day":
rows, err := h.usageSvc.SummarizeByDay(r.Context(), from, to)
if err != nil {
writeErr(w, r, err)
return
}
for _, row := range rows {
items = append(items, AdminUsageItem{
Key: row.Day.Format(time.RFC3339),
PromptTokens: row.PromptTokens,
CompletionTokens: row.CompletionTokens,
ThinkingTokens: row.ThinkingTokens,
CostUSD: row.CostUSD,
})
}
default:
writeErr(w, r, errs.New(errs.CodeInvalidInput, "invalid group_by"))
return
}
userDTOs := make([]UserUsageDTO, 0, len(byUser))
for _, row := range byUser {
userDTOs = append(userDTOs, UserUsageDTO{
UserID: row.UserID,
PromptTokens: row.PromptTokens,
CompletionTokens: row.CompletionTokens,
ThinkingTokens: row.ThinkingTokens,
CostUSD: row.CostUSD,
})
}
modelDTOs := make([]ModelUsageDTO, 0, len(byModel))
for _, row := range byModel {
modelDTOs = append(modelDTOs, ModelUsageDTO{
ModelID: row.ModelID,
PromptTokens: row.PromptTokens,
CompletionTokens: row.CompletionTokens,
ThinkingTokens: row.ThinkingTokens,
CostUSD: row.CostUSD,
})
}
dayDTOs := make([]DayUsageDTO, 0, len(byDay))
for _, row := range byDay {
dayDTOs = append(dayDTOs, DayUsageDTO{
Day: row.Day,
PromptTokens: row.PromptTokens,
CompletionTokens: row.CompletionTokens,
ThinkingTokens: row.ThinkingTokens,
CostUSD: row.CostUSD,
})
}
writeJSON(w, http.StatusOK, AdminUsageResp{
ByUser: userDTOs,
ByModel: modelDTOs,
ByDay: dayDTOs,
})
writeJSON(w, http.StatusOK, AdminUsageResp{Items: items})
}