feat: close v3 governance evidence and slo metrics wiring
This commit is contained in:
@@ -591,7 +591,7 @@ func NewAPIHandlerWithAuth(adminAuth AdminAuthConfig, actions ActionSet, dsn ...
|
||||
mux.Handle("DELETE /api/hosts/{hostID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handleDeleteHost(w, r, actions.DeleteHost)
|
||||
})))
|
||||
return mux
|
||||
return metrics.Middleware(mux)
|
||||
}
|
||||
|
||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -2692,12 +2692,14 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
|
||||
// 1. Extract bearer key
|
||||
auth := strings.TrimSpace(r.Header.Get("Authorization"))
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
metrics.RecordUserKeyChatRequest("unauthorized")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusUnauthorized, Code: "unauthorized", Message: "missing or invalid Authorization header"})
|
||||
return
|
||||
}
|
||||
keyToken := strings.TrimPrefix(auth, "Bearer ")
|
||||
keyToken = strings.TrimSpace(keyToken)
|
||||
if keyToken == "" {
|
||||
metrics.RecordUserKeyChatRequest("unauthorized")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusUnauthorized, Code: "unauthorized", Message: "empty bearer token"})
|
||||
return
|
||||
}
|
||||
@@ -2707,6 +2709,7 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
|
||||
|
||||
store, err := sqlite.Open(r.Context(), dsn)
|
||||
if err != nil {
|
||||
metrics.RecordUserKeyChatRequest("db_error")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "db_error", Message: "database error"})
|
||||
return
|
||||
}
|
||||
@@ -2714,6 +2717,7 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
|
||||
|
||||
keys, err := store.UserKeys().ListByFingerprint(r.Context(), keyFingerprint)
|
||||
if err != nil || len(keys) == 0 {
|
||||
metrics.RecordUserKeyChatRequest("invalid_api_key")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusUnauthorized, Code: "unauthorized", Message: "invalid API key"})
|
||||
return
|
||||
}
|
||||
@@ -2723,17 +2727,25 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
|
||||
// Governance check
|
||||
log.Printf("gateway: key %s admin_status=%s paused=%v fingerprint=%s", key.KeyID, key.AdminStatus, key.AdminStatus == "paused", keyFingerprint)
|
||||
if key.AdminStatus == "paused" {
|
||||
metrics.RecordUserKeyChatRequest("key_paused")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusForbidden, Code: "key_paused", Message: "API key is paused"})
|
||||
return
|
||||
}
|
||||
if key.AdminStatus == "retired" || key.AdminStatus == "deleted" {
|
||||
metrics.RecordUserKeyChatRequest("key_retired")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusForbidden, Code: "key_retired", Message: "API key is no longer active"})
|
||||
return
|
||||
}
|
||||
if key.QuotaStatus == "exhausted" {
|
||||
metrics.RecordUserKeyChatRequest("quota_exhausted")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusForbidden, Code: "quota_exhausted", Message: "API key quota exhausted"})
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Parse request body (OpenAI-compatible)
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, maxJSONBodyBytes))
|
||||
if err != nil {
|
||||
metrics.RecordUserKeyChatRequest("bad_request")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "cannot read request body"})
|
||||
return
|
||||
}
|
||||
@@ -2745,12 +2757,14 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &openAIReq); err != nil {
|
||||
metrics.RecordUserKeyChatRequest("bad_request")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
model := strings.TrimSpace(openAIReq.Model)
|
||||
if model == "" {
|
||||
metrics.RecordUserKeyChatRequest("bad_request")
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "model is required"})
|
||||
return
|
||||
}
|
||||
@@ -2771,6 +2785,7 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
|
||||
|
||||
result, err := proxyChat(r.Context(), proxyReq)
|
||||
if err != nil {
|
||||
metrics.RecordUserKeyChatRequest("proxy_error")
|
||||
writeHTTPError(w, classifyError(err))
|
||||
return
|
||||
}
|
||||
@@ -2835,6 +2850,7 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
|
||||
}
|
||||
}
|
||||
|
||||
metrics.RecordUserKeyChatRequest("ok")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(respPayload)
|
||||
|
||||
Reference in New Issue
Block a user