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:
devin-ai-integration[bot] 2026-09-10 03:10:42 +00:00 committed by GitHub
parent 5264f8ed48
commit 87e2026f0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 117 additions and 0 deletions

View file

@ -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

View file

@ -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{}{

View file

@ -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")