mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
feat(terraform): allow custom team_id on litellm_team (#40459)
Add an optional, computed, force-new team_id argument to the litellm_team resource. When set, it is sent to /team/new as the team_id; when omitted the provider keeps generating a UUID. Read mirrors the resource id into state so import and plan stay clean. Resolves LIT-6399 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
4ddc5e2c29
commit
18eb6d7998
4 changed files with 113 additions and 3 deletions
|
|
@ -16,6 +16,7 @@ longer signal it.
|
|||
|
||||
### Added
|
||||
|
||||
- **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
|
||||
- **user**: New `litellm_user` resource and `litellm_user` / `litellm_users` data sources for managing internal users
|
||||
|
|
|
|||
|
|
@ -14,6 +14,16 @@ resource "litellm_team" "engineering" {
|
|||
}
|
||||
```
|
||||
|
||||
### Team with a Custom ID
|
||||
|
||||
```hcl
|
||||
resource "litellm_team" "platform" {
|
||||
team_id = "platform-team"
|
||||
team_alias = "platform"
|
||||
models = ["gpt-4-proxy"]
|
||||
}
|
||||
```
|
||||
|
||||
### Team with Comprehensive Configuration
|
||||
|
||||
```hcl
|
||||
|
|
@ -92,6 +102,8 @@ resource "litellm_team" "model_dependent_team" {
|
|||
|
||||
The following arguments are supported:
|
||||
|
||||
* `team_id` - (Optional) A stable, human-readable ID for the team (for example `platform-team`). If omitted, the provider generates a random UUID. Changing this forces a new team to be created.
|
||||
|
||||
* `team_alias` - (Required) A human-readable identifier for the team.
|
||||
|
||||
* `organization_id` - (Optional) The ID of the organization this team belongs to.
|
||||
|
|
@ -152,7 +164,7 @@ The following arguments are supported:
|
|||
|
||||
In addition to the arguments above, the following attributes are exported:
|
||||
|
||||
* `id` - The unique identifier for the team.
|
||||
* `id` - The unique identifier for the team, equal to `team_id`.
|
||||
|
||||
## Import
|
||||
|
||||
|
|
@ -162,7 +174,7 @@ Teams can be imported using the team ID:
|
|||
terraform import litellm_team.engineering <team-id>
|
||||
```
|
||||
|
||||
Note: The team ID is generated when the team is created and is different from the `team_alias`.
|
||||
Note: Unless `team_id` is set, the team ID is generated when the team is created and is different from the `team_alias`.
|
||||
|
||||
## Note on Team Members
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ func ResourceLiteLLMTeam() *schema.Resource {
|
|||
},
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"team_id": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
Description: "Unique ID for the team. Generated by the provider if not provided",
|
||||
},
|
||||
"team_alias": {
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
|
|
@ -162,7 +169,7 @@ func ResourceLiteLLMTeam() *schema.Resource {
|
|||
func resourceLiteLLMTeamCreate(d *schema.ResourceData, m interface{}) error {
|
||||
client := m.(*Client)
|
||||
|
||||
teamID := uuid.New().String()
|
||||
teamID := resolveTeamID(d)
|
||||
teamData := buildTeamData(d, teamID)
|
||||
|
||||
// Throughput limit types are only accepted by /team/new, not /team/update.
|
||||
|
|
@ -214,6 +221,7 @@ func resourceLiteLLMTeamRead(d *schema.ResourceData, m interface{}) error {
|
|||
teamResp := infoResp.TeamInfo
|
||||
|
||||
// Update the state with values from the response or fall back to the data passed in during creation
|
||||
d.Set("team_id", d.Id())
|
||||
d.Set("team_alias", GetStringValue(teamResp.TeamAlias, d.Get("team_alias").(string)))
|
||||
d.Set("organization_id", GetStringValue(teamResp.OrganizationID, d.Get("organization_id").(string)))
|
||||
|
||||
|
|
@ -354,6 +362,13 @@ func resourceLiteLLMTeamDelete(d *schema.ResourceData, m interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func resolveTeamID(d *schema.ResourceData) string {
|
||||
if v, ok := d.GetOk("team_id"); ok {
|
||||
return v.(string)
|
||||
}
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
func buildTeamData(d *schema.ResourceData, teamID string) map[string]interface{} {
|
||||
teamData := map[string]interface{}{
|
||||
"team_id": teamID,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
|
||||
)
|
||||
|
|
@ -85,6 +86,87 @@ func TestTeamCreateSendsSoftBudgetTagsAndAlertEmails(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTeamCreateSendsConfiguredTeamID(t *testing.T) {
|
||||
var captured map[string]interface{}
|
||||
srv := newTeamTestServer(t, &captured, `{"team_id":"platform-team","team_info":{"team_id":"platform-team","team_alias":"platform"},"keys":[],"team_memberships":[]}`)
|
||||
defer srv.Close()
|
||||
|
||||
d := newTeamResourceData(t, map[string]interface{}{
|
||||
"team_id": "platform-team",
|
||||
"team_alias": "platform",
|
||||
})
|
||||
|
||||
if err := resourceLiteLLMTeamCreate(d, NewClient(srv.URL, "test-key", true)); err != nil {
|
||||
t.Fatalf("create failed: %v", err)
|
||||
}
|
||||
|
||||
if got := captured["team_id"]; got != "platform-team" {
|
||||
t.Fatalf("payload team_id = %v, want platform-team", got)
|
||||
}
|
||||
if got := d.Id(); got != "platform-team" {
|
||||
t.Fatalf("resource id = %q, want platform-team", got)
|
||||
}
|
||||
if got := d.Get("team_id"); got != "platform-team" {
|
||||
t.Fatalf("state team_id = %v, want platform-team", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamCreateGeneratesTeamIDWhenUnset(t *testing.T) {
|
||||
var captured map[string]interface{}
|
||||
srv := newTeamTestServer(t, &captured, `{"team_id":"x","team_info":{"team_alias":"eng"},"keys":[],"team_memberships":[]}`)
|
||||
defer srv.Close()
|
||||
|
||||
d := newTeamResourceData(t, map[string]interface{}{"team_alias": "eng"})
|
||||
|
||||
if err := resourceLiteLLMTeamCreate(d, NewClient(srv.URL, "test-key", true)); err != nil {
|
||||
t.Fatalf("create failed: %v", err)
|
||||
}
|
||||
|
||||
sent, _ := captured["team_id"].(string)
|
||||
if _, err := uuid.Parse(sent); err != nil {
|
||||
t.Fatalf("payload team_id = %q, want a generated UUID: %v", sent, err)
|
||||
}
|
||||
if d.Id() != sent || d.Get("team_id") != sent {
|
||||
t.Fatalf("id = %q, state team_id = %v, want both to equal the sent id %q", d.Id(), d.Get("team_id"), sent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamReadSetsTeamIDFromResourceID(t *testing.T) {
|
||||
var captured map[string]interface{}
|
||||
srv := newTeamTestServer(t, &captured, `{"team_id":"imported-team","team_info":{"team_id":"imported-team","team_alias":"imported"},"keys":[],"team_memberships":[]}`)
|
||||
defer srv.Close()
|
||||
|
||||
d := newTeamResourceData(t, map[string]interface{}{})
|
||||
d.SetId("imported-team")
|
||||
|
||||
if err := resourceLiteLLMTeamRead(d, NewClient(srv.URL, "test-key", true)); err != nil {
|
||||
t.Fatalf("read failed: %v", err)
|
||||
}
|
||||
if got := d.Get("team_id"); got != "imported-team" {
|
||||
t.Fatalf("team_id = %v, want imported-team", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamIDChangeForcesReplacement(t *testing.T) {
|
||||
res := ResourceLiteLLMTeam()
|
||||
priorData := schema.TestResourceDataRaw(t, res.Schema, map[string]interface{}{
|
||||
"team_id": "old-team",
|
||||
"team_alias": "eng",
|
||||
})
|
||||
priorData.SetId("old-team")
|
||||
config := terraform.NewResourceConfigRaw(map[string]interface{}{
|
||||
"team_id": "new-team",
|
||||
"team_alias": "eng",
|
||||
})
|
||||
diff, err := res.Diff(context.Background(), priorData.State(), config, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("diff failed: %v", err)
|
||||
}
|
||||
if diff == nil || !diff.RequiresNew() {
|
||||
t.Fatalf("changing team_id must force replacement, diff = %+v", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamReadMapsTeamInfoEnvelope(t *testing.T) {
|
||||
var captured map[string]interface{}
|
||||
srv := newTeamTestServer(t, &captured, teamInfoWithSoftBudget)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue