Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions internal/memory/embed/ollama.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
39 changes: 39 additions & 0 deletions internal/memory/embed/openai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading