From 4d0ab210096b0822fc468577223976285560815e 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 1/3] 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")) + } +} From b7f8dc71b89941ed78ef314ce02df2fd09a0d816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20L=C3=B6ffler?= Date: Mon, 28 Sep 2026 10:11:06 +0200 Subject: [PATCH 2/3] style(terraform): remove redundant vector store comments --- terraform/provider/litellm/resource_vector_store_crud.go | 3 --- terraform/provider/litellm/resource_vector_store_crud_test.go | 3 --- 2 files changed, 6 deletions(-) diff --git a/terraform/provider/litellm/resource_vector_store_crud.go b/terraform/provider/litellm/resource_vector_store_crud.go index 101fd69a0e3..b8718646174 100644 --- a/terraform/provider/litellm/resource_vector_store_crud.go +++ b/terraform/provider/litellm/resource_vector_store_crud.go @@ -30,8 +30,6 @@ 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{ @@ -80,7 +78,6 @@ func resourceLiteLLMVectorStoreRead(d *schema.ResourceData, m interface{}) error return nil } - // /vector_store/info nests the store under "vector_store". var infoResp struct { VectorStore VectorStoreResponse `json:"vector_store"` } diff --git a/terraform/provider/litellm/resource_vector_store_crud_test.go b/terraform/provider/litellm/resource_vector_store_crud_test.go index 268780be243..424895b2a03 100644 --- a/terraform/provider/litellm/resource_vector_store_crud_test.go +++ b/terraform/provider/litellm/resource_vector_store_crud_test.go @@ -54,9 +54,6 @@ func TestVectorStoreReadDoesNotPersistServerLitellmParams(t *testing.T) { } } -// /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{} From 92688af811ae0a54d9a594de6f05b5d7c3f1ce23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20L=C3=B6ffler?= Date: Mon, 28 Sep 2026 10:30:38 +0200 Subject: [PATCH 3/3] test: align interactions compliance checks with current Google schema --- .../unit/interactions/test_openapi_compliance.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/unit/interactions/test_openapi_compliance.py b/tests/unit/interactions/test_openapi_compliance.py index d3f1183cea6..462a9581a7f 100644 --- a/tests/unit/interactions/test_openapi_compliance.py +++ b/tests/unit/interactions/test_openapi_compliance.py @@ -3,12 +3,14 @@ OpenAPI compliance tests for Google Interactions API. Validates that our SDK requests/responses match the OpenAPI spec at: https://ai.google.dev/static/api/interactions.openapi.json +Schema names verified against that spec on 2026-09-28. Run with: pytest tests/unit/interactions/test_openapi_compliance.py -v """ import json import os +import re from typing import Any, Dict from unittest.mock import MagicMock, patch @@ -60,12 +62,11 @@ class TestRequestCompliance: """Tests that our request bodies match the OpenAPI spec.""" def test_create_model_interaction_request_schema(self, spec_dict): - """Verify CreateModelInteractionParams schema fields.""" - schema = spec_dict["components"]["schemas"]["CreateModelInteractionParams"] + """Verify ModelInteraction schema fields.""" + schema = spec_dict["components"]["schemas"]["ModelInteraction"] - # Required fields per spec assert "model" in schema["required"] - assert "input" in schema["required"] + assert "input" in schema["properties"] # Check our supported optional fields exist in spec our_optional_fields = [ @@ -88,7 +89,7 @@ class TestRequestCompliance: def test_input_types_match_spec(self, spec_dict): """Verify input field supports string, Content, Content[], Turn[].""" - schema = spec_dict["components"]["schemas"]["CreateModelInteractionParams"] + schema = spec_dict["components"]["schemas"]["ModelInteraction"] input_schema = schema["properties"]["input"] # The input property may be inline oneOf or a $ref to InteractionsInput @@ -313,7 +314,7 @@ class TestEndpointCompliance: get_path = None for path, methods in paths.items(): - if "{id}" in path and "interactions" in path and "get" in methods: + if re.fullmatch(r".*/interactions/\{[^/{}]+\}", path) and "get" in methods: get_path = path break @@ -326,7 +327,7 @@ class TestEndpointCompliance: delete_path = None for path, methods in paths.items(): - if "{id}" in path and "interactions" in path and "delete" in methods: + if re.fullmatch(r".*/interactions/\{[^/{}]+\}", path) and "delete" in methods: delete_path = path break