mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
* feat(terraform): close key/team schema gaps, fix team read envelope, add import support * feat(terraform): add fallback resource/data source and key/team block resources * feat(terraform): add access group and unified access group resources and data sources * feat(terraform): add guardrail and prompt resources and data sources * feat(terraform): add agent and search tool resources and data sources * feat(terraform): add user and budget resources and data sources * feat(terraform): add tag and project resources and data sources * docs(terraform): changelog and readme for parity additions * feat(terraform): add data sources for keys, teams, models, organizations, and mcp servers * fix(terraform): key update 400 on empty budget_duration, key info envelope, config-supplied key * fix(terraform): hash raw keys to SHA-256 tokens in key lookup URLs and block resource IDs
384 lines
11 KiB
Go
384 lines
11 KiB
Go
package litellm
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
|
)
|
|
|
|
const (
|
|
endpointKeyInfo = "/key/info"
|
|
endpointKeyList = "/key/list"
|
|
)
|
|
|
|
type keyInfoDetail struct {
|
|
Token string `json:"token"`
|
|
KeyName string `json:"key_name"`
|
|
KeyAlias string `json:"key_alias"`
|
|
Spend float64 `json:"spend"`
|
|
MaxBudget *float64 `json:"max_budget"`
|
|
Models []string `json:"models"`
|
|
UserID string `json:"user_id"`
|
|
TeamID string `json:"team_id"`
|
|
OrgID string `json:"org_id"`
|
|
TPMLimit *int `json:"tpm_limit"`
|
|
RPMLimit *int `json:"rpm_limit"`
|
|
MaxParallelRequests *int `json:"max_parallel_requests"`
|
|
BudgetDuration string `json:"budget_duration"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
Blocked *bool `json:"blocked"`
|
|
Expires string `json:"expires"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
type keyInfoEnvelope struct {
|
|
Key string `json:"key"`
|
|
Info keyInfoDetail `json:"info"`
|
|
}
|
|
|
|
type keyListEnvelope struct {
|
|
Keys []keyInfoDetail `json:"keys"`
|
|
TotalCount int `json:"total_count"`
|
|
CurrentPage int `json:"current_page"`
|
|
TotalPages int `json:"total_pages"`
|
|
}
|
|
|
|
func dataSourceLiteLLMKey() *schema.Resource {
|
|
return &schema.Resource{
|
|
Read: dataSourceLiteLLMKeyRead,
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"key": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
Sensitive: true,
|
|
Description: "The API key (or its hash) to look up",
|
|
},
|
|
"token_id": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Description: "Hashed token identifier of the key",
|
|
},
|
|
"key_name": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Description: "Redacted display name of the key",
|
|
},
|
|
"key_alias": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"models": {
|
|
Type: schema.TypeList,
|
|
Computed: true,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
},
|
|
"spend": {
|
|
Type: schema.TypeFloat,
|
|
Computed: true,
|
|
},
|
|
"max_budget": {
|
|
Type: schema.TypeFloat,
|
|
Computed: true,
|
|
},
|
|
"user_id": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"team_id": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"organization_id": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"tpm_limit": {
|
|
Type: schema.TypeInt,
|
|
Computed: true,
|
|
},
|
|
"rpm_limit": {
|
|
Type: schema.TypeInt,
|
|
Computed: true,
|
|
},
|
|
"max_parallel_requests": {
|
|
Type: schema.TypeInt,
|
|
Computed: true,
|
|
},
|
|
"budget_duration": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"metadata": {
|
|
Type: schema.TypeMap,
|
|
Computed: true,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
},
|
|
"tags": {
|
|
Type: schema.TypeList,
|
|
Computed: true,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
},
|
|
"blocked": {
|
|
Type: schema.TypeBool,
|
|
Computed: true,
|
|
},
|
|
"expires": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"created_at": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"updated_at": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func dataSourceLiteLLMKeyRead(d *schema.ResourceData, m interface{}) error {
|
|
client := m.(*Client)
|
|
// Look up by the SHA-256 token hash so the raw key never appears in the
|
|
// request URL, where reverse-proxy access logs could record it.
|
|
key := hashedKeyToken(d.Get("key").(string))
|
|
|
|
resp, err := MakeRequest(client, "GET", fmt.Sprintf("%s?key=%s", endpointKeyInfo, url.QueryEscape(key)), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read key info: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if err := handleResponse(resp, "reading key info"); err != nil {
|
|
return err
|
|
}
|
|
|
|
var envelope keyInfoEnvelope
|
|
if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
|
|
return fmt.Errorf("failed to decode key info response: %w", err)
|
|
}
|
|
info := envelope.Info
|
|
|
|
// Never persist the raw key as the ID; the hashed token is safe to store.
|
|
d.SetId(GetStringValue(info.Token, "key"))
|
|
d.Set("token_id", info.Token)
|
|
d.Set("key_name", info.KeyName)
|
|
d.Set("key_alias", info.KeyAlias)
|
|
d.Set("models", info.Models)
|
|
d.Set("spend", info.Spend)
|
|
if info.MaxBudget != nil {
|
|
d.Set("max_budget", *info.MaxBudget)
|
|
}
|
|
d.Set("user_id", info.UserID)
|
|
d.Set("team_id", info.TeamID)
|
|
d.Set("organization_id", info.OrgID)
|
|
if info.TPMLimit != nil {
|
|
d.Set("tpm_limit", *info.TPMLimit)
|
|
}
|
|
if info.RPMLimit != nil {
|
|
d.Set("rpm_limit", *info.RPMLimit)
|
|
}
|
|
if info.MaxParallelRequests != nil {
|
|
d.Set("max_parallel_requests", *info.MaxParallelRequests)
|
|
}
|
|
d.Set("budget_duration", info.BudgetDuration)
|
|
|
|
metadata := map[string]string{}
|
|
for k, v := range info.Metadata {
|
|
if s, ok := v.(string); ok {
|
|
metadata[k] = s
|
|
}
|
|
}
|
|
d.Set("metadata", metadata)
|
|
d.Set("tags", toStringSlice(info.Metadata["tags"]))
|
|
|
|
if info.Blocked != nil {
|
|
d.Set("blocked", *info.Blocked)
|
|
}
|
|
d.Set("expires", info.Expires)
|
|
d.Set("created_at", info.CreatedAt)
|
|
d.Set("updated_at", info.UpdatedAt)
|
|
|
|
log.Printf("[INFO] Successfully read key info for token: %s", info.Token)
|
|
return nil
|
|
}
|
|
|
|
func dataSourceLiteLLMKeys() *schema.Resource {
|
|
return &schema.Resource{
|
|
Read: dataSourceLiteLLMKeysRead,
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"page": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Default: 1,
|
|
Description: "Page number for pagination",
|
|
},
|
|
"size": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Default: 100,
|
|
Description: "Number of keys per page",
|
|
},
|
|
"user_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "Filter keys by user ID",
|
|
},
|
|
"team_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "Filter keys by team ID",
|
|
},
|
|
"organization_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "Filter keys by organization ID",
|
|
},
|
|
"key_alias": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "Filter keys by key alias",
|
|
},
|
|
"include_team_keys": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Description: "Include all keys for teams the caller is an admin of",
|
|
},
|
|
"total_count": {
|
|
Type: schema.TypeInt,
|
|
Computed: true,
|
|
},
|
|
"total_pages": {
|
|
Type: schema.TypeInt,
|
|
Computed: true,
|
|
},
|
|
"current_page": {
|
|
Type: schema.TypeInt,
|
|
Computed: true,
|
|
},
|
|
"ids": {
|
|
Type: schema.TypeList,
|
|
Computed: true,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Description: "Hashed token identifiers of the returned keys",
|
|
},
|
|
"keys": {
|
|
Type: schema.TypeList,
|
|
Computed: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"token_id": {Type: schema.TypeString, Computed: true},
|
|
"key_name": {Type: schema.TypeString, Computed: true},
|
|
"key_alias": {Type: schema.TypeString, Computed: true},
|
|
"spend": {Type: schema.TypeFloat, Computed: true},
|
|
"max_budget": {Type: schema.TypeFloat, Computed: true},
|
|
"models": {Type: schema.TypeList, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}},
|
|
"user_id": {Type: schema.TypeString, Computed: true},
|
|
"team_id": {Type: schema.TypeString, Computed: true},
|
|
"organization_id": {Type: schema.TypeString, Computed: true},
|
|
"tpm_limit": {Type: schema.TypeInt, Computed: true},
|
|
"rpm_limit": {Type: schema.TypeInt, Computed: true},
|
|
"budget_duration": {Type: schema.TypeString, Computed: true},
|
|
"blocked": {Type: schema.TypeBool, Computed: true},
|
|
"expires": {Type: schema.TypeString, Computed: true},
|
|
"created_at": {Type: schema.TypeString, Computed: true},
|
|
"updated_at": {Type: schema.TypeString, Computed: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func dataSourceLiteLLMKeysRead(d *schema.ResourceData, m interface{}) error {
|
|
client := m.(*Client)
|
|
|
|
query := url.Values{}
|
|
query.Set("return_full_object", "true")
|
|
query.Set("page", strconv.Itoa(d.Get("page").(int)))
|
|
query.Set("size", strconv.Itoa(d.Get("size").(int)))
|
|
for param, attr := range map[string]string{
|
|
"user_id": "user_id",
|
|
"team_id": "team_id",
|
|
"organization_id": "organization_id",
|
|
"key_alias": "key_alias",
|
|
} {
|
|
if v, ok := d.GetOk(attr); ok {
|
|
query.Set(param, v.(string))
|
|
}
|
|
}
|
|
if d.Get("include_team_keys").(bool) {
|
|
query.Set("include_team_keys", "true")
|
|
}
|
|
|
|
resp, err := MakeRequest(client, "GET", fmt.Sprintf("%s?%s", endpointKeyList, query.Encode()), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list keys: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if err := handleResponse(resp, "listing keys"); err != nil {
|
|
return err
|
|
}
|
|
|
|
var envelope keyListEnvelope
|
|
if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
|
|
return fmt.Errorf("failed to decode key list response: %w", err)
|
|
}
|
|
|
|
ids := make([]string, 0, len(envelope.Keys))
|
|
keys := make([]map[string]interface{}, 0, len(envelope.Keys))
|
|
for _, k := range envelope.Keys {
|
|
ids = append(ids, k.Token)
|
|
keys = append(keys, map[string]interface{}{
|
|
"token_id": k.Token,
|
|
"key_name": k.KeyName,
|
|
"key_alias": k.KeyAlias,
|
|
"spend": k.Spend,
|
|
"max_budget": keyDerefFloat(k.MaxBudget),
|
|
"models": k.Models,
|
|
"user_id": k.UserID,
|
|
"team_id": k.TeamID,
|
|
"organization_id": k.OrgID,
|
|
"tpm_limit": keyDerefInt(k.TPMLimit),
|
|
"rpm_limit": keyDerefInt(k.RPMLimit),
|
|
"budget_duration": k.BudgetDuration,
|
|
"blocked": k.Blocked != nil && *k.Blocked,
|
|
"expires": k.Expires,
|
|
"created_at": k.CreatedAt,
|
|
"updated_at": k.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
d.SetId(query.Encode())
|
|
d.Set("total_count", envelope.TotalCount)
|
|
d.Set("total_pages", envelope.TotalPages)
|
|
d.Set("current_page", envelope.CurrentPage)
|
|
d.Set("ids", ids)
|
|
d.Set("keys", keys)
|
|
|
|
log.Printf("[INFO] Successfully listed %d keys", len(keys))
|
|
return nil
|
|
}
|
|
|
|
func keyDerefFloat(v *float64) float64 {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func keyDerefInt(v *int) int {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
return *v
|
|
}
|