mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(terraform): read key fields the proxy stores in metadata back from /key/info (#40513)
Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
5264f8ed48
commit
87e2026f0a
3 changed files with 117 additions and 0 deletions
|
|
@ -39,6 +39,7 @@ longer signal it.
|
|||
|
||||
- **team**: Read now decodes the `team_info` envelope `/team/info` actually returns, so team attributes refresh from the proxy instead of always falling back to the prior state
|
||||
- **key**: Read now unwraps the `info` envelope `/key/info` actually returns; previously reads mapped nothing back into state, so drift on a key was never detected
|
||||
- **key**: Read now picks up `model_rpm_limit`, `model_tpm_limit`, `guardrails`, `tags`, `enforced_params`, `allowed_passthrough_routes`, `rpm_limit_type`, `tpm_limit_type` and `prompts` from `info.metadata`, where the proxy actually stores them; previously they stayed empty in state, so a matching config showed a permanent phantom diff on them and out-of-band changes to them were never detected
|
||||
- **key**: Updates no longer send an empty `budget_duration`, which the proxy rejects with a 400; any update to a key without a configured `budget_duration` previously failed outright
|
||||
- **key**: A config-supplied `key` value (write-only) is now forwarded to `/key/generate`; previously it was silently dropped and the proxy generated a random key instead
|
||||
- **security**: The `litellm_key` data source and `litellm_key_block` resource normalize raw `sk-` keys to their SHA-256 token hash before building request URLs and resource IDs, so plaintext keys no longer land in reverse-proxy access logs, Terraform plan output, or state IDs
|
||||
|
|
|
|||
|
|
@ -87,12 +87,40 @@ func (c *Client) GetKey(keyID string) (*Key, error) {
|
|||
info["key"] = k
|
||||
}
|
||||
}
|
||||
hoistKeyFieldsStoredInMetadata(info)
|
||||
return c.parseKeyResponse(info)
|
||||
}
|
||||
|
||||
return c.parseKeyResponse(resp)
|
||||
}
|
||||
|
||||
var keyFieldsStoredInMetadata = []string{
|
||||
"model_rpm_limit",
|
||||
"model_tpm_limit",
|
||||
"guardrails",
|
||||
"tags",
|
||||
"enforced_params",
|
||||
"allowed_passthrough_routes",
|
||||
"rpm_limit_type",
|
||||
"tpm_limit_type",
|
||||
"prompts",
|
||||
}
|
||||
|
||||
func hoistKeyFieldsStoredInMetadata(info map[string]interface{}) {
|
||||
metadata, ok := info["metadata"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, field := range keyFieldsStoredInMetadata {
|
||||
if existing, present := info[field]; present && existing != nil {
|
||||
continue
|
||||
}
|
||||
if v, present := metadata[field]; present {
|
||||
info[field] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) UpdateKey(key *Key) (*Key, error) {
|
||||
// Create a new map with only the fields that can be updated
|
||||
updateData := map[string]interface{}{
|
||||
|
|
|
|||
|
|
@ -394,6 +394,94 @@ func TestGetKeyUnwrapsInfoEnvelope(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetKeyReadsFieldsStoredInMetadata(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{
|
||||
"key": "hash-1",
|
||||
"info": {
|
||||
"models": ["gpt-4o-mini"],
|
||||
"metadata": {
|
||||
"team": "core-infra",
|
||||
"model_rpm_limit": {"gpt-4o-mini": 7},
|
||||
"model_tpm_limit": {"gpt-4o-mini": 10000},
|
||||
"guardrails": ["pii-guard"],
|
||||
"tags": ["prod"],
|
||||
"enforced_params": ["user"],
|
||||
"allowed_passthrough_routes": ["/v1/foo"],
|
||||
"rpm_limit_type": "guaranteed_throughput",
|
||||
"tpm_limit_type": "dynamic",
|
||||
"prompts": ["p1"]
|
||||
}
|
||||
}
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
key, err := client.GetKey("hash-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetKey returned error: %v", err)
|
||||
}
|
||||
if got, ok := key.ModelRPMLimit["gpt-4o-mini"].(float64); !ok || got != 7 {
|
||||
t.Errorf("ModelRPMLimit = %v, want gpt-4o-mini=7 read from metadata", key.ModelRPMLimit)
|
||||
}
|
||||
if got, ok := key.ModelTPMLimit["gpt-4o-mini"].(float64); !ok || got != 10000 {
|
||||
t.Errorf("ModelTPMLimit = %v, want gpt-4o-mini=10000 read from metadata", key.ModelTPMLimit)
|
||||
}
|
||||
if len(key.Guardrails) != 1 || key.Guardrails[0] != "pii-guard" {
|
||||
t.Errorf("Guardrails = %v, want [pii-guard]", key.Guardrails)
|
||||
}
|
||||
if len(key.Tags) != 1 || key.Tags[0] != "prod" {
|
||||
t.Errorf("Tags = %v, want [prod]", key.Tags)
|
||||
}
|
||||
if len(key.EnforcedParams) != 1 || key.EnforcedParams[0] != "user" {
|
||||
t.Errorf("EnforcedParams = %v, want [user]", key.EnforcedParams)
|
||||
}
|
||||
if len(key.AllowedPassthroughRoutes) != 1 || key.AllowedPassthroughRoutes[0] != "/v1/foo" {
|
||||
t.Errorf("AllowedPassthroughRoutes = %v, want [/v1/foo]", key.AllowedPassthroughRoutes)
|
||||
}
|
||||
if key.RPMLimitType != "guaranteed_throughput" || key.TPMLimitType != "dynamic" {
|
||||
t.Errorf("limit types = %q/%q, want guaranteed_throughput/dynamic", key.RPMLimitType, key.TPMLimitType)
|
||||
}
|
||||
if len(key.Prompts) != 1 || key.Prompts[0] != "p1" {
|
||||
t.Errorf("Prompts = %v, want [p1]", key.Prompts)
|
||||
}
|
||||
if key.Metadata["team"] != "core-infra" {
|
||||
t.Errorf("Metadata = %v, want team=core-infra preserved", key.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetKeyPrefersTopLevelOverMetadataCopy(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{
|
||||
"key": "hash-1",
|
||||
"info": {
|
||||
"tags": ["top-level"],
|
||||
"guardrails": null,
|
||||
"metadata": {
|
||||
"tags": ["from-metadata"],
|
||||
"guardrails": ["from-metadata"]
|
||||
}
|
||||
}
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
key, err := client.GetKey("hash-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetKey returned error: %v", err)
|
||||
}
|
||||
if len(key.Tags) != 1 || key.Tags[0] != "top-level" {
|
||||
t.Errorf("Tags = %v, want [top-level]", key.Tags)
|
||||
}
|
||||
if len(key.Guardrails) != 1 || key.Guardrails[0] != "from-metadata" {
|
||||
t.Errorf("Guardrails = %v, want [from-metadata] (null top-level must not shadow)", key.Guardrails)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceKeyReadDropsMissingKeyFromState(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue