Files
Luminary/internal/gateway/health_check.go
T
renjue 76ba500417
CI / docker (push) Successful in 2m4s
Initial commit: Luminary AI Gateway
OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress,
ingress key governance, monitoring, and security controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 21:46:16 +08:00

191 lines
5.0 KiB
Go

package gateway
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/rose_cat707/luminary/internal/model"
"github.com/rose_cat707/luminary/internal/provider"
)
func (s *Service) healthCheckEndpoints(p *model.Provider) []provider.EndpointType {
var out []provider.EndpointType
if s.providerAllowsEndpoint(p, provider.EndpointListModels) {
out = append(out, provider.EndpointListModels)
}
switch p.Category {
case model.CategoryEmbedding:
if s.providerAllowsEndpoint(p, provider.EndpointEmbeddings) {
out = append(out, provider.EndpointEmbeddings)
}
case model.CategoryRerank:
if s.providerAllowsEndpoint(p, provider.EndpointRerank) {
out = append(out, provider.EndpointRerank)
}
}
return out
}
func (s *Service) runHealthChecks(
ctx context.Context,
p *model.Provider,
keys []model.ProviderKey,
cfg provider.ProviderConfig,
) (model.ProviderStatus, int64, string) {
endpoints := s.healthCheckEndpoints(p)
if len(endpoints) == 0 {
return model.ProviderUnhealthy, 0, "no health check endpoint enabled"
}
start := time.Now()
var lastErr string
for _, ep := range endpoints {
status, _, msg := s.runHealthCheck(ctx, p, keys, cfg, ep)
if status == model.ProviderHealthy {
return model.ProviderHealthy, time.Since(start).Milliseconds(), ""
}
if msg != "" {
lastErr = msg
}
}
if lastErr == "" {
lastErr = "health check failed"
}
return model.ProviderUnhealthy, time.Since(start).Milliseconds(), lastErr
}
func (s *Service) runHealthCheck(
ctx context.Context,
p *model.Provider,
keys []model.ProviderKey,
cfg provider.ProviderConfig,
endpoint provider.EndpointType,
) (model.ProviderStatus, int64, string) {
start := time.Now()
latency := func() int64 { return time.Since(start).Milliseconds() }
switch endpoint {
case provider.EndpointListModels:
if len(keys) == 0 && p.Type == model.ProviderOllama {
if err := s.openai.HealthCheckWithConfig(ctx, "", cfg); err == nil {
return model.ProviderHealthy, latency(), ""
}
return model.ProviderUnhealthy, latency(), "health check failed"
}
for _, k := range keys {
if !k.Enabled {
continue
}
apiKey, err := s.DecryptAPIKey(k.APIKeyEnc)
if err != nil {
continue
}
var checkErr error
if p.Type == model.ProviderAnthropic {
checkErr = s.anthropic.HealthCheckWithConfig(ctx, apiKey, cfg)
} else {
checkErr = s.openai.HealthCheckWithConfig(ctx, apiKey, cfg)
}
if checkErr == nil {
return model.ProviderHealthy, latency(), ""
}
}
return model.ProviderUnhealthy, latency(), "health check failed"
case provider.EndpointEmbeddings, provider.EndpointRerank:
body, err := healthProbeBody(s, p, endpoint)
if err != nil {
return model.ProviderUnhealthy, latency(), err.Error()
}
if len(keys) == 0 && p.Type == model.ProviderOllama {
ok, msg := s.probeEndpoint(ctx, "", cfg, endpoint, body)
if ok {
return model.ProviderHealthy, latency(), ""
}
return model.ProviderUnhealthy, latency(), msg
}
var lastErr string
for _, k := range keys {
if !k.Enabled {
continue
}
apiKey, err := s.DecryptAPIKey(k.APIKeyEnc)
if err != nil {
continue
}
if ok, msg := s.probeEndpoint(ctx, apiKey, cfg, endpoint, body); ok {
return model.ProviderHealthy, latency(), ""
} else if msg != "" {
lastErr = msg
}
}
if lastErr == "" {
lastErr = "health check failed"
}
return model.ProviderUnhealthy, latency(), lastErr
default:
return model.ProviderUnhealthy, latency(), "unsupported health check endpoint"
}
}
func (s *Service) probeEndpoint(
ctx context.Context,
apiKey string,
cfg provider.ProviderConfig,
endpoint provider.EndpointType,
body []byte,
) (bool, string) {
resp, err := s.openai.ForwardWithConfig(ctx, apiKey, cfg, endpoint, body)
if err != nil {
return false, err.Error()
}
if resp == nil {
return false, "empty response"
}
if isHealthCheckHTTPStatus(resp.StatusCode) {
return true, ""
}
return false, fmt.Sprintf("%s: HTTP %d", endpoint, resp.StatusCode)
}
func healthProbeBody(s *Service, p *model.Provider, endpoint provider.EndpointType) ([]byte, error) {
modelID := probeModelID(s, p.ID, endpoint)
switch endpoint {
case provider.EndpointEmbeddings:
return json.Marshal(map[string]string{"model": modelID, "input": "."})
case provider.EndpointRerank:
return json.Marshal(map[string]any{
"model": modelID,
"query": "ping",
"documents": []string{"ping"},
})
default:
return nil, fmt.Errorf("unsupported probe endpoint %s", endpoint)
}
}
func probeModelID(s *Service, providerID uint, endpoint provider.EndpointType) string {
for _, m := range s.cache.GetModels(providerID) {
if m.Enabled && m.ModelID != "" {
return m.ModelID
}
}
switch endpoint {
case provider.EndpointEmbeddings:
return "text-embedding-3-small"
case provider.EndpointRerank:
return "rerank-multilingual-v3.0"
default:
return "health-check"
}
}
func isHealthCheckHTTPStatus(code int) bool {
if code >= 200 && code < 300 {
return true
}
// Upstream reachable but rejected payload/model — still counts as healthy.
return code == 400 || code == 422
}