From 0e2872c98bb170a7302c0c59698d60e632641ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20L=C3=B6ffler?= Date: Fri, 11 Sep 2026 09:21:41 +0200 Subject: [PATCH] fix(terraform): send a vector_store_id on create and unwrap the info envelope Creating a litellm_vector_store never worked. /vector_store/new requires vector_store_id, but the attribute is computed and the provider left it unset, so the proxy rejected every create. The resource was also keyed on the store's name instead of its id, and read unmarshalled /vector_store/info straight into VectorStoreResponse even though the proxy nests the store under "vector_store", so every attribute read back empty. Mint a UUID for the new store, key the resource on it, and decode the info envelope. --- .../litellm/resource_vector_store_crud.go | 18 ++++-- .../resource_vector_store_crud_test.go | 63 ++++++++++++++++++- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/terraform/provider/litellm/resource_vector_store_crud.go b/terraform/provider/litellm/resource_vector_store_crud.go index b05017f7125..101fd69a0e3 100644 --- a/terraform/provider/litellm/resource_vector_store_crud.go +++ b/terraform/provider/litellm/resource_vector_store_crud.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -29,7 +30,12 @@ func resourceLiteLLMVectorStoreCreate(d *schema.ResourceData, m interface{}) err paramsMap[k] = v } + // /vector_store/new rejects a missing vector_store_id, and the attribute is + // computed, so the provider has to mint the id it stores under. + vectorStoreID := uuid.New().String() + vectorStoreRequest := VectorStoreRequest{ + VectorStoreID: vectorStoreID, CustomLLMProvider: customLLMProvider, VectorStoreName: vectorStoreName, VectorStoreDescription: vectorStoreDescription, @@ -49,9 +55,7 @@ func resourceLiteLLMVectorStoreCreate(d *schema.ResourceData, m interface{}) err return fmt.Errorf("failed to create vector store: %w", err) } - // Set the resource ID to the vector store name for now - // We'll update this after reading the response to get the actual ID - d.SetId(vectorStoreName) + d.SetId(vectorStoreID) return resourceLiteLLMVectorStoreRead(d, m) } @@ -76,8 +80,11 @@ func resourceLiteLLMVectorStoreRead(d *schema.ResourceData, m interface{}) error return nil } - var vectorStoreResp VectorStoreResponse - err = handleVectorStoreAPIResponse(resp, &vectorStoreResp, client) + // /vector_store/info nests the store under "vector_store". + var infoResp struct { + VectorStore VectorStoreResponse `json:"vector_store"` + } + err = handleVectorStoreAPIResponse(resp, &infoResp, client) if err != nil { if err.Error() == "vector_store_not_found" { d.SetId("") @@ -85,6 +92,7 @@ func resourceLiteLLMVectorStoreRead(d *schema.ResourceData, m interface{}) error } return fmt.Errorf("failed to read vector store: %w", err) } + vectorStoreResp := infoResp.VectorStore // Update the resource ID to the actual vector store ID from the response if vectorStoreResp.VectorStoreID != "" { diff --git a/terraform/provider/litellm/resource_vector_store_crud_test.go b/terraform/provider/litellm/resource_vector_store_crud_test.go index 485ec54346d..268780be243 100644 --- a/terraform/provider/litellm/resource_vector_store_crud_test.go +++ b/terraform/provider/litellm/resource_vector_store_crud_test.go @@ -19,7 +19,7 @@ func TestVectorStoreReadDoesNotPersistServerLitellmParams(t *testing.T) { "api_base": "https://upstream.example.com", }, } - body, _ := json.Marshal(resp) + body, _ := json.Marshal(map[string]interface{}{"vector_store": resp}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -53,3 +53,64 @@ func TestVectorStoreReadDoesNotPersistServerLitellmParams(t *testing.T) { t.Fatalf("read did not populate non-sensitive fields") } } + +// /vector_store/new 400s without a vector_store_id, and the attribute is +// computed, so create has to mint one and key the resource on it rather than on +// the store's name. +func TestVectorStoreCreateSendsGeneratedIDAndUsesItAsResourceID(t *testing.T) { + var createBody map[string]interface{} + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.URL.Path == "/vector_store/new" { + json.NewDecoder(r.Body).Decode(&createBody) + id, _ := createBody["vector_store_id"].(string) + if id == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"detail":"vector_store_id and custom_llm_provider are required"}`)) + return + } + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "vector_store": VectorStoreResponse{VectorStoreID: id, VectorStoreName: "kb", CustomLLMProvider: "openai"}, + }) + return + } + + var info VectorStoreInfoRequest + json.NewDecoder(r.Body).Decode(&info) + json.NewEncoder(w).Encode(map[string]interface{}{ + "vector_store": VectorStoreResponse{ + VectorStoreID: info.VectorStoreID, + VectorStoreName: "kb", + CustomLLMProvider: "openai", + CreatedAt: "2026-01-01T00:00:00Z", + }, + }) + })) + defer srv.Close() + + d := schema.TestResourceDataRaw(t, resourceLiteLLMVectorStore().Schema, map[string]interface{}{ + "vector_store_name": "kb", + "custom_llm_provider": "openai", + }) + + if err := resourceLiteLLMVectorStoreCreate(d, NewClient(srv.URL, "test-key", true)); err != nil { + t.Fatalf("create failed: %v", err) + } + + sentID, _ := createBody["vector_store_id"].(string) + if sentID == "" { + t.Fatal("create payload omitted vector_store_id, which the proxy rejects") + } + if d.Id() != sentID { + t.Errorf("resource id = %q, want the created store id %q", d.Id(), sentID) + } + if d.Get("vector_store_id").(string) != sentID { + t.Errorf("vector_store_id = %q, want %q", d.Get("vector_store_id").(string), sentID) + } + if d.Get("created_at").(string) != "2026-01-01T00:00:00Z" { + t.Errorf("create did not refresh computed fields from the API: %v", d.Get("created_at")) + } +}