diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index 133d123..f72ea81 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -55,6 +55,7 @@ type Config struct { CORSAllowedOrigin string GlobalHTTPProxy string GlobalHTTPProxySource string + PlatformProxyBypassIDs string LogLevel slog.Level BillingEngineMode string ProcessRole string @@ -155,6 +156,7 @@ func Load() Config { CORSAllowedOrigin: env("CORS_ALLOWED_ORIGIN", "http://localhost:5178,http://127.0.0.1:5178"), GlobalHTTPProxy: globalProxy.HTTPProxy, GlobalHTTPProxySource: globalProxy.Source, + PlatformProxyBypassIDs: env("AI_GATEWAY_PLATFORM_PROXY_BYPASS_IDS", ""), LogLevel: logLevel(env("LOG_LEVEL", "info")), BillingEngineMode: strings.ToLower(env("BILLING_ENGINE_MODE", "observe")), ProcessRole: strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_PROCESS_ROLE", "all"))), diff --git a/apps/api/internal/runner/proxy.go b/apps/api/internal/runner/proxy.go index d0ccdc7..8a8bb7a 100644 --- a/apps/api/internal/runner/proxy.go +++ b/apps/api/internal/runner/proxy.go @@ -38,6 +38,13 @@ func (s *Service) httpClientForCandidate(candidate store.RuntimeModelCandidate, if simulated { return s.httpClients.none, nil } + // Some Worker sites have direct provider egress but are not authorized to + // use a platform's shared proxy. Keep this override explicitly scoped to + // platform UUIDs so one site's routing exception cannot bypass proxies for + // unrelated providers or platforms. + if platformIDListed(s.cfg.PlatformProxyBypassIDs, candidate.PlatformID) { + return s.httpClients.none, nil + } config, err := netproxy.Normalize(netproxy.FromPlatformConfig(candidate.PlatformConfig)) if err != nil { return nil, &clients.ClientError{Code: "invalid_proxy", Message: err.Error(), Retryable: false} @@ -55,6 +62,19 @@ func (s *Service) httpClientForCandidate(candidate store.RuntimeModelCandidate, } } +func platformIDListed(raw string, platformID string) bool { + platformID = strings.TrimSpace(platformID) + if platformID == "" { + return false + } + for value := range strings.SplitSeq(raw, ",") { + if strings.TrimSpace(value) == platformID { + return true + } + } + return false +} + func (c *httpClientCache) customClient(rawProxy string) (*http.Client, error) { normalized, proxyURL, err := netproxy.ParseHTTPProxy(rawProxy) if err != nil { diff --git a/apps/api/internal/runner/proxy_test.go b/apps/api/internal/runner/proxy_test.go index c066d3f..c699ad3 100644 --- a/apps/api/internal/runner/proxy_test.go +++ b/apps/api/internal/runner/proxy_test.go @@ -101,6 +101,41 @@ func TestPlatformProxyModeCustomUsesConfiguredHTTPProxy(t *testing.T) { } } +func TestPlatformProxyBypassIDUsesDirectConnection(t *testing.T) { + var targetHits int + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + targetHits++ + _, _ = w.Write([]byte("target")) + })) + defer target.Close() + + var proxyHits int + proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + proxyHits++ + w.WriteHeader(http.StatusProxyAuthRequired) + })) + defer proxy.Close() + + client, err := testProxyService(config.Config{ + PlatformProxyBypassIDs: "other-platform, official-gemini-platform ", + }).httpClientForCandidate(store.RuntimeModelCandidate{ + PlatformID: "official-gemini-platform", + PlatformConfig: map[string]any{"networkProxy": map[string]any{"mode": "custom", "httpProxy": proxy.URL}}, + }, false) + if err != nil { + t.Fatalf("build bypassed http client: %v", err) + } + resp, err := client.Get(target.URL) + if err != nil { + t.Fatalf("get target directly: %v", err) + } + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + if resp.StatusCode != http.StatusOK || targetHits != 1 || proxyHits != 0 { + t.Fatalf("unexpected status=%d targetHits=%d proxyHits=%d", resp.StatusCode, targetHits, proxyHits) + } +} + func TestPlatformProxyModeGlobalUsesConfiguredGlobalHTTPProxy(t *testing.T) { var targetHits int target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/deploy/kubernetes/production/application.yaml b/deploy/kubernetes/production/application.yaml index f745469..341a069 100644 --- a/deploy/kubernetes/production/application.yaml +++ b/deploy/kubernetes/production/application.yaml @@ -715,6 +715,8 @@ spec: fieldPath: metadata.uid - name: EASYAI_SITE value: hongkong + - name: AI_GATEWAY_PLATFORM_PROXY_BYPASS_IDS + value: 99372d7c-f2a4-472a-987f-30cb76c7962c ports: - name: health containerPort: 8088 diff --git a/tests/release/cluster-release-helper-test.sh b/tests/release/cluster-release-helper-test.sh index 0167a25..a862bbf 100755 --- a/tests/release/cluster-release-helper-test.sh +++ b/tests/release/cluster-release-helper-test.sh @@ -116,5 +116,7 @@ fi hongkong_strategy=$(deployment_strategy easyai-worker-hongkong) grep -Fq ' replicas: 1' <<<"$hongkong_strategy" grep -Fq 'easyai.io/worker-only' <<<"$hongkong_strategy" +grep -Fq 'AI_GATEWAY_PLATFORM_PROXY_BYPASS_IDS' <<<"$hongkong_strategy" +grep -Fq '99372d7c-f2a4-472a-987f-30cb76c7962c' <<<"$hongkong_strategy" echo 'cluster_release_helper_tests=PASS'