mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
fix(terraform-provider): apply custom headers on all request paths
MakeRequest was missing custom_headers, so most resources never sent them. Share header application, build the client from ProviderConfig, and drop credential headers on cross-origin redirects
This commit is contained in:
parent
441ec017da
commit
00ec28d121
9 changed files with 152 additions and 49 deletions
|
|
@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- **provider**: Optional `custom_headers` map to send extra HTTP headers on every LiteLLM API request (e.g. proxy or gateway headers). `x-api-key` from `api_key` always takes precedence
|
||||
- **provider**: Optional `custom_headers` map to send extra HTTP headers on every LiteLLM API request (e.g. proxy or gateway headers). Applied by both request helpers. `x-api-key` from `api_key` always takes precedence. Cross-origin redirects drop custom headers and `x-api-key`
|
||||
|
||||
## [0.4.0] - 2026-08-06
|
||||
|
||||
|
|
|
|||
|
|
@ -20,18 +20,48 @@ type Client struct {
|
|||
CustomHeaders map[string]string
|
||||
}
|
||||
|
||||
func NewClient(apiBase, apiKey string, insecureSkipVerify bool) *Client {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify},
|
||||
func NewClient(cfg ProviderConfig) *Client {
|
||||
headers := make(map[string]string, len(cfg.CustomHeaders))
|
||||
for k, v := range cfg.CustomHeaders {
|
||||
headers[k] = v
|
||||
}
|
||||
|
||||
return &Client{
|
||||
APIBase: apiBase,
|
||||
APIKey: apiKey,
|
||||
httpClient: &http.Client{Transport: tr},
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
CustomHeaders: map[string]string{},
|
||||
c := &Client{
|
||||
APIBase: cfg.APIBase,
|
||||
APIKey: cfg.APIKey,
|
||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||
CustomHeaders: headers,
|
||||
}
|
||||
c.httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify},
|
||||
},
|
||||
CheckRedirect: c.checkRedirect,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) checkRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return fmt.Errorf("stopped after 10 redirects")
|
||||
}
|
||||
orig := via[0].URL
|
||||
if req.URL.Scheme != orig.Scheme || req.URL.Host != orig.Host {
|
||||
for k := range c.CustomHeaders {
|
||||
req.Header.Del(k)
|
||||
}
|
||||
req.Header.Del("x-api-key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) applyRequestHeaders(req *http.Request) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("accept", "application/json")
|
||||
for k, v := range c.CustomHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
req.Header.Set("x-api-key", c.APIKey)
|
||||
}
|
||||
|
||||
// Organization member methods
|
||||
|
|
@ -281,12 +311,7 @@ func (c *Client) sendRequest(method, path string, body interface{}) (map[string]
|
|||
return nil, fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("accept", "application/json")
|
||||
for k, v := range c.CustomHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
req.Header.Set("x-api-key", c.APIKey)
|
||||
c.applyRequestHeaders(req)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,22 @@ package litellm
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testClient(apiBase, apiKey string, insecure bool, headers map[string]string) *Client {
|
||||
return NewClient(ProviderConfig{
|
||||
APIBase: apiBase,
|
||||
APIKey: apiKey,
|
||||
InsecureSkipVerify: insecure,
|
||||
CustomHeaders: headers,
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendRequestCustomHeaders(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("X-Proxy-Auth"); got != "proxy-token" {
|
||||
|
|
@ -24,20 +34,84 @@ func TestSendRequestCustomHeaders(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "sk-real-key", false)
|
||||
client.CustomHeaders = map[string]string{
|
||||
client := testClient(srv.URL, "sk-real-key", false, map[string]string{
|
||||
"X-Proxy-Auth": "proxy-token",
|
||||
"X-Request-Source": "terraform",
|
||||
"x-api-key": "should-not-win",
|
||||
}
|
||||
})
|
||||
|
||||
if _, err := client.sendRequest("GET", "/health", nil); err != nil {
|
||||
t.Fatalf("sendRequest: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestCustomHeaders(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("X-Proxy-Auth"); got != "proxy-token" {
|
||||
t.Errorf("X-Proxy-Auth = %q, want %q", got, "proxy-token")
|
||||
}
|
||||
if got := r.Header.Get("x-api-key"); got != "sk-real-key" {
|
||||
t.Errorf("x-api-key = %q, want configured API key (must not be overridden)", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := testClient(srv.URL, "sk-real-key", false, map[string]string{
|
||||
"X-Proxy-Auth": "proxy-token",
|
||||
"x-api-key": "should-not-win",
|
||||
})
|
||||
|
||||
resp, err := MakeRequest(client, "GET", "/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("MakeRequest: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossOriginRedirectStripsSensitiveHeaders(t *testing.T) {
|
||||
var finalAuth, finalAPIKey string
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
finalAuth = r.Header.Get("X-Proxy-Auth")
|
||||
finalAPIKey = r.Header.Get("x-api-key")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
||||
}))
|
||||
defer target.Close()
|
||||
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("X-Proxy-Auth"); got != "proxy-token" {
|
||||
t.Errorf("origin X-Proxy-Auth = %q, want proxy-token", got)
|
||||
}
|
||||
http.Redirect(w, r, target.URL+"/final", http.StatusFound)
|
||||
}))
|
||||
defer origin.Close()
|
||||
|
||||
client := testClient(origin.URL, "sk-real-key", false, map[string]string{
|
||||
"X-Proxy-Auth": "proxy-token",
|
||||
})
|
||||
|
||||
resp, err := MakeRequest(client, "GET", "/start", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("MakeRequest: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
|
||||
if finalAuth != "" {
|
||||
t.Errorf("cross-origin redirect kept X-Proxy-Auth = %q", finalAuth)
|
||||
}
|
||||
if finalAPIKey != "" {
|
||||
t.Errorf("cross-origin redirect kept x-api-key = %q", finalAPIKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactSensitiveDataNestedCredentialValues(t *testing.T) {
|
||||
c := NewClient("http://localhost:4000", "sk-test", false)
|
||||
c := testClient("http://localhost:4000", "sk-test", false, nil)
|
||||
|
||||
input := `{"credential_name":"azure-cred","credential_values":{"api_key":"sk-secret-123","config":{"region":"us-east-1","client_secret":"nested-secret"}}}`
|
||||
got := c.redactSensitiveData(input)
|
||||
|
|
@ -56,7 +130,7 @@ func TestRedactSensitiveDataNestedCredentialValues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactSensitiveDataDeeplyNestedSensitiveKeys(t *testing.T) {
|
||||
c := NewClient("http://localhost:4000", "sk-test", false)
|
||||
c := testClient("http://localhost:4000", "sk-test", false, nil)
|
||||
|
||||
input := `{"data":[{"litellm_params":{"model":"gpt-4","api_key":"sk-deep-456","aws_secret_access_key":"aws-secret"}}]}`
|
||||
got := c.redactSensitiveData(input)
|
||||
|
|
@ -72,7 +146,7 @@ func TestRedactSensitiveDataDeeplyNestedSensitiveKeys(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactSensitiveDataTopLevelStringFields(t *testing.T) {
|
||||
c := NewClient("http://localhost:4000", "sk-test", false)
|
||||
c := testClient("http://localhost:4000", "sk-test", false, nil)
|
||||
|
||||
input := `{"model_api_key":"sk-top-789","vertex_credentials":"{\"type\":\"service_account\"}","team_alias":"eng"}`
|
||||
got := c.redactSensitiveData(input)
|
||||
|
|
@ -88,7 +162,7 @@ func TestRedactSensitiveDataTopLevelStringFields(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRedactSensitiveDataNonJSONFallback(t *testing.T) {
|
||||
c := NewClient("http://localhost:4000", "sk-test", false)
|
||||
c := testClient("http://localhost:4000", "sk-test", false, nil)
|
||||
|
||||
input := `error before "api_key": "sk-fallback-000" after`
|
||||
got := c.redactSensitiveData(input)
|
||||
|
|
|
|||
|
|
@ -60,18 +60,23 @@ func Provider() *schema.Provider {
|
|||
|
||||
// providerConfigure configures the provider with the given schema data.
|
||||
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
|
||||
customHeaders := map[string]string{}
|
||||
if v, ok := d.GetOk("custom_headers"); ok {
|
||||
for k, val := range v.(map[string]interface{}) {
|
||||
customHeaders[k] = val.(string)
|
||||
}
|
||||
}
|
||||
|
||||
client := NewClient(
|
||||
d.Get("api_base").(string),
|
||||
d.Get("api_key").(string),
|
||||
d.Get("insecure_skip_verify").(bool),
|
||||
)
|
||||
client.CustomHeaders = customHeaders
|
||||
return client, nil
|
||||
return NewClient(ProviderConfig{
|
||||
APIBase: d.Get("api_base").(string),
|
||||
APIKey: d.Get("api_key").(string),
|
||||
InsecureSkipVerify: d.Get("insecure_skip_verify").(bool),
|
||||
CustomHeaders: customHeadersFromSchema(d),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func customHeadersFromSchema(d *schema.ResourceData) map[string]string {
|
||||
v, ok := d.GetOk("custom_headers")
|
||||
if !ok {
|
||||
return map[string]string{}
|
||||
}
|
||||
raw := v.(map[string]interface{})
|
||||
headers := make(map[string]string, len(raw))
|
||||
for k, val := range raw {
|
||||
headers[k] = val.(string)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func createTestUsers(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
client := NewClient(apiBase, apiKey, false)
|
||||
client := NewClient(ProviderConfig{APIBase: apiBase, APIKey: apiKey, InsecureSkipVerify: false})
|
||||
|
||||
// Create test users
|
||||
users := []map[string]interface{}{
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func TestRetryCredentialRead_SuccessOnFirstAttempt(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := newTestResourceData(t, "test-cred")
|
||||
|
||||
err := retryCredentialRead(d, client, 3)
|
||||
|
|
@ -72,7 +72,7 @@ func TestRetryCredentialRead_SuccessAfterRetries(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := newTestResourceData(t, "test-cred")
|
||||
|
||||
err := retryCredentialRead(d, client, 3)
|
||||
|
|
@ -93,7 +93,7 @@ func TestRetryCredentialRead_ExhaustsRetries(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := newTestResourceData(t, "test-cred")
|
||||
|
||||
err := retryCredentialRead(d, client, 2)
|
||||
|
|
@ -118,7 +118,7 @@ func TestRetryCredentialRead_NonRetryableError(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := newTestResourceData(t, "test-cred")
|
||||
|
||||
err := retryCredentialRead(d, client, 3)
|
||||
|
|
@ -154,7 +154,7 @@ func TestRetryCredentialRead_IDRestoredBetweenRetries(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := newTestResourceData(t, "my-cred")
|
||||
|
||||
err := retryCredentialRead(d, client, 2)
|
||||
|
|
@ -172,7 +172,7 @@ func TestRetryCredentialRead_MaxRetriesOne(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := newTestResourceData(t, "test-cred")
|
||||
|
||||
err := retryCredentialRead(d, client, 1)
|
||||
|
|
@ -189,7 +189,7 @@ func TestRetryCredentialRead_ConnectionError(t *testing.T) {
|
|||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := newTestResourceData(t, "test-cred")
|
||||
|
||||
err := retryCredentialRead(d, client, 1)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func TestTeamMemberUpdateSendsRole(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMTeamMember().Schema, map[string]interface{}{
|
||||
"team_id": "team-1",
|
||||
"user_id": "user-1",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func TestVectorStoreReadDoesNotPersistServerLitellmParams(t *testing.T) {
|
|||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
client := NewClient(ProviderConfig{APIBase: srv.URL, APIKey: "test-key", InsecureSkipVerify: true})
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMVectorStore().Schema, map[string]interface{}{
|
||||
"vector_store_name": "kb",
|
||||
"custom_llm_provider": "openai",
|
||||
|
|
|
|||
|
|
@ -79,8 +79,7 @@ func MakeRequest(client *Client, method, endpoint string, body interface{}) (*ht
|
|||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-api-key", client.APIKey)
|
||||
client.applyRequestHeaders(req)
|
||||
|
||||
return client.httpClient.Do(req)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue