mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
Merge 686a3b72f3 into f96200bcea
This commit is contained in:
commit
43ab17331e
4 changed files with 112 additions and 12 deletions
|
|
@ -67,6 +67,7 @@ longer signal it.
|
|||
- **mcp_server, model, key, organization_member**: Requests now accept any 2xx response instead of requiring exactly HTTP 200; the proxy legitimately returns 201 from `POST /v1/mcp/server` (and other create endpoints), so `litellm_mcp_server` create previously succeeded on the proxy but failed in the provider, leaving the resource out of state and risking a duplicate server on retry
|
||||
- **unified_access_group**: create now accepts any 2xx response instead of requiring exactly HTTP 200; `POST /v1/unified_access_group` legitimately returns 201, so creation previously succeeded on the proxy but failed in the provider, leaving the group out of state and forcing a `terraform import` to recover on the next apply's 409. Same fix and shape as the one already applied to `mcp_server`/`model`/`key`/`organization_member`; `handleResponse` (shared by several other resources) was the one status-check helper that fix didn't reach. The legacy `litellm_access_group` resource calls the unrelated `/access_group/new` endpoint, which already returns 200, so it was never affected
|
||||
- **mcp_server**: A plan right after a clean apply of a minimal litellm_mcp_server no longer shows changes. alias is now computed so the proxy's default (the server name) is kept, spec_version keeps the configured value when the proxy returns none, and an mcp_info block the proxy fills with fields the provider does not model is no longer written to state
|
||||
- **tag**: Refresh now reads `model_max_budget` back from `/tag/info`, so a per-model budget changed or cleared outside Terraform shows up as drift and an imported tag no longer plans to set it again, and equivalent JSON (key order, `25` vs `25.0`) plans no change, matching `litellm_budget` and `litellm_key` (#47010). `budget_id` is now computed as well as optional, so a tag created with inline budget fields keeps the id of the budget the proxy made instead of planning `budget_id -> null` on every run
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ The following arguments are supported:
|
|||
* `name` - (Required, Forces new resource) Unique name of the tag. Also used as the resource ID
|
||||
* `description` - (Optional) Description of the tag
|
||||
* `models` - (Optional) List of model IDs this tag applies to
|
||||
* `budget_id` - (Optional) Existing budget ID to associate with this tag. If omitted and budget fields are set, the proxy creates a budget
|
||||
* `budget_id` - (Optional) Existing budget ID to associate with this tag. If omitted and budget fields are set, the proxy creates a budget and its ID is stored here
|
||||
* `max_budget` - (Optional) Max budget in USD for this tag
|
||||
* `soft_budget` - (Optional) Soft budget in USD for this tag
|
||||
* `max_parallel_requests` - (Optional) Max concurrent requests allowed for this tag
|
||||
|
|
|
|||
|
|
@ -19,13 +19,14 @@ const (
|
|||
)
|
||||
|
||||
type tagBudgetTable struct {
|
||||
BudgetID string `json:"budget_id"`
|
||||
MaxBudget *float64 `json:"max_budget"`
|
||||
SoftBudget *float64 `json:"soft_budget"`
|
||||
MaxParallelRequests *int `json:"max_parallel_requests"`
|
||||
TPMLimit *int `json:"tpm_limit"`
|
||||
RPMLimit *int `json:"rpm_limit"`
|
||||
BudgetDuration string `json:"budget_duration"`
|
||||
BudgetID string `json:"budget_id"`
|
||||
MaxBudget *float64 `json:"max_budget"`
|
||||
SoftBudget *float64 `json:"soft_budget"`
|
||||
MaxParallelRequests *int `json:"max_parallel_requests"`
|
||||
TPMLimit *int `json:"tpm_limit"`
|
||||
RPMLimit *int `json:"rpm_limit"`
|
||||
BudgetDuration string `json:"budget_duration"`
|
||||
ModelMaxBudget interface{} `json:"model_max_budget"`
|
||||
}
|
||||
|
||||
type tagInfoEntry struct {
|
||||
|
|
@ -68,7 +69,8 @@ func resourceLiteLLMTag() *schema.Resource {
|
|||
"budget_id": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: "Existing budget ID to associate with this tag.",
|
||||
Computed: true,
|
||||
Description: "Existing budget ID to associate with this tag. When unset, holds the ID of the budget the proxy creates for inline budget fields.",
|
||||
},
|
||||
"max_budget": {
|
||||
Type: schema.TypeFloat,
|
||||
|
|
@ -101,9 +103,10 @@ func resourceLiteLLMTag() *schema.Resource {
|
|||
Description: "Duration for budget reset (e.g. '1h', '1d', '30d').",
|
||||
},
|
||||
"model_max_budget": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: "JSON object string with per-model budget configuration.",
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DiffSuppressFunc: budgetSuppressEquivalentJSON,
|
||||
Description: "JSON object string with per-model budget configuration.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -233,6 +236,8 @@ func resourceLiteLLMTagRead(d *schema.ResourceData, m interface{}) error {
|
|||
d.Set("rpm_limit", *bt.RPMLimit)
|
||||
}
|
||||
d.Set("budget_duration", GetStringValue(bt.BudgetDuration, d.Get("budget_duration").(string)))
|
||||
modelMaxBudget, _ := budgetModelMaxBudgetString(bt.ModelMaxBudget)
|
||||
d.Set("model_max_budget", modelMaxBudget)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Successfully read tag with name: %s", d.Id())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package litellm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -8,6 +9,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
|
||||
)
|
||||
|
||||
func tagInfoBody(name string) string {
|
||||
|
|
@ -140,6 +142,98 @@ func TestResourceLiteLLMTagRead(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
const tagInfoWithModelMaxBudget = `{"prod": {
|
||||
"name": "prod",
|
||||
"litellm_budget_table": {
|
||||
"budget_id": "bud-created-by-proxy",
|
||||
"model_max_budget": {"gpt-4": {"max_budget": 25.0, "budget_duration": "1d"}}
|
||||
}
|
||||
}}`
|
||||
|
||||
// readTagFromProxy returns the state a Read leaves behind for a tag configured with config.
|
||||
func readTagFromProxy(t *testing.T, config map[string]interface{}) *schema.ResourceData {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(tagInfoWithModelMaxBudget))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMTag().Schema, config)
|
||||
d.SetId("prod")
|
||||
if err := resourceLiteLLMTagRead(d, NewClient(srv.URL, "test-key", true)); err != nil {
|
||||
t.Fatalf("read failed: %v", err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func planAttributes(t *testing.T, prior *schema.ResourceData, config map[string]interface{}) map[string]*terraform.ResourceAttrDiff {
|
||||
t.Helper()
|
||||
diff, err := resourceLiteLLMTag().Diff(context.Background(), prior.State(), terraform.NewResourceConfigRaw(config), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("diff failed: %v", err)
|
||||
}
|
||||
if diff == nil {
|
||||
return nil
|
||||
}
|
||||
return diff.Attributes
|
||||
}
|
||||
|
||||
func TestResourceLiteLLMTagRead_PicksUpModelMaxBudgetChangedOnTheProxy(t *testing.T) {
|
||||
d := readTagFromProxy(t, map[string]interface{}{
|
||||
"name": "prod",
|
||||
"model_max_budget": `{"gpt-4": {"max_budget": 10, "budget_duration": "1d"}}`,
|
||||
})
|
||||
|
||||
if !budgetSuppressEquivalentJSON("", d.Get("model_max_budget").(string), `{"gpt-4": {"max_budget": 25, "budget_duration": "1d"}}`, nil) {
|
||||
t.Errorf("expected the proxy's per-model budget in state, got %s", d.Get("model_max_budget"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceLiteLLMTag_ModelMaxBudgetClearedOnTheProxyPlansItBack(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{"prod": {"name": "prod", "litellm_budget_table": {"budget_id": "bud-1", "model_max_budget": null}}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
config := map[string]interface{}{
|
||||
"name": "prod",
|
||||
"model_max_budget": `{"gpt-4": {"max_budget": 10, "budget_duration": "1d"}}`,
|
||||
}
|
||||
prior := schema.TestResourceDataRaw(t, resourceLiteLLMTag().Schema, config)
|
||||
prior.SetId("prod")
|
||||
if err := resourceLiteLLMTagRead(prior, NewClient(srv.URL, "test-key", true)); err != nil {
|
||||
t.Fatalf("read failed: %v", err)
|
||||
}
|
||||
|
||||
attrs := planAttributes(t, prior, config)
|
||||
if attrs["model_max_budget"] == nil || attrs["model_max_budget"].Old != "" {
|
||||
t.Errorf("expected the plan to set model_max_budget again after it was cleared on the proxy, got %+v", attrs["model_max_budget"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceLiteLLMTag_PlanAfterInlineBudgetCreateKeepsTheProxyBudgetID(t *testing.T) {
|
||||
config := map[string]interface{}{
|
||||
"name": "prod",
|
||||
"model_max_budget": `{"gpt-4": {"max_budget": 25, "budget_duration": "1d"}}`,
|
||||
}
|
||||
prior := readTagFromProxy(t, config)
|
||||
|
||||
if attrs := planAttributes(t, prior, config); attrs["budget_id"] != nil {
|
||||
t.Errorf("expected no budget_id change when the config leaves it unset, got %+v", attrs["budget_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceLiteLLMTag_EquivalentModelMaxBudgetJSONPlansNoChange(t *testing.T) {
|
||||
prior := readTagFromProxy(t, map[string]interface{}{"name": "prod"})
|
||||
|
||||
attrs := planAttributes(t, prior, map[string]interface{}{
|
||||
"name": "prod",
|
||||
"model_max_budget": `{"gpt-4": {"max_budget": 25.0, "budget_duration": "1d"}}`,
|
||||
})
|
||||
if attrs["model_max_budget"] != nil {
|
||||
t.Errorf("expected key order and number formatting to plan no change, got %+v", attrs["model_max_budget"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceLiteLLMTagRead_404ClearsID(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue