mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
Merge f75725929f into dab2deb5ed
This commit is contained in:
commit
19721b7171
5 changed files with 108 additions and 8 deletions
|
|
@ -126,13 +126,18 @@ func (c *Client) UpdateKey(key *Key) (*Key, error) {
|
|||
updateData := map[string]interface{}{
|
||||
"key": key.Key,
|
||||
"team_id": key.TeamID,
|
||||
"key_alias": key.KeyAlias,
|
||||
"aliases": key.Aliases,
|
||||
"permissions": key.Permissions,
|
||||
"model_max_budget": key.ModelMaxBudget,
|
||||
"blocked": key.Blocked,
|
||||
}
|
||||
|
||||
if key.KeyAlias != "" {
|
||||
updateData["key_alias"] = key.KeyAlias
|
||||
} else if key.clearKeyAlias {
|
||||
updateData["key_alias"] = nil
|
||||
}
|
||||
|
||||
// The proxy keeps the stored metadata only when the field is absent, so nil means omit.
|
||||
if key.Metadata != nil {
|
||||
updateData["metadata"] = key.Metadata
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ func resourceKeyUpdate(ctx context.Context, d *schema.ResourceData, m interface{
|
|||
|
||||
key := &Key{Key: d.Id()}
|
||||
mapResourceDataToKey(d, key)
|
||||
key.clearKeyAlias = d.HasChange("key_alias") && key.KeyAlias == ""
|
||||
if !d.HasChange("duration") {
|
||||
key.Duration = ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,6 +321,98 @@ func TestUpdateKeyOmitsEmptyBudgetDuration(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUpdateKeyOmitsEmptyKeyAlias(t *testing.T) {
|
||||
var captured map[string]interface{}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
json.Unmarshal(body, &captured)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"key": "sk-test"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(srv.URL, "test-key", true)
|
||||
if _, err := client.UpdateKey(&Key{Key: "sk-test"}); err != nil {
|
||||
t.Fatalf("UpdateKey returned error: %v", err)
|
||||
}
|
||||
if _, present := captured["key_alias"]; present {
|
||||
t.Errorf("update payload contains empty key_alias: %v", captured["key_alias"])
|
||||
}
|
||||
|
||||
if _, err := client.UpdateKey(&Key{Key: "sk-test", KeyAlias: "alias-1"}); err != nil {
|
||||
t.Fatalf("UpdateKey returned error: %v", err)
|
||||
}
|
||||
if captured["key_alias"] != "alias-1" {
|
||||
t.Errorf("key_alias = %v, want alias-1", captured["key_alias"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceKeyAliasChangesConverge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
priorAlias string
|
||||
configuredAlias interface{}
|
||||
wantAlias string
|
||||
wantPresent bool
|
||||
wantNull bool
|
||||
}{
|
||||
{name: "aliasless"},
|
||||
{name: "remove", priorAlias: "alias-1", wantPresent: true, wantNull: true},
|
||||
{name: "blank", priorAlias: "alias-1", configuredAlias: "", wantPresent: true, wantNull: true},
|
||||
{name: "assign", configuredAlias: "alias-1", wantAlias: "alias-1", wantPresent: true},
|
||||
{name: "rename", priorAlias: "alias-1", configuredAlias: "alias-2", wantAlias: "alias-2", wantPresent: true},
|
||||
{name: "retain", priorAlias: "alias-1", configuredAlias: "alias-1", wantAlias: "alias-1", wantPresent: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
storedAlias := tc.priorAlias
|
||||
updates := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if req.URL.Path == "/key/update" {
|
||||
updates++
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
|
||||
t.Error(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
value, present := payload["key_alias"]
|
||||
if present != tc.wantPresent || (present && (value == nil) != tc.wantNull) {
|
||||
t.Errorf("key_alias = %#v, present = %v; want present = %v, null = %v", value, present, tc.wantPresent, tc.wantNull)
|
||||
}
|
||||
if present {
|
||||
storedAlias, _ = value.(string)
|
||||
}
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"key": "hash-1",
|
||||
"info": map[string]interface{}{"key_alias": storedAlias, "team_id": "team-1"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
res := resourceKey()
|
||||
priorData := newKeyResourceData(t, map[string]interface{}{"key_alias": tc.priorAlias, "team_id": "team-1", "max_budget": 10.0})
|
||||
priorData.SetId("hash-1")
|
||||
config := terraform.NewResourceConfigRaw(map[string]interface{}{"key_alias": tc.configuredAlias, "team_id": "team-1", "max_budget": 25.0})
|
||||
diff, err := res.Diff(context.Background(), priorData.State(), config, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state, diags := res.Apply(context.Background(), priorData.State(), diff, NewClient(srv.URL, "test-key", true))
|
||||
if diags.HasError() {
|
||||
t.Fatalf("apply failed: %v", diags)
|
||||
}
|
||||
if updates != 1 || storedAlias != tc.wantAlias || state.Attributes["key_alias"] != tc.wantAlias {
|
||||
t.Fatalf("updates = %d, stored alias = %q, state alias = %q; want %q", updates, storedAlias, state.Attributes["key_alias"], tc.wantAlias)
|
||||
}
|
||||
nextDiff, err := res.Diff(context.Background(), state, config, nil)
|
||||
if err != nil || (nextDiff != nil && !nextDiff.Empty()) {
|
||||
t.Fatalf("subsequent plan not clean: diff = %v, error = %v", nextDiff, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceKeyUpdateFailureKeepsPriorState(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ type ModelInfo struct {
|
|||
|
||||
// Key represents a LiteLLM API key.
|
||||
type Key struct {
|
||||
clearKeyAlias bool
|
||||
Key string `json:"key,omitempty"`
|
||||
TokenID string `json:"token_id,omitempty"`
|
||||
Models []string `json:"models"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue