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.
This commit is contained in:
Julian Löffler 2026-09-11 09:21:41 +02:00
parent 9a715df212
commit 0e2872c98b
No known key found for this signature in database
2 changed files with 75 additions and 6 deletions

View file

@ -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 != "" {

View file

@ -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"))
}
}