package plugin import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" "cpa-ext/internal/web" ) const ( managementBase = "/v0/management/plugins/" + PluginName resourceBase = "/v0/resource/plugins/" + PluginName routeUsage = "/usage" routePrices = "/prices" resourceUI = "/ui" ) func managementRegistration() ManagementRegistrationResponse { return ManagementRegistrationResponse{ Routes: []ManagementRoute{ {Method: http.MethodGet, Path: managementBase + routeUsage, Description: "查看最近的用量记录。"}, {Method: http.MethodGet, Path: managementBase + routePrices, Description: "查看模型价格。"}, {Method: http.MethodPut, Path: managementBase + routePrices, Description: "保存模型价格。"}, {Method: http.MethodDelete, Path: managementBase + routePrices, Description: "删除模型价格。"}, }, Resources: []ResourceRoute{ {Path: resourceBase + resourceUI, Menu: "用量记录", Description: "查看 CPA 最近收到的请求用量。"}, }, } } func (a *App) handleManagement(raw []byte) ([]byte, error) { var req ManagementRequest if err := json.Unmarshal(raw, &req); err != nil { return nil, fmt.Errorf("解析管理请求: %w", err) } path := strings.TrimRight(req.Path, "/") if req.Method == http.MethodGet && path == resourceBase+resourceUI { return OKEnvelope(ManagementResponse{ StatusCode: http.StatusOK, Headers: http.Header{ "Content-Type": []string{"text/html; charset=utf-8"}, "Cache-Control": []string{"no-store"}, }, Body: web.UI(), }) } if req.Method == http.MethodGet && path == managementBase+routeUsage { return OKEnvelope(a.usageResponse()) } if path == managementBase+routePrices { switch req.Method { case http.MethodGet: return OKEnvelope(a.listPrices()) case http.MethodPut: return OKEnvelope(a.putPrice(req.Body)) case http.MethodDelete: return OKEnvelope(a.deletePrice(req.Body)) } } return OKEnvelope(jsonManagementResponse(http.StatusNotFound, map[string]any{ "error": map[string]string{ "code": "not_found", "message": "管理路由不存在: " + req.Method + " " + req.Path, }, })) } type usageListResponse struct { Records []usageListItem `json:"records"` Retained int `json:"retained"` } // usageListItem 是请求明细表的稳定接口。当前回调拿不到的字段保留为空值。 type usageListItem struct { RequestID string `json:"request_id"` ExecutionID string `json:"execution_id"` TraceID string `json:"trace_id"` RequestedAt time.Time `json:"requested_at"` APIKey string `json:"api_key"` KeyAlias string `json:"key_alias"` Model string `json:"model"` ReasoningEffort string `json:"reasoning_effort"` ServiceTier string `json:"service_tier"` Speed string `json:"speed"` Failed bool `json:"failed"` Outcome string `json:"outcome"` StatusCode int `json:"status_code"` Error string `json:"error"` ExecutorType string `json:"executor_type"` RequestType string `json:"request_type"` Endpoint string `json:"endpoint"` TTFTMilliseconds int64 `json:"ttft_ms"` SpeedTPS *float64 `json:"speed_tps"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` ReasoningTokens int64 `json:"reasoning_tokens"` CacheReadTokens int64 `json:"cache_read_tokens"` CacheWriteTokens int64 `json:"cache_write_tokens"` CacheRate *float64 `json:"cache_rate"` TotalTokens int64 `json:"total_tokens"` CostUSD *float64 `json:"cost_usd"` CostAvailable bool `json:"cost_available"` PriceTier string `json:"price_tier"` FastRequested bool `json:"fast_requested"` FastPricingApplied bool `json:"fast_pricing_applied"` ClientIP string `json:"client_ip"` } func (a *App) usageResponse() ManagementResponse { a.mu.RLock() defer a.mu.RUnlock() if a.usage == nil { return jsonManagementResponse(http.StatusServiceUnavailable, map[string]any{ "error": map[string]string{"code": "database_unavailable", "message": "用量数据库尚未初始化"}, }) } records, err := a.usage.Recent(context.Background(), 1000) if err != nil { return jsonManagementResponse(http.StatusInternalServerError, map[string]any{ "error": map[string]string{"code": "database_error", "message": err.Error()}, }) } items := make([]usageListItem, 0, len(records)) for _, record := range records { ttftMilliseconds := record.TTFT.Milliseconds() speedTPS := usageSpeed(record.OutputTokens, record.TTFT, record.Latency) if isCompactEndpoint(record.Endpoint) { // Compact 返回的是一次性 JSON,首字延迟和生成速度没有可比较的含义。 ttftMilliseconds = 0 speedTPS = nil } items = append(items, usageListItem{ RequestID: record.RequestID, ExecutionID: record.ExecutionID, TraceID: record.TraceID, RequestedAt: record.RequestedAt, APIKey: record.APIKey, KeyAlias: record.KeyAlias, Model: record.Model, ReasoningEffort: record.ReasoningEffort, ServiceTier: record.ServiceTier, Speed: record.Speed, Failed: record.Failed, Outcome: record.Outcome, StatusCode: record.StatusCode, Error: record.Error, ExecutorType: record.ExecutorType, RequestType: record.RequestType, Endpoint: record.Endpoint, TTFTMilliseconds: ttftMilliseconds, SpeedTPS: speedTPS, InputTokens: record.InputTokens, OutputTokens: record.OutputTokens, ReasoningTokens: record.ReasoningTokens, CacheReadTokens: record.CacheReadTokens, CacheWriteTokens: record.CacheWriteTokens, CacheRate: usageCacheRate(record.CacheReadTokens, record.InputTokens), TotalTokens: record.TotalTokens, CostUSD: microsToUSD(record.CostMicros), CostAvailable: record.CostMicros != nil, PriceTier: record.PriceTier, FastRequested: record.FastRequested, FastPricingApplied: record.FastPricingApplied, ClientIP: record.ClientIP, }) } return jsonManagementResponse(http.StatusOK, usageListResponse{Records: items, Retained: 1000}) } func microsToUSD(micros *int64) *float64 { if micros == nil { return nil } value := float64(*micros) / 1_000_000 return &value } func usageSpeed(outputTokens int64, ttft, latency time.Duration) *float64 { if outputTokens <= 0 || ttft <= 0 || latency <= ttft { return nil } value := float64(outputTokens) / (latency - ttft).Seconds() return &value } func usageCacheRate(cacheReadTokens, inputTokens int64) *float64 { if inputTokens <= 0 { return nil } value := float64(cacheReadTokens) / float64(inputTokens) * 100 return &value } func jsonManagementResponse(status int, value any) ManagementResponse { body, err := json.Marshal(value) if err != nil { body = []byte(`{"error":{"code":"marshal_error","message":"无法生成响应"}}`) status = http.StatusInternalServerError } return ManagementResponse{ StatusCode: status, Headers: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}}, Body: body, } }