From 099c59944912b2289b74f00359519f2bb464b227 Mon Sep 17 00:00:00 2001 From: Ziv-Wenrix Date: Tue, 27 Oct 2026 07:56:23 +0200 Subject: [PATCH] fix(terraform): honor HTTP_PROXY/HTTPS_PROXY in the provider's HTTP client NewClient built a bare http.Transport to set InsecureSkipVerify, which has no Proxy func, so the provider ignored the standard proxy environment variables and always dialed the LiteLLM gateway directly. Clone http.DefaultTransport instead and only override the TLS config, which keeps ProxyFromEnvironment and Go's default dial/idle settings. Same approach as BerriAI/terraform-provider-litellm#40, which was opened against the release mirror and never landed here. Co-Authored-By: Claude Opus 5.5 --- terraform/provider/CHANGELOG.md | 1 + terraform/provider/litellm/client.go | 7 ++++--- terraform/provider/litellm/client_test.go | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/terraform/provider/CHANGELOG.md b/terraform/provider/CHANGELOG.md index 597e0c6d4d8..110cc0823d8 100644 --- a/terraform/provider/CHANGELOG.md +++ b/terraform/provider/CHANGELOG.md @@ -52,6 +52,7 @@ longer signal it. ### Fixed - **model**: `additional_drop_params` in `litellm_model.additional_litellm_params` is now sent to the proxy as the deployment's `additional_drop_params` list, the same list a `model_list` entry takes. Before, the provider never sent the list and instead deleted every key it named from `litellm_params`, so a `store` or `vertex_location` listed there was lost and requests still carried the parameters (#38349). The value must be a JSON list of parameter names such as `jsonencode(["store"])`, which `terraform plan` now checks, and `null` entries are skipped as before. When the configuration sets the list, refresh reads the stored list back from the proxy, so a list changed outside Terraform shows up as drift. On a model an older provider created, the proxy has no stored list, so the first plan after upgrading shows the list and every listed key the configuration sets as changes, since that apply writes them. A write-only secret such as `model_api_key_wo` is never shown in a plan +- **provider**: The provider now honors the standard `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` environment variables. Its HTTP client was a bare `http.Transport`, built that way to support `insecure_skip_verify`, which left out Go's `ProxyFromEnvironment`, so every request went direct even when the LiteLLM gateway is only reachable through a forward proxy (for example a Tailscale userspace-networking proxy on a CI runner) - **user**: `litellm_user` updates no longer send `blocked`, which `/user/update` writes into the user table where no such column exists, so every update failed with a 400 (`Could not find field at upsertOneLiteLLM_UserTable.create.blocked`). Create still sends `blocked`, including `false`, so a `default_internal_user_params.blocked` on the proxy cannot block the auto-created key. Changing `blocked` after creation has no effect, since the proxy has no user-level block; use `litellm_key_block` instead - **model**: `litellm_model` refresh now reads the `{"data": [...]}` envelope `/model/info` returns, so `model_info` fields changed outside Terraform show up as drift instead of silently keeping the previous state - **key**: An update that changes `team_id` and fails because the key was already cascade-deleted along with its previous team now recovers by recreating the key under the new team, instead of aborting the apply. The key's absence is confirmed against the proxy first, so an unrelated failure still errors out, and a `team_id` change between two teams that both still exist stays a plain in-place update diff --git a/terraform/provider/litellm/client.go b/terraform/provider/litellm/client.go index 28a89f1eafd..51e640ea791 100644 --- a/terraform/provider/litellm/client.go +++ b/terraform/provider/litellm/client.go @@ -35,9 +35,10 @@ func isNotFound(err error) bool { } func NewClient(apiBase, apiKey string, insecureSkipVerify bool) *Client { - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}, - } + // Clone the default transport so the client keeps its proxy (HTTP_PROXY, HTTPS_PROXY, NO_PROXY), + // dial and idle-connection settings; a bare http.Transport has no Proxy func and ignores them. + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: insecureSkipVerify} return &Client{ APIBase: apiBase, diff --git a/terraform/provider/litellm/client_test.go b/terraform/provider/litellm/client_test.go index 77c7bf817d8..054010af4c3 100644 --- a/terraform/provider/litellm/client_test.go +++ b/terraform/provider/litellm/client_test.go @@ -4,6 +4,7 @@ import ( "errors" "net/http" "net/http/httptest" + "reflect" "strings" "testing" ) @@ -62,6 +63,23 @@ func TestSendRequestAcceptsFullSuccessRange(t *testing.T) { } } +func TestNewClientHonorsProxyEnvironment(t *testing.T) { + for _, insecure := range []bool{false, true} { + c := NewClient("https://litellm.example.com", "sk-test", insecure) + + tr, ok := c.httpClient.Transport.(*http.Transport) + if !ok { + t.Fatalf("transport is %T, want *http.Transport", c.httpClient.Transport) + } + if tr.Proxy == nil || reflect.ValueOf(tr.Proxy).Pointer() != reflect.ValueOf(http.ProxyFromEnvironment).Pointer() { + t.Errorf("insecure=%v: transport does not use http.ProxyFromEnvironment, so HTTP_PROXY/HTTPS_PROXY are ignored", insecure) + } + if tr.TLSClientConfig == nil || tr.TLSClientConfig.InsecureSkipVerify != insecure { + t.Errorf("insecure=%v: TLSClientConfig.InsecureSkipVerify not set from the argument", insecure) + } + } +} + func TestRedactSensitiveDataNestedCredentialValues(t *testing.T) { c := NewClient("http://localhost:4000", "sk-test", false)