NewRequestBridge creates a new *[RequestBridge] and registers the HTTP routes defined by the given providers. Any routes which are requested but not registered will be reverse-proxied to the upstream service. A [intercept.Recorder] is also required to record prompt, tool, and token use. mcpProxy w
(ctx context.Context, providers []provider.Provider, rec recorder.Recorder, mcpProxy mcp.ServerProxier, logger slog.Logger, m *metrics.Metrics, tracer trace.Tracer)
| 111 | // Circuit breaker configuration is obtained from each provider's CircuitBreakerConfig() method. |
| 112 | // Providers returning nil will not have circuit breaker protection. |
| 113 | func NewRequestBridge(ctx context.Context, providers []provider.Provider, rec recorder.Recorder, mcpProxy mcp.ServerProxier, logger slog.Logger, m *metrics.Metrics, tracer trace.Tracer) (*RequestBridge, error) { |
| 114 | if err := validateProviders(providers); err != nil { |
| 115 | return nil, err |
| 116 | } |
| 117 | |
| 118 | mux := http.NewServeMux() |
| 119 | |
| 120 | for _, prov := range providers { |
| 121 | // Disabled providers serve a 503 sentinel on every path under |
| 122 | // "/<name>/". Bound to the bare name (not RoutePrefix) so paths |
| 123 | // outside the provider's normal "/v1" subtree are also caught. |
| 124 | if !prov.Enabled() { |
| 125 | prefix := fmt.Sprintf("/%s/", prov.Name()) |
| 126 | mux.HandleFunc(prefix, disabledProviderHandler(prov.Name(), logger)) |
| 127 | continue |
| 128 | } |
| 129 | // Create per-provider circuit breaker if configured |
| 130 | cfg := prov.CircuitBreakerConfig() |
| 131 | providerName := prov.Name() |
| 132 | onChange := func(endpoint, model string, from, to gobreaker.State) { |
| 133 | logger.Info(context.Background(), "circuit breaker state change", |
| 134 | slog.F("provider", providerName), |
| 135 | slog.F("endpoint", endpoint), |
| 136 | slog.F("model", model), |
| 137 | slog.F("from", from.String()), |
| 138 | slog.F("to", to.String()), |
| 139 | ) |
| 140 | if m != nil { |
| 141 | m.CircuitBreakerState.WithLabelValues(providerName, endpoint, model).Set(circuitbreaker.StateToGaugeValue(to)) |
| 142 | if to == gobreaker.StateOpen { |
| 143 | m.CircuitBreakerTrips.WithLabelValues(providerName, endpoint, model).Inc() |
| 144 | } |
| 145 | } |
| 146 | } |
| 147 | cbs := circuitbreaker.NewProviderCircuitBreakers(providerName, cfg, onChange, m) |
| 148 | |
| 149 | // Add the known provider-specific routes which are bridged (i.e. intercepted and augmented). |
| 150 | for _, path := range prov.BridgedRoutes() { |
| 151 | handler := newInterceptionProcessor(prov, cbs, rec, mcpProxy, logger, m, tracer) |
| 152 | route, err := url.JoinPath(prov.RoutePrefix(), path) |
| 153 | if err != nil { |
| 154 | logger.Error(ctx, "failed to join path", |
| 155 | slog.Error(err), |
| 156 | slog.F("provider", providerName), |
| 157 | slog.F("prefix", prov.RoutePrefix()), |
| 158 | slog.F("path", path), |
| 159 | ) |
| 160 | return nil, xerrors.Errorf("failed to configure provider '%v': failed to join bridged path: %w", providerName, err) |
| 161 | } |
| 162 | mux.Handle(route, handler) |
| 163 | } |
| 164 | |
| 165 | // Any requests which passthrough to this will be reverse-proxied to the upstream. |
| 166 | // |
| 167 | // We have to whitelist the known-safe routes because an API key with elevated privileges (i.e. admin) might be |
| 168 | // configured, so we should just reverse-proxy known-safe routes. |
| 169 | ftr := newPassthroughRouter(prov, logger.Named(fmt.Sprintf("passthrough.%s", prov.Name())), m, tracer) |
| 170 | for _, path := range prov.PassthroughRoutes() { |