diff --git a/terraform/provider/CHANGELOG.md b/terraform/provider/CHANGELOG.md index 8f40ed6dfb7..554c7da687b 100644 --- a/terraform/provider/CHANGELOG.md +++ b/terraform/provider/CHANGELOG.md @@ -16,6 +16,7 @@ longer signal it. ### Added +- **model**: `tags` attribute on `litellm_model`, matching what `/model/new` and `/model/update` already accept under `litellm_params.tags`, for [tag-based routing](https://docs.litellm.ai/docs/proxy/tag_routing) - **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 diff --git a/terraform/provider/docs/resources/model.md b/terraform/provider/docs/resources/model.md index 0409b48b391..d722697f00c 100644 --- a/terraform/provider/docs/resources/model.md +++ b/terraform/provider/docs/resources/model.md @@ -33,6 +33,7 @@ resource "litellm_model" "advanced_gpt4" { tier = "paid" team_id = "team-123" mode = "chat" + tags = ["team:platform"] reasoning_effort = "medium" thinking_enabled = true thinking_budget_tokens = 1024 @@ -122,6 +123,8 @@ The following arguments are supported: * `litellm_credential_name` - (Optional) string. Name of a LiteLLM credential to use for this model. +* `tags` - (Optional) list(string). Tags applied to this model deployment, used for [tag-based routing](https://docs.litellm.ai/docs/proxy/tag_routing) so requests carrying a matching tag are routed to it. + * `tier` - (Optional) string. The usage tier for this model. Valid values are `"free"` or `"paid"`. Default: `"free"`. * `team_id` - (Optional) string. Associate the model with a specific team. diff --git a/terraform/provider/litellm/resource_model.go b/terraform/provider/litellm/resource_model.go index b0a7304718b..5e8e1afe630 100644 --- a/terraform/provider/litellm/resource_model.go +++ b/terraform/provider/litellm/resource_model.go @@ -175,6 +175,12 @@ func resourceLiteLLMModel() *schema.Resource { Optional: true, Description: "Name of the LiteLLM credential to use", }, + "tags": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Tags for tag-based routing (see https://docs.litellm.ai/docs/proxy/tag_routing)", + }, "additional_litellm_params": { Type: schema.TypeMap, Optional: true, diff --git a/terraform/provider/litellm/resource_model_crud.go b/terraform/provider/litellm/resource_model_crud.go index fc5d5b09dd5..b820333068e 100644 --- a/terraform/provider/litellm/resource_model_crud.go +++ b/terraform/provider/litellm/resource_model_crud.go @@ -163,6 +163,9 @@ func createOrUpdateModel(d *schema.ResourceData, m interface{}, isUpdate bool) e if thinking != nil { litellmParams["thinking"] = thinking } + if v, ok := d.GetOk("tags"); ok || d.HasChange("tags") { + litellmParams["tags"] = v + } // Add additional parameters if provided if additionalParams, ok := d.GetOk("additional_litellm_params"); ok { @@ -327,6 +330,11 @@ func resourceLiteLLMModelRead(d *schema.ResourceData, m interface{}) error { d.Set("tier", GetStringValue(modelResp.ModelInfo.Tier, d.Get("tier").(string))) d.Set("mode", GetStringValue(modelResp.ModelInfo.Mode, d.Get("mode").(string))) d.Set("team_id", GetStringValue(modelResp.ModelInfo.TeamID, d.Get("team_id").(string))) + if modelResp.LiteLLMParams.Tags != nil { + d.Set("tags", modelResp.LiteLLMParams.Tags) + } else { + d.Set("tags", d.Get("tags")) + } // Preserve credential name from state since it might not be returned by API d.Set("litellm_credential_name", d.Get("litellm_credential_name").(string)) diff --git a/terraform/provider/litellm/resource_model_tags_test.go b/terraform/provider/litellm/resource_model_tags_test.go new file mode 100644 index 00000000000..786a2eccf6e --- /dev/null +++ b/terraform/provider/litellm/resource_model_tags_test.go @@ -0,0 +1,151 @@ +package litellm + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func newModelTestServer(t *testing.T, captured *map[string]interface{}, infoBody string) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case endpointModelNew, endpointModelUpdate: + body, _ := io.ReadAll(r.Body) + json.Unmarshal(body, captured) + w.Write([]byte(infoBody)) + case endpointModelInfo: + w.Write([]byte(infoBody)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) +} + +func newModelResourceData(t *testing.T, raw map[string]interface{}) *schema.ResourceData { + t.Helper() + return schema.TestResourceDataRaw(t, resourceLiteLLMModel().Schema, raw) +} + +func TestModelCreateSendsTags(t *testing.T) { + var captured map[string]interface{} + infoBody := `{ + "model_name": "gpt-4.1", + "litellm_params": { + "model": "azure/gpt-4.1", + "custom_llm_provider": "azure", + "tags": ["advisor"] + }, + "model_info": { + "id": "model-1", + "db_model": true, + "base_model": "gpt-4.1", + "tier": "free", + "mode": "chat" + } + }` + srv := newModelTestServer(t, &captured, infoBody) + defer srv.Close() + + d := newModelResourceData(t, map[string]interface{}{ + "model_name": "gpt-4.1", + "custom_llm_provider": "azure", + "base_model": "gpt-4.1", + "tags": []interface{}{"advisor"}, + }) + + if err := resourceLiteLLMModelCreate(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("create failed: %v", err) + } + + litellmParams, ok := captured["litellm_params"].(map[string]interface{}) + if !ok { + t.Fatalf("payload litellm_params = %v, want a map", captured["litellm_params"]) + } + wantTags := []interface{}{"advisor"} + if got := litellmParams["tags"]; !reflect.DeepEqual(got, wantTags) { + t.Fatalf("payload litellm_params.tags = %v, want %v", got, wantTags) + } +} + +func TestModelReadPopulatesTagsFromResponse(t *testing.T) { + var captured map[string]interface{} + infoBody := `{ + "model_name": "gpt-4.1", + "litellm_params": { + "model": "azure/gpt-4.1", + "custom_llm_provider": "azure", + "tags": ["advisor", "callbot"] + }, + "model_info": { + "id": "model-1", + "db_model": true, + "base_model": "gpt-4.1", + "tier": "free", + "mode": "chat" + } + }` + srv := newModelTestServer(t, &captured, infoBody) + defer srv.Close() + + d := newModelResourceData(t, map[string]interface{}{ + "model_name": "gpt-4.1", + "custom_llm_provider": "azure", + "base_model": "gpt-4.1", + }) + d.SetId("model-1") + + if err := resourceLiteLLMModelRead(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("read failed: %v", err) + } + + wantTags := []interface{}{"advisor", "callbot"} + if got := d.Get("tags"); !reflect.DeepEqual(got, wantTags) { + t.Fatalf("state tags = %v, want %v", got, wantTags) + } +} + +func TestModelUpdateOmitsTagsWhenUnset(t *testing.T) { + var captured map[string]interface{} + infoBody := `{ + "model_name": "gpt-4.1", + "litellm_params": { + "model": "azure/gpt-4.1", + "custom_llm_provider": "azure" + }, + "model_info": { + "id": "model-1", + "db_model": true, + "base_model": "gpt-4.1", + "tier": "free", + "mode": "chat" + } + }` + srv := newModelTestServer(t, &captured, infoBody) + defer srv.Close() + + d := newModelResourceData(t, map[string]interface{}{ + "model_name": "gpt-4.1", + "custom_llm_provider": "azure", + "base_model": "gpt-4.1", + }) + d.SetId("model-1") + + if err := resourceLiteLLMModelUpdate(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("update failed: %v", err) + } + + litellmParams, ok := captured["litellm_params"].(map[string]interface{}) + if !ok { + t.Fatalf("payload litellm_params = %v, want a map", captured["litellm_params"]) + } + if _, ok := litellmParams["tags"]; ok { + t.Fatalf("payload litellm_params.tags = %v, want omitted when unset", litellmParams["tags"]) + } +} diff --git a/terraform/provider/litellm/types.go b/terraform/provider/litellm/types.go index 8bcf7dc4fe3..bc21403e7ba 100644 --- a/terraform/provider/litellm/types.go +++ b/terraform/provider/litellm/types.go @@ -104,6 +104,7 @@ type LiteLLMParams struct { VertexProject string `json:"vertex_project,omitempty"` VertexLocation string `json:"vertex_location,omitempty"` VertexCredentials string `json:"vertex_credentials,omitempty"` + Tags []string `json:"tags,omitempty"` } // ModelInfo represents information about a model.