mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-09 14:56:27 +00:00
feat(aiproxy): add authenticated proxy for OpenAI-compatible APIs and MCP servers
Hides upstream URLs and keys behind the security layer. Per-upstream roles, model/tool allowlists, per-user rate limits, Before/AfterProxy hooks, audit sink, Prometheus metrics (metrics.AIProxyRecorder) and upstreams loaded from the resolvespec_ai_proxies stored procedure.
This commit is contained in:
@@ -47,6 +47,14 @@ type Provider interface {
|
||||
Handler() http.Handler
|
||||
}
|
||||
|
||||
// AIProxyRecorder is optionally implemented by providers that record aiproxy traffic.
|
||||
// pkg/aiproxy uses it when the global provider implements it.
|
||||
type AIProxyRecorder interface {
|
||||
// RecordAIProxy records one proxied (or refused) request. statusClass is "2xx".."5xx",
|
||||
// outcome is ok, upstream_error, denied or rate_limited, model may be empty.
|
||||
RecordAIProxy(upstream, kind, model, statusClass, outcome string, duration time.Duration, promptTokens, completionTokens int64)
|
||||
}
|
||||
|
||||
// Resetter is optionally implemented by providers that can clear their recorded stats.
|
||||
type Resetter interface {
|
||||
Reset()
|
||||
|
||||
@@ -32,6 +32,9 @@ type PrometheusProvider struct {
|
||||
eventDuration *prometheus.HistogramVec
|
||||
eventQueueSize prometheus.Gauge
|
||||
panicsTotal *prometheus.CounterVec
|
||||
aiRequests *prometheus.CounterVec
|
||||
aiDuration *prometheus.HistogramVec
|
||||
aiTokens *prometheus.CounterVec
|
||||
|
||||
pathLimiter *pathLimiter
|
||||
pathNormalizer func(*http.Request) string
|
||||
@@ -162,6 +165,28 @@ func NewPrometheusProvider(cfg *Config) *PrometheusProvider {
|
||||
},
|
||||
[]string{"method"},
|
||||
),
|
||||
aiRequests: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: metricName("aiproxy_requests_total"),
|
||||
Help: "Total number of requests handled by the AI proxy",
|
||||
},
|
||||
[]string{"upstream", "kind", "model", "status", "outcome"},
|
||||
),
|
||||
aiDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: metricName("aiproxy_request_duration_seconds"),
|
||||
Help: "AI proxy request duration in seconds",
|
||||
Buckets: cfg.HTTPRequestBuckets,
|
||||
},
|
||||
[]string{"upstream", "kind"},
|
||||
),
|
||||
aiTokens: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: metricName("aiproxy_tokens_total"),
|
||||
Help: "Tokens reported by AI proxy upstreams",
|
||||
},
|
||||
[]string{"upstream", "model", "type"},
|
||||
),
|
||||
|
||||
pathLimiter: newPathLimiter(cfg.HTTPMaxPaths),
|
||||
pathNormalizer: cfg.HTTPPathNormalizer,
|
||||
@@ -310,6 +335,21 @@ func (p *PrometheusProvider) RecordPanic(methodName string) {
|
||||
p.panicsTotal.WithLabelValues(methodName).Inc()
|
||||
}
|
||||
|
||||
// RecordAIProxy implements AIProxyRecorder
|
||||
func (p *PrometheusProvider) RecordAIProxy(upstream, kind, model, statusClass, outcome string, duration time.Duration, promptTokens, completionTokens int64) {
|
||||
if !p.enabled {
|
||||
return
|
||||
}
|
||||
p.aiRequests.WithLabelValues(upstream, kind, model, statusClass, outcome).Inc()
|
||||
p.aiDuration.WithLabelValues(upstream, kind).Observe(duration.Seconds())
|
||||
if promptTokens > 0 {
|
||||
p.aiTokens.WithLabelValues(upstream, model, "prompt").Add(float64(promptTokens))
|
||||
}
|
||||
if completionTokens > 0 {
|
||||
p.aiTokens.WithLabelValues(upstream, model, "completion").Add(float64(completionTokens))
|
||||
}
|
||||
}
|
||||
|
||||
// Handler implements Provider interface
|
||||
// It responds 404 when metrics are disabled.
|
||||
func (p *PrometheusProvider) Handler() http.Handler {
|
||||
@@ -437,6 +477,9 @@ func (p *PrometheusProvider) Reset() {
|
||||
p.eventProcessed.Reset()
|
||||
p.eventDuration.Reset()
|
||||
p.panicsTotal.Reset()
|
||||
p.aiRequests.Reset()
|
||||
p.aiDuration.Reset()
|
||||
p.aiTokens.Reset()
|
||||
p.pathLimiter.reset()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user