76ba500417
CI / docker (push) Successful in 2m4s
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>
191 lines
5.0 KiB
Go
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
|
|
}
|