mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
feat(terraform): expose server_metadata on litellm_key so undeclared metadata is visible (#42453)
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
21c442759e
commit
2ea43214c9
4 changed files with 62 additions and 0 deletions
|
|
@ -16,6 +16,7 @@ longer signal it.
|
|||
|
||||
### Added
|
||||
|
||||
- **key**: Computed `server_metadata` attribute on `litellm_key` exposing every metadata entry the proxy stores, so metadata created outside Terraform is visible in state and drift on it shows on refresh, while `metadata` keeps tracking only the declared entries and updates keep preserving undeclared ones
|
||||
- **team_member_add**: `tpm_limit`, `rpm_limit`, `budget_duration`, and `allowed_models` attributes on `litellm_team_member_add`, applied to every member of the resource; `budget_duration` and `allowed_models` ride on `/team/member_add`, while the limits are sent through `/team/member_update`, which is where the proxy accepts them
|
||||
- **team**: Optional `team_id` argument on `litellm_team`, so teams can be created with a stable, human-readable ID instead of a provider-generated UUID; changing it forces replacement
|
||||
- `litellm_jwt_key_mapping` accepts `token_id` as an alternative to `key`, so a
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ In addition to all arguments above, the following attributes are exported:
|
|||
|
||||
* `key` - The generated API key. This is the actual key value that will be used for authentication.
|
||||
|
||||
* `server_metadata` - Map of every metadata entry the proxy stores for this key, including entries not declared in `metadata`, so drift on them is visible on refresh. Entries already exposed as their own attributes (`model_rpm_limit`, `model_tpm_limit`, `tags`, `guardrails`, `enforced_params`, `allowed_passthrough_routes`, `rpm_limit_type`, `tpm_limit_type`, `prompts`) are omitted and non-string values are JSON encoded. Terraform never writes it; `metadata` still tracks only the entries declared in the configuration.
|
||||
|
||||
* `spend` - The current spend for this key. This reflects the total amount spent using this key so far.
|
||||
|
||||
## State Management
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/go-cty/cty"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
||||
|
|
@ -61,6 +62,12 @@ func resourceKey() *schema.Resource {
|
|||
Optional: true,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
},
|
||||
"server_metadata": {
|
||||
Type: schema.TypeMap,
|
||||
Computed: true,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
Description: "Every metadata entry the proxy stores for this key, including ones not declared in metadata. Read-only, so drift on undeclared entries shows up on refresh without Terraform taking ownership of them. Entries the provider already exposes as their own attributes (model_rpm_limit, model_tpm_limit, tags, guardrails, enforced_params, allowed_passthrough_routes, rpm_limit_type, tpm_limit_type, prompts) are omitted, and non-string values are JSON encoded",
|
||||
},
|
||||
"tpm_limit": {
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
|
|
@ -304,6 +311,7 @@ func resourceKeyRead(ctx context.Context, d *schema.ResourceData, m interface{})
|
|||
return nil
|
||||
}
|
||||
|
||||
d.Set("server_metadata", serverKeyMetadata(key.Metadata))
|
||||
key.Metadata = declaredKeyMetadata(key.Metadata, d.Get("metadata").(map[string]interface{}))
|
||||
mapKeyToResourceData(d, key)
|
||||
return nil
|
||||
|
|
@ -395,6 +403,25 @@ func declaredKeyMetadata(server, declared map[string]interface{}) map[string]int
|
|||
return result
|
||||
}
|
||||
|
||||
func serverKeyMetadata(server map[string]interface{}) map[string]string {
|
||||
result := make(map[string]string, len(server))
|
||||
for k, v := range server {
|
||||
if slices.Contains(keyFieldsStoredInMetadata, k) {
|
||||
continue
|
||||
}
|
||||
if s, ok := v.(string); ok {
|
||||
result[k] = s
|
||||
continue
|
||||
}
|
||||
encoded, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result[k] = string(encoded)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeKeyMetadata(server, oldDeclared, newDeclared map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{}, len(server)+len(newDeclared))
|
||||
for k, v := range server {
|
||||
|
|
|
|||
|
|
@ -600,6 +600,12 @@ func TestKeyUpdateWithoutMetadataChangePreservesServerMetadata(t *testing.T) {
|
|||
if got := newState.Attributes["metadata.a"]; got != "1" {
|
||||
t.Errorf("metadata.a = %q, want 1", got)
|
||||
}
|
||||
if got := newState.Attributes["server_metadata.server_side"]; got != "x" {
|
||||
t.Errorf("server_metadata.server_side = %q, want x", got)
|
||||
}
|
||||
if _, present := proxy.updates[0]["server_metadata"]; present {
|
||||
t.Errorf("computed server_metadata was sent on /key/update: %v", proxy.updates[0]["server_metadata"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyUpdateWithMetadataChangeMergesOverServerMetadata(t *testing.T) {
|
||||
|
|
@ -654,6 +660,32 @@ func TestKeyReadKeepsOnlyDeclaredMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKeyReadExposesUndeclaredMetadataInServerMetadata(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)},
|
||||
"nested": map[string]interface{}{"k": "v"},
|
||||
}}
|
||||
srv := httptest.NewServer(proxy.handler())
|
||||
defer srv.Close()
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
|
||||
d := newKeyResourceData(t, map[string]interface{}{"metadata": map[string]interface{}{"a": "1"}})
|
||||
d.SetId("hash-1")
|
||||
if diags := resourceKeyRead(context.Background(), d, client); diags.HasError() {
|
||||
t.Fatalf("Read returned error: %v", diags)
|
||||
}
|
||||
|
||||
if got, want := d.Get("metadata"), map[string]interface{}{"a": "1"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("metadata in state = %v, want %v", got, want)
|
||||
}
|
||||
want := map[string]interface{}{"a": "1", "server_side": "x", "nested": `{"k":"v"}`}
|
||||
if got := d.Get("server_metadata"); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("server_metadata in state = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyUpdateSendsChangedDuration(t *testing.T) {
|
||||
proxy := &fakeKeyProxy{metadata: map[string]interface{}{}}
|
||||
srv := httptest.NewServer(proxy.handler())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue