diff --git a/terraform/provider/CHANGELOG.md b/terraform/provider/CHANGELOG.md index 079bb7d8667..4123b7e6b51 100644 --- a/terraform/provider/CHANGELOG.md +++ b/terraform/provider/CHANGELOG.md @@ -16,6 +16,7 @@ longer signal it. ### Added +- **model**: Optional `display_name` argument on `litellm_model`, sent as `model_info.display_name` and returned as `display_name` by `/v1/models`, so client model pickers show a readable name; changes are persisted through `/model/{id}/update` since `/model/update` ignores `model_info`; also exported by the `litellm_model` and `litellm_models` data sources - **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 @@ -48,6 +49,7 @@ longer signal it. ### Fixed +- **model**: `litellm_model` refresh now reads the `{"data": [...]}` envelope `/model/info` returns, so `model_info` fields changed outside Terraform show up as drift instead of silently keeping the previous state - **key**: An update that changes `team_id` and fails because the key was already cascade-deleted along with its previous team now recovers by recreating the key under the new team, instead of aborting the apply. The key's absence is confirmed against the proxy first, so an unrelated failure still errors out, and a `team_id` change between two teams that both still exist stays a plain in-place update - **credential**: create now reports a `credential_name` collision as a clear error naming the `terraform import` command that adopts the existing credential, instead of surfacing the proxy's raw 500 with a Prisma `Unique constraint failed` message. New `adopt_existing` argument (default `false`) opts into taking the existing credential over during create, which makes `apply` idempotent again once state loses track of a credential that still exists on the proxy. Requires a proxy that answers 409 on the collision; older proxies are still detected by their 500 message - **credential**: credential names and `model_id` are now percent-encoded in request URLs, so a name containing `/`, `?`, `#` or spaces reaches the proxy intact instead of being cut at the first reserved character and read, updated or deleted as a different credential diff --git a/terraform/provider/docs/data-sources/model.md b/terraform/provider/docs/data-sources/model.md index 6976ff1523a..587652bcbd8 100644 --- a/terraform/provider/docs/data-sources/model.md +++ b/terraform/provider/docs/data-sources/model.md @@ -43,6 +43,7 @@ In addition to all arguments above, the following attributes are exported: * `tier` - Model tier (`free` or `paid`). * `mode` - Model mode, e.g. `chat` or `embedding`. * `team_id` - Team the deployment is scoped to, if any. +* `display_name` - Human-readable name returned by `/v1/models`, if configured. * `db_model` - Whether the deployment is stored in the database (as opposed to config). ## Security Note diff --git a/terraform/provider/docs/data-sources/models.md b/terraform/provider/docs/data-sources/models.md index 7862dc30ab7..1cf0fd36ab3 100644 --- a/terraform/provider/docs/data-sources/models.md +++ b/terraform/provider/docs/data-sources/models.md @@ -41,4 +41,5 @@ In addition to all arguments above, the following attributes are exported: * `tier` - Model tier (`free` or `paid`). * `mode` - Model mode, e.g. `chat` or `embedding`. * `team_id` - Team the deployment is scoped to, if any. + * `display_name` - Human-readable name returned by `/v1/models`, if configured. * `db_model` - Whether the deployment is stored in the database. diff --git a/terraform/provider/docs/resources/model.md b/terraform/provider/docs/resources/model.md index 0409b48b391..5bb68bfe918 100644 --- a/terraform/provider/docs/resources/model.md +++ b/terraform/provider/docs/resources/model.md @@ -126,6 +126,8 @@ The following arguments are supported: * `team_id` - (Optional) string. Associate the model with a specific team. +* `display_name` - (Optional) string. Human-readable name stored in `model_info.display_name` and returned as `display_name` by `/v1/models`, so clients such as Claude Code and Claude Desktop show it in their model picker instead of `model_name`. When unset, clients fall back to `model_name`. + * `mode` - (Optional) string. The intended use of the model. Valid values are: * `completion` * `embedding` diff --git a/terraform/provider/litellm/data_source_model.go b/terraform/provider/litellm/data_source_model.go index 78af04ac160..6b9993d267e 100644 --- a/terraform/provider/litellm/data_source_model.go +++ b/terraform/provider/litellm/data_source_model.go @@ -23,14 +23,15 @@ type modelInfoParams struct { } type modelInfoMeta struct { - ID string `json:"id"` - DBModel bool `json:"db_model"` - BaseModel string `json:"base_model"` - Tier string `json:"tier"` - Mode string `json:"mode"` - TeamID string `json:"team_id"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID string `json:"id"` + DBModel bool `json:"db_model"` + BaseModel string `json:"base_model"` + Tier string `json:"tier"` + Mode string `json:"mode"` + TeamID string `json:"team_id"` + DisplayName string `json:"display_name"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } type modelInfoEntry struct { @@ -112,6 +113,10 @@ func dataSourceLiteLLMModel() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "display_name": { + Type: schema.TypeString, + Computed: true, + }, "db_model": { Type: schema.TypeBool, Computed: true, @@ -161,6 +166,7 @@ func dataSourceLiteLLMModelRead(d *schema.ResourceData, m interface{}) error { d.Set("tier", entry.ModelInfo.Tier) d.Set("mode", entry.ModelInfo.Mode) d.Set("team_id", entry.ModelInfo.TeamID) + d.Set("display_name", entry.ModelInfo.DisplayName) d.Set("db_model", entry.ModelInfo.DBModel) log.Printf("[INFO] Successfully read model with ID: %s", modelID) @@ -197,6 +203,7 @@ func dataSourceLiteLLMModels() *schema.Resource { "tier": {Type: schema.TypeString, Computed: true}, "mode": {Type: schema.TypeString, Computed: true}, "team_id": {Type: schema.TypeString, Computed: true}, + "display_name": {Type: schema.TypeString, Computed: true}, "db_model": {Type: schema.TypeBool, Computed: true}, }, }, @@ -247,6 +254,7 @@ func dataSourceLiteLLMModelsRead(d *schema.ResourceData, m interface{}) error { "tier": entry.ModelInfo.Tier, "mode": entry.ModelInfo.Mode, "team_id": entry.ModelInfo.TeamID, + "display_name": entry.ModelInfo.DisplayName, "db_model": entry.ModelInfo.DBModel, }) } diff --git a/terraform/provider/litellm/data_source_model_test.go b/terraform/provider/litellm/data_source_model_test.go index 97d7f07dcd8..b46a355853a 100644 --- a/terraform/provider/litellm/data_source_model_test.go +++ b/terraform/provider/litellm/data_source_model_test.go @@ -35,7 +35,8 @@ func TestDataSourceModelReadSingleObject(t *testing.T) { "base_model": "gpt-4o", "tier": "paid", "mode": "chat", - "team_id": "team-1" + "team_id": "team-1", + "display_name": "GPT-4o" } } }`)) @@ -66,6 +67,7 @@ func TestDataSourceModelReadSingleObject(t *testing.T) { "tier": "paid", "mode": "chat", "team_id": "team-1", + "display_name": "GPT-4o", "db_model": true, } for attr, want := range checks { @@ -115,7 +117,7 @@ func TestDataSourceModelsRead(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "data": [ - {"model_name": "a", "litellm_params": {"model": "openai/a", "custom_llm_provider": "openai"}, "model_info": {"id": "id-1", "db_model": true}}, + {"model_name": "a", "litellm_params": {"model": "openai/a", "custom_llm_provider": "openai"}, "model_info": {"id": "id-1", "db_model": true, "display_name": "Model A"}}, {"model_name": "b", "litellm_params": {"model": "anthropic/b", "custom_llm_provider": "anthropic"}, "model_info": {"id": "id-2"}} ] }`)) @@ -143,7 +145,11 @@ func TestDataSourceModelsRead(t *testing.T) { t.Fatalf("expected 2 models, got %d", len(models)) } first := models[0].(map[string]interface{}) - if first["model_name"] != "a" || first["custom_llm_provider"] != "openai" || first["db_model"] != true { + if first["model_name"] != "a" || first["custom_llm_provider"] != "openai" || first["db_model"] != true || first["display_name"] != "Model A" { t.Errorf("unexpected first model: %v", first) } + second := models[1].(map[string]interface{}) + if second["display_name"] != "" { + t.Errorf("expected empty display_name for model without one, got %v", second["display_name"]) + } } diff --git a/terraform/provider/litellm/resource_model.go b/terraform/provider/litellm/resource_model.go index b0a7304718b..85cb1d038bf 100644 --- a/terraform/provider/litellm/resource_model.go +++ b/terraform/provider/litellm/resource_model.go @@ -93,6 +93,11 @@ func resourceLiteLLMModel() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "display_name": { + Type: schema.TypeString, + Optional: true, + Description: "Human-readable name returned as display_name by /v1/models, shown in client model pickers instead of model_name", + }, "mode": { Type: schema.TypeString, Optional: true, diff --git a/terraform/provider/litellm/resource_model_crud.go b/terraform/provider/litellm/resource_model_crud.go index fc5d5b09dd5..c7db2a673e5 100644 --- a/terraform/provider/litellm/resource_model_crud.go +++ b/terraform/provider/litellm/resource_model_crud.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "net/url" "strconv" "strings" "time" @@ -53,6 +54,7 @@ func retryModelRead(d *schema.ResourceData, m interface{}, maxRetries int) error const ( endpointModelNew = "/model/new" endpointModelUpdate = "/model/update" + endpointModelPatch = "/model/%s/update" endpointModelInfo = "/model/info" endpointModelDelete = "/model/delete" ) @@ -246,12 +248,13 @@ func createOrUpdateModel(d *schema.ResourceData, m interface{}, isUpdate bool) e ModelName: d.Get("model_name").(string), LiteLLMParams: litellmParams, ModelInfo: ModelInfo{ - ID: modelID, - DBModel: true, - BaseModel: pricingBaseModel, - Tier: d.Get("tier").(string), - Mode: d.Get("mode").(string), - TeamID: d.Get("team_id").(string), + ID: modelID, + DBModel: true, + BaseModel: pricingBaseModel, + Tier: d.Get("tier").(string), + Mode: d.Get("mode").(string), + TeamID: d.Get("team_id").(string), + DisplayName: d.Get("display_name").(string), }, Additional: make(map[string]interface{}), } @@ -275,6 +278,12 @@ func createOrUpdateModel(d *schema.ResourceData, m interface{}, isUpdate bool) e return fmt.Errorf("failed to %s model: %w", map[bool]string{true: "update", false: "create"}[isUpdate], err) } + if isUpdate && d.HasChange("display_name") { + if err := patchModelDisplayName(client, modelID, d.Get("display_name").(string)); err != nil { + return fmt.Errorf("failed to update model display_name: %w", err) + } + } + d.SetId(modelID) log.Printf("[INFO] Model created with ID %s. Starting retry mechanism to read the model...", modelID) @@ -282,6 +291,19 @@ func createOrUpdateModel(d *schema.ResourceData, m interface{}, isUpdate bool) e return retryModelRead(d, m, 5) } +// /model/update only merges litellm_params, so model_info changes go through the PATCH endpoint. +func patchModelDisplayName(client *Client, modelID, displayName string) error { + resp, err := MakeRequest(client, "PATCH", fmt.Sprintf(endpointModelPatch, url.PathEscape(modelID)), ModelInfoPatch{ + ModelInfo: ModelInfoPatchFields{ID: modelID, DisplayName: displayName}, + }) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = handleAPIResponse(resp, nil, client) + return err +} + func resourceLiteLLMModelCreate(d *schema.ResourceData, m interface{}) error { return createOrUpdateModel(d, m, false) } @@ -327,6 +349,7 @@ 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))) + d.Set("display_name", modelResp.ModelInfo.DisplayName) // 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_test.go b/terraform/provider/litellm/resource_model_test.go new file mode 100644 index 00000000000..0be3c39a3c5 --- /dev/null +++ b/terraform/provider/litellm/resource_model_test.go @@ -0,0 +1,244 @@ +package litellm + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func modelInfoBody(displayName string) string { + modelInfo := map[string]interface{}{ + "id": "model-123", + "db_model": true, + "base_model": "claude-sonnet-4-5", + "tier": "free", + "mode": "chat", + } + if displayName != "" { + modelInfo["display_name"] = displayName + } + body, _ := json.Marshal(map[string]interface{}{ + "model_name": "sonnet-4-5-anthropic", + "litellm_params": map[string]interface{}{"model": "anthropic/claude-sonnet-4-5", "custom_llm_provider": "anthropic"}, + "model_info": modelInfo, + }) + return string(body) +} + +func modelInfoDataEnvelope(displayName string) string { + return `{"data": [` + modelInfoBody(displayName) + `]}` +} + +func TestResourceLiteLLMModelCreateSendsDisplayName(t *testing.T) { + var createPayload map[string]interface{} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/model/new": + if err := json.NewDecoder(r.Body).Decode(&createPayload); err != nil { + t.Errorf("failed to decode create payload: %v", err) + } + w.Write([]byte(modelInfoBody("Claude Sonnet 4.5"))) + case "/model/info": + w.Write([]byte(modelInfoBody("Claude Sonnet 4.5"))) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + + d := schema.TestResourceDataRaw(t, resourceLiteLLMModel().Schema, map[string]interface{}{ + "model_name": "sonnet-4-5-anthropic", + "custom_llm_provider": "anthropic", + "base_model": "claude-sonnet-4-5", + "model_api_key": "sk-ant-test", + "mode": "chat", + "display_name": "Claude Sonnet 4.5", + }) + + if err := resourceLiteLLMModelCreate(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("create failed: %v", err) + } + + modelInfo, ok := createPayload["model_info"].(map[string]interface{}) + if !ok { + t.Fatalf("expected model_info object in create payload, got %v", createPayload["model_info"]) + } + if modelInfo["display_name"] != "Claude Sonnet 4.5" { + t.Errorf("expected model_info.display_name 'Claude Sonnet 4.5', got %v", modelInfo["display_name"]) + } + if got := d.Get("display_name").(string); got != "Claude Sonnet 4.5" { + t.Errorf("expected state display_name 'Claude Sonnet 4.5', got %q", got) + } +} + +func TestResourceLiteLLMModelCreateOmitsUnsetDisplayName(t *testing.T) { + var createPayload map[string]interface{} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/model/new": + if err := json.NewDecoder(r.Body).Decode(&createPayload); err != nil { + t.Errorf("failed to decode create payload: %v", err) + } + w.Write([]byte(modelInfoBody(""))) + case "/model/info": + w.Write([]byte(modelInfoBody(""))) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + + d := schema.TestResourceDataRaw(t, resourceLiteLLMModel().Schema, map[string]interface{}{ + "model_name": "sonnet-4-5-anthropic", + "custom_llm_provider": "anthropic", + "base_model": "claude-sonnet-4-5", + "model_api_key": "sk-ant-test", + }) + + if err := resourceLiteLLMModelCreate(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("create failed: %v", err) + } + + modelInfo := createPayload["model_info"].(map[string]interface{}) + if _, present := modelInfo["display_name"]; present { + t.Errorf("expected display_name to be omitted from model_info when unset, got %v", modelInfo["display_name"]) + } + if got := d.Get("display_name").(string); got != "" { + t.Errorf("expected empty state display_name, got %q", got) + } +} + +func TestResourceLiteLLMModelReadDisplayName(t *testing.T) { + cases := map[string]struct { + serverBody string + want string + }{ + "server value wins inside data envelope": {serverBody: modelInfoDataEnvelope("Renamed In Admin UI"), want: "Renamed In Admin UI"}, + "server value wins unwrapped": {serverBody: modelInfoBody("Renamed In Admin UI"), want: "Renamed In Admin UI"}, + "external removal clears state": {serverBody: modelInfoDataEnvelope(""), want: ""}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/model/info" { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Write([]byte(tc.serverBody)) + })) + defer srv.Close() + + d := schema.TestResourceDataRaw(t, resourceLiteLLMModel().Schema, map[string]interface{}{ + "model_name": "sonnet-4-5-anthropic", + "custom_llm_provider": "anthropic", + "base_model": "claude-sonnet-4-5", + "display_name": "Claude Sonnet 4.5", + }) + d.SetId("model-123") + + if err := resourceLiteLLMModelRead(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("read failed: %v", err) + } + if got := d.Get("display_name").(string); got != tc.want { + t.Errorf("expected display_name %q, got %q", tc.want, got) + } + }) + } +} + +func updateResourceData(t *testing.T, oldDisplayName, newDisplayName string) *schema.ResourceData { + t.Helper() + res := resourceLiteLLMModel() + attrs := map[string]string{ + "model_name": "sonnet-4-5-anthropic", + "custom_llm_provider": "anthropic", + "base_model": "claude-sonnet-4-5", + } + if oldDisplayName != "" { + attrs["display_name"] = oldDisplayName + } + state := &terraform.InstanceState{ID: "model-123", Attributes: attrs} + diff, err := res.Diff(context.Background(), state, &terraform.ResourceConfig{Config: map[string]interface{}{ + "model_name": "sonnet-4-5-anthropic", + "custom_llm_provider": "anthropic", + "base_model": "claude-sonnet-4-5", + "display_name": newDisplayName, + }}, nil) + if err != nil { + t.Fatalf("diff failed: %v", err) + } + d, err := schema.InternalMap(res.Schema).Data(state, diff) + if err != nil { + t.Fatalf("data failed: %v", err) + } + return d +} + +func TestResourceLiteLLMModelUpdatePatchesDisplayName(t *testing.T) { + cases := map[string]struct { + newName string + }{ + "changed name is patched": {newName: "Claude Sonnet 4.5 v2"}, + "cleared name is patched": {newName: ""}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + var patchPayload map[string]interface{} + var patchPath string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPost && r.URL.Path == "/model/update": + w.Write([]byte(modelInfoBody("Claude Sonnet 4.5"))) + case r.Method == http.MethodPatch: + patchPath = r.URL.Path + if err := json.NewDecoder(r.Body).Decode(&patchPayload); err != nil { + t.Errorf("failed to decode patch payload: %v", err) + } + w.Write([]byte(modelInfoBody(tc.newName))) + case r.URL.Path == "/model/info": + w.Write([]byte(modelInfoDataEnvelope(tc.newName))) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + + d := updateResourceData(t, "Claude Sonnet 4.5", tc.newName) + if err := resourceLiteLLMModelUpdate(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("update failed: %v", err) + } + if patchPath != "/model/model-123/update" { + t.Fatalf("expected PATCH /model/model-123/update, got %q", patchPath) + } + modelInfo := patchPayload["model_info"].(map[string]interface{}) + if modelInfo["display_name"] != tc.newName { + t.Errorf("expected patched display_name %q, got %v", tc.newName, modelInfo["display_name"]) + } + if got := d.Get("display_name").(string); got != tc.newName { + t.Errorf("expected state display_name %q, got %q", tc.newName, got) + } + }) + } +} + +func TestResourceLiteLLMModelUpdateSkipsPatchWhenDisplayNameUnchanged(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPatch { + t.Errorf("unexpected PATCH %s", r.URL.Path) + } + w.Write([]byte(modelInfoDataEnvelope("Claude Sonnet 4.5"))) + })) + defer srv.Close() + + d := updateResourceData(t, "Claude Sonnet 4.5", "Claude Sonnet 4.5") + if err := resourceLiteLLMModelUpdate(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("update failed: %v", err) + } +} diff --git a/terraform/provider/litellm/types.go b/terraform/provider/litellm/types.go index 8bcf7dc4fe3..a8784b8a6a9 100644 --- a/terraform/provider/litellm/types.go +++ b/terraform/provider/litellm/types.go @@ -25,6 +25,16 @@ type ModelResponse struct { Additional map[string]interface{} `json:"additional"` } +// ModelInfoPatch is the body for PATCH /model/{id}/update; display_name is sent even when empty so it can be cleared. +type ModelInfoPatch struct { + ModelInfo ModelInfoPatchFields `json:"model_info"` +} + +type ModelInfoPatchFields struct { + ID string `json:"id"` + DisplayName string `json:"display_name"` +} + // ModelRequest represents a request to create or update a model. type ModelRequest struct { ModelName string `json:"model_name"` @@ -108,12 +118,13 @@ type LiteLLMParams struct { // ModelInfo represents information about a model. type ModelInfo struct { - ID string `json:"id"` - DBModel bool `json:"db_model"` - BaseModel string `json:"base_model"` - Tier string `json:"tier"` - Mode string `json:"mode"` - TeamID string `json:"team_id,omitempty"` + ID string `json:"id"` + DBModel bool `json:"db_model"` + BaseModel string `json:"base_model"` + Tier string `json:"tier"` + Mode string `json:"mode"` + TeamID string `json:"team_id,omitempty"` + DisplayName string `json:"display_name,omitempty"` } // Key represents a LiteLLM API key. diff --git a/terraform/provider/litellm/utils.go b/terraform/provider/litellm/utils.go index f8f66afba3c..ce1dae55f59 100644 --- a/terraform/provider/litellm/utils.go +++ b/terraform/provider/litellm/utils.go @@ -55,6 +55,13 @@ func handleAPIResponse(resp *http.Response, reqBody interface{}, client *Client) resp.Status, client.redactSensitiveData(string(bodyBytes)), client.redactSensitiveData(string(reqBodyBytes))) } + var envelope struct { + Data []json.RawMessage `json:"data"` + } + if err := json.Unmarshal(bodyBytes, &envelope); err == nil && len(envelope.Data) > 0 { + bodyBytes = envelope.Data[0] + } + var modelResp ModelResponse if err := json.Unmarshal(bodyBytes, &modelResp); err != nil { return nil, fmt.Errorf("failed to parse response: %v", err) diff --git a/terraform/provider/tools/endpointaudit/coverage_allowlist.txt b/terraform/provider/tools/endpointaudit/coverage_allowlist.txt index 7aacf7ceab9..e4574031d86 100644 --- a/terraform/provider/tools/endpointaudit/coverage_allowlist.txt +++ b/terraform/provider/tools/endpointaudit/coverage_allowlist.txt @@ -97,7 +97,6 @@ GET /guardrails/{guardrail_id} GET /prompts/{prompt_id} GET /prompts/{prompt_id}/versions PATCH /guardrails/{guardrail_id} -PATCH /model/{model_id}/update PATCH /prompts/{prompt_id} PATCH /team/{team_id} POST /team/model/add