mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
Merge pull request #38682 from BerriAI/devin_ai_1787937848_tf_team_member_fields
feat(terraform): add tpm_limit, rpm_limit, budget_duration, allowed_models to litellm_team_member_add
This commit is contained in:
commit
d35af8d302
4 changed files with 403 additions and 20 deletions
|
|
@ -16,6 +16,7 @@ longer signal it.
|
|||
|
||||
### Added
|
||||
|
||||
- **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
|
||||
- **jwt_key_mapping**: New `litellm_jwt_key_mapping` resource for the proxy's JWT to virtual key mappings, so JWT clients identified by a claim (`client_id`, `azp`, `sub`) map to virtual keys and inherit their models, budgets and rate limits. Supports `description` and `is_active`, rotating the mapped key in place, and forces replacement when the claim name or value changes
|
||||
- **team**: `soft_budget`, `tags`, and `soft_budget_alerting_emails` attributes on `litellm_team`, matching what `/team/new` and `/team/update` already accept; `soft_budget_alerting_emails` is sent under `metadata`, where the proxy reads it
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ resource "litellm_team_member_add" "example" {
|
|||
}
|
||||
|
||||
max_budget_in_team = 100.0
|
||||
budget_duration = "30d"
|
||||
tpm_limit = 100000
|
||||
rpm_limit = 100
|
||||
allowed_models = ["gpt-4"]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -152,6 +156,12 @@ resource "litellm_team_member_add" "budget_example" {
|
|||
* `user_email` - (Optional) The email of the user to add to the team.
|
||||
* `role` - (Required) The role of the user in the team. Must be one of: "admin" or "user".
|
||||
* `max_budget_in_team` - (Optional) The maximum budget allocated for the team members.
|
||||
* `budget_duration` - (Optional) Duration after which each member's budget resets, for example "1h", "24h", "7d", "30d". If not set, the budget never resets.
|
||||
* `tpm_limit` - (Optional) Tokens per minute limit applied to each team member. Sent via `/team/member_update` after members are added, since `/team/member_add` does not accept it.
|
||||
* `rpm_limit` - (Optional) Requests per minute limit applied to each team member. Sent via `/team/member_update` after members are added, since `/team/member_add` does not accept it.
|
||||
* `allowed_models` - (Optional) List of models each team member can access. If not set, members inherit the team's `default_team_member_models` or all team models.
|
||||
|
||||
Removing `budget_duration`, `tpm_limit`, `rpm_limit`, or `allowed_models` from the configuration clears that setting on every member through `/team/member_update`.
|
||||
|
||||
## Import
|
||||
|
||||
|
|
|
|||
|
|
@ -49,10 +49,105 @@ func resourceLiteLLMTeamMemberAdd() *schema.Resource {
|
|||
Type: schema.TypeFloat,
|
||||
Optional: true,
|
||||
},
|
||||
"tpm_limit": {
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
},
|
||||
"rpm_limit": {
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
},
|
||||
"budget_duration": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
"allowed_models": {
|
||||
Type: schema.TypeList,
|
||||
Optional: true,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expandAllowedModels(raw []interface{}) []string {
|
||||
models := make([]string, 0, len(raw))
|
||||
for _, m := range raw {
|
||||
models = append(models, m.(string))
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func applyAddOnlySettings(d *schema.ResourceData, payload map[string]interface{}) {
|
||||
if v, ok := d.GetOk("budget_duration"); ok {
|
||||
payload["budget_duration"] = v.(string)
|
||||
}
|
||||
if v, ok := d.GetOk("allowed_models"); ok {
|
||||
payload["allowed_models"] = expandAllowedModels(v.([]interface{}))
|
||||
}
|
||||
}
|
||||
|
||||
func applyLimits(d *schema.ResourceData, payload map[string]interface{}) {
|
||||
for _, key := range []string{"tpm_limit", "rpm_limit"} {
|
||||
if v, ok := d.GetOk(key); ok {
|
||||
payload[key] = v.(int)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyUpdateSettings(d *schema.ResourceData, payload map[string]interface{}) {
|
||||
applyAddOnlySettings(d, payload)
|
||||
applyLimits(d, payload)
|
||||
for _, key := range []string{"tpm_limit", "rpm_limit", "budget_duration"} {
|
||||
if _, ok := d.GetOk(key); !ok && d.HasChange(key) {
|
||||
payload[key] = nil
|
||||
}
|
||||
}
|
||||
if _, ok := d.GetOk("allowed_models"); !ok && d.HasChange("allowed_models") {
|
||||
payload["allowed_models"] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func memberIdentity(member map[string]interface{}, payload map[string]interface{}) {
|
||||
if userID, ok := member["user_id"].(string); ok && userID != "" {
|
||||
payload["user_id"] = userID
|
||||
}
|
||||
if userEmail, ok := member["user_email"].(string); ok && userEmail != "" {
|
||||
payload["user_email"] = userEmail
|
||||
}
|
||||
}
|
||||
|
||||
// tpm/rpm limits are only accepted by /team/member_update, not /team/member_add
|
||||
func setMemberLimits(client *Client, d *schema.ResourceData, teamID string, members []map[string]interface{}) error {
|
||||
limits := map[string]interface{}{}
|
||||
applyLimits(d, limits)
|
||||
if len(limits) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, member := range members {
|
||||
updateData := map[string]interface{}{
|
||||
"team_id": teamID,
|
||||
}
|
||||
for k, v := range limits {
|
||||
updateData[k] = v
|
||||
}
|
||||
memberIdentity(member, updateData)
|
||||
|
||||
log.Printf("[DEBUG] Set team member limits request payload: %+v", updateData)
|
||||
|
||||
resp, err := MakeRequest(client, "POST", "/team/member_update", updateData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting team member limits: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := handleResponse(resp, "setting team member limits"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceLiteLLMTeamMemberAddCreate(d *schema.ResourceData, m interface{}) error {
|
||||
client := m.(*Client)
|
||||
|
||||
|
|
@ -81,6 +176,7 @@ func resourceLiteLLMTeamMemberAddCreate(d *schema.ResourceData, m interface{}) e
|
|||
"team_id": teamID,
|
||||
"max_budget_in_team": maxBudget,
|
||||
}
|
||||
applyAddOnlySettings(d, memberData)
|
||||
|
||||
log.Printf("[DEBUG] Create team members request payload: %+v", memberData)
|
||||
|
||||
|
|
@ -94,9 +190,12 @@ func resourceLiteLLMTeamMemberAddCreate(d *schema.ResourceData, m interface{}) e
|
|||
return err
|
||||
}
|
||||
|
||||
// Set ID as team_id since this resource manages all members for a team
|
||||
d.SetId(teamID)
|
||||
|
||||
if err := setMemberLimits(client, d, teamID, membersList); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return resourceLiteLLMTeamMemberAddRead(d, m)
|
||||
}
|
||||
|
||||
|
|
@ -140,11 +239,13 @@ func resourceLiteLLMTeamMemberAddUpdate(d *schema.ResourceData, m interface{}) e
|
|||
// Track which members have been updated to avoid duplicates
|
||||
updatedMembers := make(map[string]bool)
|
||||
|
||||
// Check if max_budget_in_team has changed
|
||||
if d.HasChange("max_budget_in_team") {
|
||||
log.Printf("[DEBUG] max_budget_in_team changed, updating all existing members with new budget: %f", maxBudget)
|
||||
// Check if any team-wide member setting has changed
|
||||
settingsChanged := d.HasChange("max_budget_in_team") || d.HasChange("tpm_limit") || d.HasChange("rpm_limit") ||
|
||||
d.HasChange("budget_duration") || d.HasChange("allowed_models")
|
||||
if settingsChanged {
|
||||
log.Printf("[DEBUG] Member settings changed, updating all existing members")
|
||||
|
||||
// Update ALL existing members with the new budget
|
||||
// Update ALL existing members with the new settings
|
||||
for key, newMember := range newMemberMap {
|
||||
if _, exists := oldMemberMap[key]; exists {
|
||||
updateData := map[string]interface{}{
|
||||
|
|
@ -152,22 +253,18 @@ func resourceLiteLLMTeamMemberAddUpdate(d *schema.ResourceData, m interface{}) e
|
|||
"role": newMember["role"].(string),
|
||||
"max_budget_in_team": maxBudget,
|
||||
}
|
||||
if userID, ok := newMember["user_id"].(string); ok && userID != "" {
|
||||
updateData["user_id"] = userID
|
||||
}
|
||||
if userEmail, ok := newMember["user_email"].(string); ok && userEmail != "" {
|
||||
updateData["user_email"] = userEmail
|
||||
}
|
||||
applyUpdateSettings(d, updateData)
|
||||
memberIdentity(newMember, updateData)
|
||||
|
||||
log.Printf("[DEBUG] Update team member budget request payload: %+v", updateData)
|
||||
log.Printf("[DEBUG] Update team member settings request payload: %+v", updateData)
|
||||
|
||||
resp, err := MakeRequest(client, "POST", "/team/member_update", updateData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating team member budget: %v", err)
|
||||
return fmt.Errorf("error updating team member settings: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := handleResponse(resp, "updating team member budget"); err != nil {
|
||||
if err := handleResponse(resp, "updating team member settings"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -220,12 +317,8 @@ func resourceLiteLLMTeamMemberAddUpdate(d *schema.ResourceData, m interface{}) e
|
|||
"role": newMember["role"].(string),
|
||||
"max_budget_in_team": maxBudget,
|
||||
}
|
||||
if userID, ok := newMember["user_id"].(string); ok && userID != "" {
|
||||
updateData["user_id"] = userID
|
||||
}
|
||||
if userEmail, ok := newMember["user_email"].(string); ok && userEmail != "" {
|
||||
updateData["user_email"] = userEmail
|
||||
}
|
||||
applyUpdateSettings(d, updateData)
|
||||
memberIdentity(newMember, updateData)
|
||||
|
||||
log.Printf("[DEBUG] Update team member request payload: %+v", updateData)
|
||||
|
||||
|
|
@ -265,6 +358,7 @@ func resourceLiteLLMTeamMemberAddUpdate(d *schema.ResourceData, m interface{}) e
|
|||
"team_id": teamID,
|
||||
"max_budget_in_team": maxBudget,
|
||||
}
|
||||
applyAddOnlySettings(d, memberData)
|
||||
|
||||
log.Printf("[DEBUG] Adding new team members request payload: %+v", memberData)
|
||||
|
||||
|
|
@ -277,6 +371,10 @@ func resourceLiteLLMTeamMemberAddUpdate(d *schema.ResourceData, m interface{}) e
|
|||
if err := handleResponse(resp, "adding team members"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setMemberLimits(client, d, teamID, membersToAdd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return resourceLiteLLMTeamMemberAddRead(d, m)
|
||||
|
|
|
|||
274
terraform/provider/litellm/resource_team_member_add_test.go
Normal file
274
terraform/provider/litellm/resource_team_member_add_test.go
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
package litellm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
|
||||
)
|
||||
|
||||
func TestTeamMemberAddCreateSendsMemberSettings(t *testing.T) {
|
||||
var addPayload map[string]interface{}
|
||||
var updatePayloads []map[string]interface{}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var payload map[string]interface{}
|
||||
json.Unmarshal(body, &payload)
|
||||
switch r.URL.Path {
|
||||
case "/team/member_add":
|
||||
addPayload = payload
|
||||
case "/team/member_update":
|
||||
updatePayloads = append(updatePayloads, payload)
|
||||
default:
|
||||
t.Errorf("unexpected request path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMTeamMemberAdd().Schema, map[string]interface{}{
|
||||
"team_id": "team-1",
|
||||
"member": []interface{}{
|
||||
map[string]interface{}{
|
||||
"user_id": "user-1",
|
||||
"role": "user",
|
||||
},
|
||||
},
|
||||
"max_budget_in_team": 25.0,
|
||||
"tpm_limit": 1000,
|
||||
"rpm_limit": 10,
|
||||
"budget_duration": "30d",
|
||||
"allowed_models": []interface{}{"claude-opus-4-6-v1"},
|
||||
})
|
||||
|
||||
if err := resourceLiteLLMTeamMemberAddCreate(d, client); err != nil {
|
||||
t.Fatalf("create failed: %v", err)
|
||||
}
|
||||
|
||||
if addPayload["budget_duration"] != "30d" {
|
||||
t.Fatalf("member_add payload sent budget_duration %v, want 30d", addPayload["budget_duration"])
|
||||
}
|
||||
wantModels := []interface{}{"claude-opus-4-6-v1"}
|
||||
if !reflect.DeepEqual(addPayload["allowed_models"], wantModels) {
|
||||
t.Fatalf("member_add payload sent allowed_models %v, want %v", addPayload["allowed_models"], wantModels)
|
||||
}
|
||||
if _, ok := addPayload["tpm_limit"]; ok {
|
||||
t.Fatalf("member_add payload must not carry tpm_limit, got %v", addPayload["tpm_limit"])
|
||||
}
|
||||
|
||||
if len(updatePayloads) != 1 {
|
||||
t.Fatalf("expected 1 member_update call for limits, got %d", len(updatePayloads))
|
||||
}
|
||||
update := updatePayloads[0]
|
||||
if update["tpm_limit"] != float64(1000) {
|
||||
t.Fatalf("member_update payload sent tpm_limit %v, want 1000", update["tpm_limit"])
|
||||
}
|
||||
if update["rpm_limit"] != float64(10) {
|
||||
t.Fatalf("member_update payload sent rpm_limit %v, want 10", update["rpm_limit"])
|
||||
}
|
||||
if update["user_id"] != "user-1" {
|
||||
t.Fatalf("member_update payload sent user_id %v, want user-1", update["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamMemberAddCreateOmitsUnsetSettings(t *testing.T) {
|
||||
var addPayload map[string]interface{}
|
||||
updateCalls := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
switch r.URL.Path {
|
||||
case "/team/member_add":
|
||||
json.Unmarshal(body, &addPayload)
|
||||
case "/team/member_update":
|
||||
updateCalls++
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMTeamMemberAdd().Schema, map[string]interface{}{
|
||||
"team_id": "team-1",
|
||||
"member": []interface{}{
|
||||
map[string]interface{}{
|
||||
"user_id": "user-1",
|
||||
"role": "user",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := resourceLiteLLMTeamMemberAddCreate(d, client); err != nil {
|
||||
t.Fatalf("create failed: %v", err)
|
||||
}
|
||||
|
||||
for _, field := range []string{"tpm_limit", "rpm_limit", "budget_duration", "allowed_models"} {
|
||||
if _, ok := addPayload[field]; ok {
|
||||
t.Fatalf("member_add payload must not carry unset %s, got %v", field, addPayload[field])
|
||||
}
|
||||
}
|
||||
if updateCalls != 0 {
|
||||
t.Fatalf("expected no member_update calls without limits, got %d", updateCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamMemberAddCreateSetsIDBeforeLimitsFail(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/team/member_update" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"error":"boom"}`))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMTeamMemberAdd().Schema, map[string]interface{}{
|
||||
"team_id": "team-1",
|
||||
"member": []interface{}{
|
||||
map[string]interface{}{
|
||||
"user_id": "user-1",
|
||||
"role": "user",
|
||||
},
|
||||
},
|
||||
"tpm_limit": 1000,
|
||||
})
|
||||
|
||||
if err := resourceLiteLLMTeamMemberAddCreate(d, client); err == nil {
|
||||
t.Fatal("create should fail when member_update fails")
|
||||
}
|
||||
if d.Id() != "team-1" {
|
||||
t.Fatalf("resource ID = %q after failed limits call, want team-1 so Terraform can taint and recreate it", d.Id())
|
||||
}
|
||||
}
|
||||
|
||||
// newTeamMemberUpdateResourceData builds a ResourceData with one member in state
|
||||
// and a real old -> new diff on the scalar settings, so d.HasChange and d.GetOk
|
||||
// behave as they do during a real Update call
|
||||
func newTeamMemberUpdateResourceData(t *testing.T, old, new map[string]string) *schema.ResourceData {
|
||||
t.Helper()
|
||||
attrs := map[string]string{
|
||||
"team_id": "team-1",
|
||||
"member.#": "1",
|
||||
"member.1.user_id": "user-1",
|
||||
"member.1.user_email": "",
|
||||
"member.1.role": "user",
|
||||
"allowed_models.#": "0",
|
||||
"max_budget_in_team": "25",
|
||||
}
|
||||
for k, v := range old {
|
||||
attrs[k] = v
|
||||
}
|
||||
diffAttrs := map[string]*terraform.ResourceAttrDiff{}
|
||||
for k, v := range new {
|
||||
diffAttrs[k] = &terraform.ResourceAttrDiff{Old: attrs[k], New: v}
|
||||
}
|
||||
for k := range old {
|
||||
if _, ok := new[k]; !ok {
|
||||
diffAttrs[k] = &terraform.ResourceAttrDiff{Old: attrs[k], New: "", NewRemoved: true}
|
||||
}
|
||||
}
|
||||
state := &terraform.InstanceState{ID: "team-1", Attributes: attrs}
|
||||
d, err := schema.InternalMap(resourceLiteLLMTeamMemberAdd().Schema).Data(state, &terraform.InstanceDiff{Attributes: diffAttrs})
|
||||
if err != nil {
|
||||
t.Fatalf("building ResourceData returned error: %v", err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func runTeamMemberUpdate(t *testing.T, d *schema.ResourceData) []map[string]interface{} {
|
||||
t.Helper()
|
||||
var updatePayloads []map[string]interface{}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/team/member_update" {
|
||||
t.Errorf("unexpected request path: %s", r.URL.Path)
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var payload map[string]interface{}
|
||||
json.Unmarshal(body, &payload)
|
||||
updatePayloads = append(updatePayloads, payload)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if err := resourceLiteLLMTeamMemberAddUpdate(d, NewClient(srv.URL, "test-key", true)); err != nil {
|
||||
t.Fatalf("update failed: %v", err)
|
||||
}
|
||||
if len(updatePayloads) != 1 {
|
||||
t.Fatalf("expected 1 member_update call, got %d", len(updatePayloads))
|
||||
}
|
||||
return updatePayloads
|
||||
}
|
||||
|
||||
func TestTeamMemberAddUpdateSendsChangedSettings(t *testing.T) {
|
||||
d := newTeamMemberUpdateResourceData(t,
|
||||
map[string]string{"tpm_limit": "1000", "rpm_limit": "10", "budget_duration": "30d"},
|
||||
map[string]string{"tpm_limit": "500", "rpm_limit": "5", "budget_duration": "7d", "allowed_models.#": "1", "allowed_models.0": "gpt-5.2"},
|
||||
)
|
||||
|
||||
update := runTeamMemberUpdate(t, d)[0]
|
||||
if update["tpm_limit"] != float64(500) || update["rpm_limit"] != float64(5) {
|
||||
t.Fatalf("member_update payload limits = %v/%v, want 500/5", update["tpm_limit"], update["rpm_limit"])
|
||||
}
|
||||
if update["budget_duration"] != "7d" {
|
||||
t.Fatalf("member_update payload budget_duration = %v, want 7d", update["budget_duration"])
|
||||
}
|
||||
if !reflect.DeepEqual(update["allowed_models"], []interface{}{"gpt-5.2"}) {
|
||||
t.Fatalf("member_update payload allowed_models = %v, want [gpt-5.2]", update["allowed_models"])
|
||||
}
|
||||
if update["user_id"] != "user-1" {
|
||||
t.Fatalf("member_update payload user_id = %v, want user-1", update["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamMemberAddUpdateClearsRemovedSettings(t *testing.T) {
|
||||
d := newTeamMemberUpdateResourceData(t,
|
||||
map[string]string{"tpm_limit": "1000", "rpm_limit": "10", "budget_duration": "30d", "allowed_models.#": "1", "allowed_models.0": "gpt-5.2"},
|
||||
map[string]string{"allowed_models.#": "0"},
|
||||
)
|
||||
|
||||
update := runTeamMemberUpdate(t, d)[0]
|
||||
for _, field := range []string{"tpm_limit", "rpm_limit", "budget_duration"} {
|
||||
v, present := update[field]
|
||||
if !present {
|
||||
t.Fatalf("member_update payload omitted removed %s, so the proxy would keep the old value", field)
|
||||
}
|
||||
if v != nil {
|
||||
t.Fatalf("member_update payload %s = %v, want explicit null", field, v)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(update["allowed_models"], []interface{}{}) {
|
||||
t.Fatalf("member_update payload allowed_models = %v, want empty list", update["allowed_models"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamMemberAddUpdateLeavesUnchangedSettingsAlone(t *testing.T) {
|
||||
d := newTeamMemberUpdateResourceData(t,
|
||||
map[string]string{"budget_duration": "30d"},
|
||||
map[string]string{"budget_duration": "7d"},
|
||||
)
|
||||
|
||||
update := runTeamMemberUpdate(t, d)[0]
|
||||
for _, field := range []string{"tpm_limit", "rpm_limit"} {
|
||||
if v, present := update[field]; present {
|
||||
t.Fatalf("member_update payload must not touch never-set %s, got %v", field, v)
|
||||
}
|
||||
}
|
||||
if _, present := update["allowed_models"]; present {
|
||||
t.Fatalf("member_update payload must not touch unchanged allowed_models, got %v", update["allowed_models"])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue