This commit is contained in:
cosminfrincu 2026-10-27 02:25:20 +00:00 • committed by GitHub
commit cd9ac46505
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 92 additions and 0 deletions

View file

@ -51,6 +51,7 @@ longer signal it.
### Fixed
- **key**: Updates no longer resend unchanged `allowed_passthrough_routes`, avoiding an Enterprise-only 403 on OSS proxies when passthrough metadata is managed outside Terraform
- **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
- **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

View file

@ -372,6 +372,9 @@ func resourceKeyUpdate(ctx context.Context, d *schema.ResourceData, m interface{
if allowedRoutesNotConfigured(d) {
key.AllowedRoutes = nil
}
if !d.HasChange("allowed_passthrough_routes") {
key.AllowedPassthroughRoutes = nil
}
if !d.HasChange("duration") {
key.Duration = ""
}

View file

@ -1,6 +1,7 @@
package litellm
import (
"bytes"
"context"
"encoding/json"
"fmt"
@ -965,6 +966,93 @@ func applyKeyUpdate(t *testing.T, client *Client, stateAttrs map[string]string,
return newState
}
func TestKeyUpdateOmitsUnchangedPassthroughRoutes(t *testing.T) {
for _, field := range []string{"models", "allowed_routes"} {
t.Run(field, func(t *testing.T) {
const route = "/direct/openai/v1/batches"
proxy := &fakeKeyProxy{metadata: map[string]interface{}{
"allowed_passthrough_routes": []string{route},
"server_side": "preserved",
}}
handler := proxy.handler()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/key/update" {
body, err := io.ReadAll(r.Body)
if err != nil {
t.Error(err)
http.Error(w, "invalid test request", http.StatusBadRequest)
return
}
var payload map[string]interface{}
if err := json.Unmarshal(body, &payload); err != nil {
t.Error(err)
http.Error(w, "invalid test request", http.StatusBadRequest)
return
}
if _, present := payload["allowed_passthrough_routes"]; present {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"error":"Enterprise required: allowed_passthrough_routes"}`))
return
}
r.Body = io.NopCloser(bytes.NewReader(body))
}
handler(w, r)
}))
defer srv.Close()
applyKeyUpdate(t, NewClient(srv.URL, "test-key", true),
map[string]string{
"key_alias": "alias-1",
"allowed_passthrough_routes.#": "1",
"allowed_passthrough_routes.0": route,
"blocked": "true",
field + ".#": "1",
field + ".0": "previous",
},
map[string]interface{}{
"key_alias": "alias-1",
"allowed_passthrough_routes": []interface{}{route},
"blocked": true,
field: []interface{}{"replacement"},
},
)
if len(proxy.updates) != 1 {
t.Fatalf("expected one update, got %d", len(proxy.updates))
}
if got := proxy.updates[0][field]; !reflect.DeepEqual(got, []interface{}{"replacement"}) {
t.Errorf("%s = %v, want [replacement]", field, got)
}
if proxy.updates[0]["blocked"] != true {
t.Error("update must preserve the operator's block")
}
if proxy.metadata["server_side"] != "preserved" ||
!reflect.DeepEqual(proxy.metadata["allowed_passthrough_routes"], []string{route}) {
t.Errorf("server metadata changed: %v", proxy.metadata)
}
})
}
}
func TestKeyUpdateSendsChangedPassthroughRoutes(t *testing.T) {
proxy := &fakeKeyProxy{metadata: map[string]interface{}{}}
srv := httptest.NewServer(proxy.handler())
defer srv.Close()
applyKeyUpdate(t, NewClient(srv.URL, "test-key", true),
map[string]string{
"key_alias": "alias-1",
"allowed_passthrough_routes.#": "1",
"allowed_passthrough_routes.0": "/previous",
},
map[string]interface{}{
"key_alias": "alias-1",
"allowed_passthrough_routes": []interface{}{"/replacement"},
},
)
if got := proxy.updates[0]["allowed_passthrough_routes"]; !reflect.DeepEqual(got, []interface{}{"/replacement"}) {
t.Errorf("changed passthrough routes = %v, want [/replacement]", got)
}
}
func TestKeyUpdateWithoutMetadataChangePreservesServerMetadata(t *testing.T) {
proxy := &fakeKeyProxy{metadata: map[string]interface{}{"a": "1", "server_side": "x", "model_rpm_limit": map[string]interface{}{"gpt-4o-mini": float64(5)}}}
srv := httptest.NewServer(proxy.handler())