diff --git a/internal/memory/embed/ollama.go b/internal/memory/embed/ollama.go index f1aef461..fe437806 100644 --- a/internal/memory/embed/ollama.go +++ b/internal/memory/embed/ollama.go @@ -105,8 +105,11 @@ func NewClientWithModel(model string) *Client { http: &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ - // Bypass system proxy for localhost connections. - Proxy: nil, + // Honor proxy env for remote endpoints (credential + // gateways inject auth at the proxy boundary); bypass + // it for localhost/loopback servers, where a stray + // HTTPS_PROXY would only get in the way. + Proxy: proxyFunc(endpoint), DialContext: (&net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 30 * time.Second, @@ -116,6 +119,20 @@ func NewClientWithModel(model string) *Client { } } +// proxyFunc returns an HTTP proxy resolver for the embedding endpoint. +// Loopback endpoints never use a proxy; everything else follows the +// standard HTTPS_PROXY/HTTP_PROXY/NO_PROXY environment resolution. +func proxyFunc(endpoint string) func(*http.Request) (*url.URL, error) { + if u, err := url.Parse(endpoint); err == nil { + if host := u.Hostname(); host == "localhost" || net.ParseIP(host).IsLoopback() { + // Never proxy the loopback: return an explicit no-proxy + // resolver rather than nil (a nil Proxy panics when invoked). + return func(*http.Request) (*url.URL, error) { return nil, nil } + } + } + return http.ProxyFromEnvironment +} + // Protocol returns the active wire protocol. func (c *Client) Protocol() Protocol { return c.protocol diff --git a/internal/memory/embed/openai_test.go b/internal/memory/embed/openai_test.go index 71d18c5b..bb9f1186 100644 --- a/internal/memory/embed/openai_test.go +++ b/internal/memory/embed/openai_test.go @@ -256,3 +256,42 @@ func TestOpenAIAvailableNoFallbackOnServerError(t *testing.T) { t.Fatalf("expected no embedding probe after 500 models route, got %d", embedRequests) } } + +func TestOpenAIProxyEnvHonoredForRemoteEndpoints(t *testing.T) { + // Remote endpoints must resolve their proxy from the environment: + // credential gateways inject auth at the proxy boundary. + t.Setenv("MNEMON_EMBED_ENDPOINT", "http://remote.example.test:18000/v1") + t.Setenv("HTTPS_PROXY", "http://proxy.example.test:3128") + c := NewClient() + req, err := http.NewRequest(http.MethodGet, "https://api.example.test/v1/models", nil) + if err != nil { + t.Fatal(err) + } + proxyURL, err := c.http.Transport.(*http.Transport).Proxy(req) + if err != nil { + t.Fatalf("resolve proxy: %v", err) + } + if proxyURL == nil || proxyURL.Host != "proxy.example.test:3128" { + t.Fatalf("expected env proxy for remote endpoint, got %v", proxyURL) + } +} + +func TestOllamaLoopbackBypassesProxyEnv(t *testing.T) { + // A loopback endpoint must not be routed through an environment + // proxy, even when HTTPS_PROXY is set (local Ollama behind a stray + // corporate proxy would otherwise break). + t.Setenv("MNEMON_EMBED_ENDPOINT", "http://127.0.0.1:11434") + t.Setenv("HTTPS_PROXY", "http://proxy.example.test:3128") + c := NewClient() + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:11434/api/tags", nil) + if err != nil { + t.Fatal(err) + } + proxyURL, err := c.http.Transport.(*http.Transport).Proxy(req) + if err != nil { + t.Fatalf("resolve proxy: %v", err) + } + if proxyURL != nil { + t.Fatalf("expected no proxy for loopback endpoint, got %v", proxyURL) + } +}