mirror of
https://github.com/coder/coder.git
synced 2026-10-28 02:42:54 +08:00
feat: configure multiple AI Bridge providers of the same type (#23948)
_Disclaimer: produced mostly by Claude Opus 4.6 following detailed planning._ ## Summary - Support multiple instances of the same AI Bridge provider type via indexed env vars (`CODER_AIBRIDGE_PROVIDER_<N>_<KEY>`), following the `CODER_EXTERNAL_AUTH_<N>_<KEY>` pattern - Existing single-provider env vars (`CODER_AIBRIDGE_OPENAI_KEY`, etc.) continue to work unchanged - Setting both a legacy env var and an indexed provider with the same name errors at startup to prevent silent misconfiguration - Mark legacy provider fields (`OpenAI`, `Anthropic`, `Bedrock`) as deprecated in `AIBridgeConfig` in favor of `Providers` ## Example ```sh CODER_AIBRIDGE_PROVIDER_0_TYPE=anthropic CODER_AIBRIDGE_PROVIDER_0_NAME=anthropic-corp CODER_AIBRIDGE_PROVIDER_0_KEY=sk-ant-corp-xxx CODER_AIBRIDGE_PROVIDER_0_BASE_URL=https://llm-proxy.internal.example.com/anthropic CODER_AIBRIDGE_PROVIDER_1_TYPE=anthropic CODER_AIBRIDGE_PROVIDER_1_NAME=anthropic-direct CODER_AIBRIDGE_PROVIDER_1_KEY=sk-ant-direct-yyy ``` Each instance is routed by name: - /api/v2/aibridge/**anthropic-corp**/v1/messages - /api/v2/aibridge/**anthropic-direct**/v1/messages Closes [AIGOV-157](https://linear.app/codercom/issue/AIGOV-157/spike-to-understand-if-there-is-a-simple-way-to-handle-multi-api-key) --------- Signed-off-by: Danny Kopping <danny@coder.com>
This commit is contained in:
@@ -2093,6 +2093,64 @@ func TestUpstreamProxy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestProxy_MITM_CustomProvider verifies that a non-builtin provider
|
||||
// (e.g. OpenRouter) whose domain is added to the allowlist is correctly
|
||||
// MITM'd and routed through the proxy to the bridge endpoint.
|
||||
func TestProxy_MITM_CustomProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
openrouterDomain = "openrouter.ai"
|
||||
openrouterProvider = "openrouter"
|
||||
)
|
||||
|
||||
// Track what aibridged receives.
|
||||
var receivedPath, receivedBYOK string
|
||||
|
||||
// Create a mock aibridged server that captures requests.
|
||||
aibridgedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedPath = r.URL.Path
|
||||
receivedBYOK = r.Header.Get(agplaibridge.HeaderCoderToken)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("hello from aibridged"))
|
||||
}))
|
||||
t.Cleanup(aibridgedServer.Close)
|
||||
|
||||
// Wire the custom domain and provider mapping directly, as the
|
||||
// real daemon would after calling domainsFromProviders.
|
||||
srv := newTestProxy(t,
|
||||
withCoderAccessURL(aibridgedServer.URL),
|
||||
withDomainAllowlist(openrouterDomain),
|
||||
withAIBridgeProviderFromHost(func(host string) string {
|
||||
if host == openrouterDomain {
|
||||
return openrouterProvider
|
||||
}
|
||||
return ""
|
||||
}),
|
||||
)
|
||||
|
||||
certPool := getProxyCertPool(t)
|
||||
client := newProxyClient(t, srv, makeProxyAuthHeader("coder-token"), certPool, false)
|
||||
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://"+openrouterDomain+"/api/v1/chat/completions", strings.NewReader(`{}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer user-llm-token")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, "hello from aibridged", string(body))
|
||||
|
||||
// The proxy should route through the aibridge path using the custom
|
||||
// provider name.
|
||||
require.Equal(t, "/api/v2/aibridge/"+openrouterProvider+"/api/v1/chat/completions", receivedPath)
|
||||
require.Equal(t, "coder-token", receivedBYOK)
|
||||
}
|
||||
|
||||
func TestProxy_PrivateIPBlocking(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user