From 5ce026495e749f194a8b1fc70341823ec6512b16 Mon Sep 17 00:00:00 2001 From: Vasil Date: Fri, 23 Oct 2026 18:00:00 +0200 Subject: [PATCH 1/2] feat(keys): key policies, per-key spend and prompt-cache prices The Keys page shows each key's policy and its spend against its caps, edits the policy in a dialog and sets rates when minting; team leads do it for their team's keys only. The model editor takes cache read and write prices and the prompt_cache capability. Proto synced from KTMS; the e2e test covers policy and spend and runs against the 1.0 release branches. --- CHANGELOG.md | 4 + README.md | 6 +- internal/api/api.go | 2 + internal/api/api_test.go | 2 + internal/api/keypolicy_test.go | 64 ++ internal/api/proxy.go | 84 +- internal/gen/ktms/v1/admin.pb.go | 1059 ++++++++++++++++------- internal/gen/ktms/v1/admin_grpc.pb.go | 76 ++ internal/gen/ktms/v1/gateway.pb.go | 59 +- internal/gen/ktms/v1/gateway_grpc.pb.go | 22 +- internal/web/static/js/pages/keys.js | 72 +- internal/web/static/js/pages/models.js | 7 +- proto/ktms/v1/admin.proto | 50 ++ proto/ktms/v1/gateway.proto | 17 +- scripts/e2e.refs | 6 +- scripts/e2e.sh | 15 +- 16 files changed, 1178 insertions(+), 367 deletions(-) create mode 100644 internal/api/keypolicy_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 02768cb..b825ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ The first stable release, paired with Keystone. The Model Switchboard. (KTMS) 1. - Sign-in through an access proxy: an e-mail header from `TRUSTED_PROXY_CIDRS` (oauth2-proxy, Authelia, Authentik, Pomerium, Tailscale) or a verified Cloudflare Access token (`CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD`). +- Key policies on the Keys page: allowed models and requests and tokens per minute, set when minting + or later (team leads for their team's keys), and each key's spend against its caps. +- Prompt-cache prices (cached input, cache write) and the `prompt_cache` capability in the model + editor. ### Changed - Google sign-in is optional: configure any of the methods above, alone or together. Sign-in methods diff --git a/README.md b/README.md index c5cb0a5..62bcda0 100644 --- a/README.md +++ b/README.md @@ -316,8 +316,10 @@ own validation. **Limits.** Per-user rate limits and daily token budgets, and spend caps (USD per UTC day or month) for keys and users. -**Keys.** A wizard mints client API keys: name and team, access, optional daily and monthly spend caps; the -secret is shown once with a copy button. Revoke keys, assign vendor credentials to clients (write-only: +**Keys.** A wizard mints client API keys: name and team, access, optional daily and monthly spend caps and +request and token rates; the secret is shown once with a copy button. Each key's policy (allowed models, +requests and tokens per minute) is edited in place, and its spend shows against its caps as KTMS counts it; +team leads do both for their team's keys. Revoke keys, assign vendor credentials to clients (write-only: stored encrypted by the gateway and never shown again) and re-encrypt all credentials after adding a new encryption key version. diff --git a/internal/api/api.go b/internal/api/api.go index 49c8026..f260380 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -110,6 +110,8 @@ func (s *Server) Handler() http.Handler { api("GET /api/proxy/keys", team, s.proxyListKeys) api("POST /api/proxy/keys", team, s.proxyMintKey) api("DELETE /api/proxy/keys/{name}", team, s.proxyRevokeKey) + api("PUT /api/proxy/keys/{name}/policy", team, s.proxySetKeyPolicy) + api("GET /api/proxy/spend", team, s.proxySpend) api("GET /api/proxy/nodes", globalRead, s.proxyListNodes) api("POST /api/proxy/nodes/{id}/drain", adminOnly, s.proxyDrainNode) api("GET /api/proxy/node-keys", globalRead, s.proxyListNodeKeys) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 2bbe155..02eec0d 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -33,6 +33,8 @@ type fakeAdmin struct { lastMint *pb.MintAPIKeyRequest lastRevoke string spendLimits []*pb.SpendLimitSpec + lastPolicy *pb.SetAPIKeyPolicyRequest + lastSpend *pb.GetSpendRequest } type fakeData struct { diff --git a/internal/api/keypolicy_test.go b/internal/api/keypolicy_test.go new file mode 100644 index 0000000..478c5bd --- /dev/null +++ b/internal/api/keypolicy_test.go @@ -0,0 +1,64 @@ +package api + +import ( + "context" + "net/http" + "strings" + "testing" + + "google.golang.org/grpc" + + pb "github.com/ai-process/ktms-admin/internal/gen/ktms/v1" +) + +func (f *fakeAdmin) SetAPIKeyPolicy(_ context.Context, r *pb.SetAPIKeyPolicyRequest, _ ...grpc.CallOption) (*pb.SetAPIKeyPolicyResponse, error) { + f.lastPolicy = r + return &pb.SetAPIKeyPolicyResponse{}, nil +} + +func (f *fakeAdmin) GetSpend(_ context.Context, r *pb.GetSpendRequest, _ ...grpc.CallOption) (*pb.GetSpendResponse, error) { + f.lastSpend = r + return &pb.GetSpendResponse{Windows: []*pb.SpendWindow{{Period: "day", SpentNanousd: 1e9, LimitNanousd: 5e9, RemainingNanousd: 4e9, Metered: true}}}, nil +} + +func TestKeyPolicyAccess(t *testing.T) { + h, fa := tenantServer(t) + lead, dev, admin := sessionCookie(t, "lead@corp.com"), sessionCookie(t, "dev@corp.com"), sessionCookie(t, "boss@x.com") + body := `{"allowedModels":["m1"],"rpmLimit":60,"tpmLimit":"1000"}` + + if w := call(h, "PUT", "/api/proxy/keys/billing-service/policy", body, lead, csrf); w.Code != http.StatusForbidden || fa.lastPolicy != nil { + t.Fatalf("lead on another team's key = %d, want 403 and no change", w.Code) + } + if w := call(h, "PUT", "/api/proxy/keys/search-service/policy", body, dev, csrf); w.Code != http.StatusForbidden || fa.lastPolicy != nil { + t.Fatalf("member = %d, want 403", w.Code) + } + if w := call(h, "PUT", "/api/proxy/keys/search-service/policy", body, lead, csrf); w.Code != http.StatusOK { + t.Fatalf("lead on own key = %d %s", w.Code, w.Body) + } + if p := fa.lastPolicy; p.GetName() != "search-service" || p.GetPolicy().GetRpmLimit() != 60 || p.GetPolicy().GetTpmLimit() != 1000 || len(p.GetPolicy().GetAllowedModels()) != 1 { + t.Fatalf("policy sent = %v", p) + } + if w := call(h, "PUT", "/api/proxy/keys/billing-service/policy", body, admin, csrf); w.Code != http.StatusOK { + t.Fatalf("admin = %d %s", w.Code, w.Body) + } +} + +func TestSpendAccess(t *testing.T) { + h, fa := tenantServer(t) + dev, viewer := sessionCookie(t, "dev@corp.com"), sessionCookie(t, "p@view.com") + for path, want := range map[string]int{ + "/api/proxy/spend?subject_type=key&subject_id=search-service": http.StatusOK, + "/api/proxy/spend?subject_type=team&subject_id=team-1": http.StatusOK, + "/api/proxy/spend?subject_type=key&subject_id=billing-service": http.StatusForbidden, + "/api/proxy/spend?subject_type=user&subject_id=u1": http.StatusForbidden, + "/api/proxy/spend?subject_type=team&subject_id=team-2": http.StatusForbidden, + } { + if w := call(h, "GET", path, "", dev, nil); w.Code != want { + t.Errorf("member GET %s = %d, want %d", path, w.Code, want) + } + } + w := call(h, "GET", "/api/proxy/spend?subject_type=user&subject_id=u1", "", viewer, nil) + if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), `"remaining_nanousd":"4000000000"`) || fa.lastSpend.GetSubjectId() != "u1" { + t.Fatalf("viewer = %d %s", w.Code, w.Body) + } +} diff --git a/internal/api/proxy.go b/internal/api/proxy.go index f99d7ab..aa7a7c2 100644 --- a/internal/api/proxy.go +++ b/internal/api/proxy.go @@ -97,15 +97,11 @@ func (s *Server) proxyRevokeKey(w http.ResponseWriter, r *http.Request, u *auth. ctx, cancel := context.WithTimeout(r.Context(), rpcTimeout) defer cancel() if !u.IsSuperAdmin() { - keysResp, err := s.admin.ListAPIKeys(ctx, &pb.ListAPIKeysRequest{}) + isOwnTeam, err := s.ownsKey(ctx, u, name) if err != nil { backendErr(w, err) return } - isOwnTeam := false - for _, k := range keysOfTeam(keysResp.Keys, u.TeamID) { - isOwnTeam = isOwnTeam || k.Name == name - } if !isOwnTeam { writeErr(w, http.StatusForbidden, "forbidden", "cannot revoke keys belonging to other teams") return @@ -118,6 +114,84 @@ func (s *Server) proxyRevokeKey(w http.ResponseWriter, r *http.Request, u *auth. writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) } +// ownsKey reports whether a user who is not a super admin may manage the key: +// only keys of their own team. +func (s *Server) ownsKey(ctx context.Context, u *auth.User, name string) (bool, error) { + keysResp, err := s.admin.ListAPIKeys(ctx, &pb.ListAPIKeysRequest{}) + if err != nil { + return false, err + } + for _, k := range keysOfTeam(keysResp.Keys, u.TeamID) { + if k.Name == name { + return true, nil + } + } + return false, nil +} + +// proxySetKeyPolicy replaces a key's allowed models and rates. A team lead may +// set them on the team's keys: they narrow what the team allows, never widen it. +func (s *Server) proxySetKeyPolicy(w http.ResponseWriter, r *http.Request, u *auth.User) { + var policy pb.APIKeyPolicy + if err := readProto(r, &policy); err != nil { + backendErr(w, badBody(err)) + return + } + name := r.PathValue("name") + ctx, cancel := context.WithTimeout(r.Context(), rpcTimeout) + defer cancel() + if !u.IsSuperAdmin() { + if !u.IsTeamLead() { + writeErr(w, http.StatusForbidden, "forbidden", "only team leads and admins set key policies") + return + } + ok, err := s.ownsKey(ctx, u, name) + if err != nil { + backendErr(w, err) + return + } + if !ok { + writeErr(w, http.StatusForbidden, "forbidden", "cannot change keys belonging to other teams") + return + } + } + if _, err := s.admin.SetAPIKeyPolicy(ctx, &pb.SetAPIKeyPolicyRequest{Name: name, Policy: &policy}); err != nil { + backendErr(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) +} + +// proxySpend reports a subject's spend as KTMS enforces it. Team members see +// their own team's keys and their team; everything else needs global read. +func (s *Server) proxySpend(w http.ResponseWriter, r *http.Request, u *auth.User) { + q := r.URL.Query() + req := &pb.GetSpendRequest{SubjectType: q.Get("subject_type"), SubjectId: q.Get("subject_id")} + ctx, cancel := context.WithTimeout(r.Context(), rpcTimeout) + defer cancel() + if !u.CanReadGlobal() { + allowed := req.SubjectType == "team" && req.SubjectId == u.TeamID && u.TeamID != "" + if req.SubjectType == "key" { + ok, err := s.ownsKey(ctx, u, req.SubjectId) + if err != nil { + backendErr(w, err) + return + } + allowed = ok + } + if !allowed { + writeErr(w, http.StatusForbidden, "forbidden", "only your team's spend is visible to you") + return + } + } + resp, err := s.admin.GetSpend(ctx, req) + if err != nil { + backendErr(w, err) + return + } + writeProto(w, resp) +} + func hasField(m map[string]json.RawMessage, keys ...string) bool { for _, k := range keys { if _, ok := m[k]; ok { diff --git a/internal/gen/ktms/v1/admin.pb.go b/internal/gen/ktms/v1/admin.pb.go index 57fef26..757590b 100644 --- a/internal/gen/ktms/v1/admin.pb.go +++ b/internal/gen/ktms/v1/admin.pb.go @@ -46,8 +46,14 @@ type ModelSpec struct { RpmBurst int32 `protobuf:"varint,16,opt,name=rpm_burst,json=rpmBurst,proto3" json:"rpm_burst,omitempty"` // extra burst allowed above steady-state rpm (0 = default burst of rpm - 1, <0 = burst disabled) QueueTimeoutMs int32 `protobuf:"varint,17,opt,name=queue_timeout_ms,json=queueTimeoutMs,proto3" json:"queue_timeout_ms,omitempty"` // max wait for pool inference slot in ms (0 = fallback immediately; capped to 250ms for interactive non-svc users) BudgetGraceMargin float64 `protobuf:"fixed64,18,opt,name=budget_grace_margin,json=budgetGraceMargin,proto3" json:"budget_grace_margin,omitempty"` // allowed daily budget overdraft (0.0 = system default 0.05, <0.0 = disabled) - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Prompt-cache rates, standard (off-peak) like price_in_per_mtok; peak and batch + // calls scale them by the same ratio as input. 0 = the vendor default: for + // anthropic 0.1x input to read and 1.25x to write, for bedrock 0.25x and 1.25x, + // for every other vendor 1x. + PriceCacheReadPerMtok float64 `protobuf:"fixed64,19,opt,name=price_cache_read_per_mtok,json=priceCacheReadPerMtok,proto3" json:"price_cache_read_per_mtok,omitempty"` + PriceCacheWritePerMtok float64 `protobuf:"fixed64,20,opt,name=price_cache_write_per_mtok,json=priceCacheWritePerMtok,proto3" json:"price_cache_write_per_mtok,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ModelSpec) Reset() { @@ -206,6 +212,20 @@ func (x *ModelSpec) GetBudgetGraceMargin() float64 { return 0 } +func (x *ModelSpec) GetPriceCacheReadPerMtok() float64 { + if x != nil { + return x.PriceCacheReadPerMtok + } + return 0 +} + +func (x *ModelSpec) GetPriceCacheWritePerMtok() float64 { + if x != nil { + return x.PriceCacheWritePerMtok + } + return 0 +} + type RuleSpec struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // unique, logged and put in usage meta @@ -943,6 +963,7 @@ type MintAPIKeyRequest struct { Scopes []string `protobuf:"bytes,2,rep,name=scopes,proto3" json:"scopes,omitempty"` // "generate" | "admin" OrgId string `protobuf:"bytes,3,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` TeamId string `protobuf:"bytes,4,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + Policy *APIKeyPolicy `protobuf:"bytes,5,opt,name=policy,proto3" json:"policy,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1005,6 +1026,168 @@ func (x *MintAPIKeyRequest) GetTeamId() string { return "" } +func (x *MintAPIKeyRequest) GetPolicy() *APIKeyPolicy { + if x != nil { + return x.Policy + } + return nil +} + +// APIKeyPolicy limits one client key, on top of its team's limits and each +// model's rpm. Refusals: PERMISSION_DENIED for a model outside allowed_models, +// RESOURCE_EXHAUSTED throttled:key_rpm / throttled:key_tpm for the rates, and +// FAILED_PRECONDITION request_too_large for a call above a minute of tokens. +type APIKeyPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + AllowedModels []string `protobuf:"bytes,1,rep,name=allowed_models,json=allowedModels,proto3" json:"allowed_models,omitempty"` // model ids the key may call; empty = any + RpmLimit int32 `protobuf:"varint,2,opt,name=rpm_limit,json=rpmLimit,proto3" json:"rpm_limit,omitempty"` // calls per minute over all models, 0 = unlimited + // Tokens per minute over all models, 0 = unlimited. A call books its input + // estimate when admitted and its real input + output when it ends. + TpmLimit int64 `protobuf:"varint,3,opt,name=tpm_limit,json=tpmLimit,proto3" json:"tpm_limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *APIKeyPolicy) Reset() { + *x = APIKeyPolicy{} + mi := &file_ktms_v1_admin_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *APIKeyPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APIKeyPolicy) ProtoMessage() {} + +func (x *APIKeyPolicy) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APIKeyPolicy.ProtoReflect.Descriptor instead. +func (*APIKeyPolicy) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{16} +} + +func (x *APIKeyPolicy) GetAllowedModels() []string { + if x != nil { + return x.AllowedModels + } + return nil +} + +func (x *APIKeyPolicy) GetRpmLimit() int32 { + if x != nil { + return x.RpmLimit + } + return 0 +} + +func (x *APIKeyPolicy) GetTpmLimit() int64 { + if x != nil { + return x.TpmLimit + } + return 0 +} + +// SetAPIKeyPolicy replaces a key's policy; it applies from the key's next call. +type SetAPIKeyPolicyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Policy *APIKeyPolicy `protobuf:"bytes,2,opt,name=policy,proto3" json:"policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetAPIKeyPolicyRequest) Reset() { + *x = SetAPIKeyPolicyRequest{} + mi := &file_ktms_v1_admin_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetAPIKeyPolicyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetAPIKeyPolicyRequest) ProtoMessage() {} + +func (x *SetAPIKeyPolicyRequest) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetAPIKeyPolicyRequest.ProtoReflect.Descriptor instead. +func (*SetAPIKeyPolicyRequest) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{17} +} + +func (x *SetAPIKeyPolicyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SetAPIKeyPolicyRequest) GetPolicy() *APIKeyPolicy { + if x != nil { + return x.Policy + } + return nil +} + +type SetAPIKeyPolicyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetAPIKeyPolicyResponse) Reset() { + *x = SetAPIKeyPolicyResponse{} + mi := &file_ktms_v1_admin_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetAPIKeyPolicyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetAPIKeyPolicyResponse) ProtoMessage() {} + +func (x *SetAPIKeyPolicyResponse) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetAPIKeyPolicyResponse.ProtoReflect.Descriptor instead. +func (*SetAPIKeyPolicyResponse) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{18} +} + type MintAPIKeyResponse struct { state protoimpl.MessageState `protogen:"open.v1"` PlainKey string `protobuf:"bytes,1,opt,name=plain_key,json=plainKey,proto3" json:"plain_key,omitempty"` // returned exactly once, never stored or logged @@ -1014,7 +1197,7 @@ type MintAPIKeyResponse struct { func (x *MintAPIKeyResponse) Reset() { *x = MintAPIKeyResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[16] + mi := &file_ktms_v1_admin_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1026,7 +1209,7 @@ func (x *MintAPIKeyResponse) String() string { func (*MintAPIKeyResponse) ProtoMessage() {} func (x *MintAPIKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[16] + mi := &file_ktms_v1_admin_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1039,7 +1222,7 @@ func (x *MintAPIKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintAPIKeyResponse.ProtoReflect.Descriptor instead. func (*MintAPIKeyResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{16} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{19} } func (x *MintAPIKeyResponse) GetPlainKey() string { @@ -1058,7 +1241,7 @@ type RevokeAPIKeyRequest struct { func (x *RevokeAPIKeyRequest) Reset() { *x = RevokeAPIKeyRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[17] + mi := &file_ktms_v1_admin_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1070,7 +1253,7 @@ func (x *RevokeAPIKeyRequest) String() string { func (*RevokeAPIKeyRequest) ProtoMessage() {} func (x *RevokeAPIKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[17] + mi := &file_ktms_v1_admin_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1083,7 +1266,7 @@ func (x *RevokeAPIKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeAPIKeyRequest.ProtoReflect.Descriptor instead. func (*RevokeAPIKeyRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{17} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{20} } func (x *RevokeAPIKeyRequest) GetName() string { @@ -1101,7 +1284,7 @@ type RevokeAPIKeyResponse struct { func (x *RevokeAPIKeyResponse) Reset() { *x = RevokeAPIKeyResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[18] + mi := &file_ktms_v1_admin_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1113,7 +1296,7 @@ func (x *RevokeAPIKeyResponse) String() string { func (*RevokeAPIKeyResponse) ProtoMessage() {} func (x *RevokeAPIKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[18] + mi := &file_ktms_v1_admin_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1126,7 +1309,7 @@ func (x *RevokeAPIKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeAPIKeyResponse.ProtoReflect.Descriptor instead. func (*RevokeAPIKeyResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{18} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{21} } type ListAPIKeysRequest struct { @@ -1137,7 +1320,7 @@ type ListAPIKeysRequest struct { func (x *ListAPIKeysRequest) Reset() { *x = ListAPIKeysRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[19] + mi := &file_ktms_v1_admin_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1149,7 +1332,7 @@ func (x *ListAPIKeysRequest) String() string { func (*ListAPIKeysRequest) ProtoMessage() {} func (x *ListAPIKeysRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[19] + mi := &file_ktms_v1_admin_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1162,7 +1345,7 @@ func (x *ListAPIKeysRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAPIKeysRequest.ProtoReflect.Descriptor instead. func (*ListAPIKeysRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{19} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{22} } type APIKeyInfo struct { @@ -1173,13 +1356,14 @@ type APIKeyInfo struct { Disabled bool `protobuf:"varint,4,opt,name=disabled,proto3" json:"disabled,omitempty"` OrgId string `protobuf:"bytes,5,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` TeamId string `protobuf:"bytes,6,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + Policy *APIKeyPolicy `protobuf:"bytes,7,opt,name=policy,proto3" json:"policy,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *APIKeyInfo) Reset() { *x = APIKeyInfo{} - mi := &file_ktms_v1_admin_proto_msgTypes[20] + mi := &file_ktms_v1_admin_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1191,7 +1375,7 @@ func (x *APIKeyInfo) String() string { func (*APIKeyInfo) ProtoMessage() {} func (x *APIKeyInfo) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[20] + mi := &file_ktms_v1_admin_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1204,7 +1388,7 @@ func (x *APIKeyInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use APIKeyInfo.ProtoReflect.Descriptor instead. func (*APIKeyInfo) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{20} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{23} } func (x *APIKeyInfo) GetName() string { @@ -1249,6 +1433,13 @@ func (x *APIKeyInfo) GetTeamId() string { return "" } +func (x *APIKeyInfo) GetPolicy() *APIKeyPolicy { + if x != nil { + return x.Policy + } + return nil +} + type ListAPIKeysResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Keys []*APIKeyInfo `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` @@ -1258,7 +1449,7 @@ type ListAPIKeysResponse struct { func (x *ListAPIKeysResponse) Reset() { *x = ListAPIKeysResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[21] + mi := &file_ktms_v1_admin_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1270,7 +1461,7 @@ func (x *ListAPIKeysResponse) String() string { func (*ListAPIKeysResponse) ProtoMessage() {} func (x *ListAPIKeysResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[21] + mi := &file_ktms_v1_admin_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1283,7 +1474,7 @@ func (x *ListAPIKeysResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAPIKeysResponse.ProtoReflect.Descriptor instead. func (*ListAPIKeysResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{21} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{24} } func (x *ListAPIKeysResponse) GetKeys() []*APIKeyInfo { @@ -1301,7 +1492,7 @@ type RotateEncryptionRequest struct { func (x *RotateEncryptionRequest) Reset() { *x = RotateEncryptionRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[22] + mi := &file_ktms_v1_admin_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1313,7 +1504,7 @@ func (x *RotateEncryptionRequest) String() string { func (*RotateEncryptionRequest) ProtoMessage() {} func (x *RotateEncryptionRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[22] + mi := &file_ktms_v1_admin_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1326,7 +1517,7 @@ func (x *RotateEncryptionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateEncryptionRequest.ProtoReflect.Descriptor instead. func (*RotateEncryptionRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{22} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{25} } // RotateEncryption re-encrypts every vendor key and registered node auth token @@ -1343,7 +1534,7 @@ type RotateEncryptionResponse struct { func (x *RotateEncryptionResponse) Reset() { *x = RotateEncryptionResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[23] + mi := &file_ktms_v1_admin_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1355,7 +1546,7 @@ func (x *RotateEncryptionResponse) String() string { func (*RotateEncryptionResponse) ProtoMessage() {} func (x *RotateEncryptionResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[23] + mi := &file_ktms_v1_admin_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1368,7 +1559,7 @@ func (x *RotateEncryptionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateEncryptionResponse.ProtoReflect.Descriptor instead. func (*RotateEncryptionResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{23} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{26} } func (x *RotateEncryptionResponse) GetReencrypted() int32 { @@ -1392,7 +1583,7 @@ type MintNodeKeyRequest struct { func (x *MintNodeKeyRequest) Reset() { *x = MintNodeKeyRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[24] + mi := &file_ktms_v1_admin_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1404,7 +1595,7 @@ func (x *MintNodeKeyRequest) String() string { func (*MintNodeKeyRequest) ProtoMessage() {} func (x *MintNodeKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[24] + mi := &file_ktms_v1_admin_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1417,7 +1608,7 @@ func (x *MintNodeKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintNodeKeyRequest.ProtoReflect.Descriptor instead. func (*MintNodeKeyRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{24} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{27} } func (x *MintNodeKeyRequest) GetName() string { @@ -1472,7 +1663,7 @@ type MintNodeKeyResponse struct { func (x *MintNodeKeyResponse) Reset() { *x = MintNodeKeyResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[25] + mi := &file_ktms_v1_admin_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1484,7 +1675,7 @@ func (x *MintNodeKeyResponse) String() string { func (*MintNodeKeyResponse) ProtoMessage() {} func (x *MintNodeKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[25] + mi := &file_ktms_v1_admin_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1497,7 +1688,7 @@ func (x *MintNodeKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintNodeKeyResponse.ProtoReflect.Descriptor instead. func (*MintNodeKeyResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{25} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{28} } func (x *MintNodeKeyResponse) GetId() int64 { @@ -1523,7 +1714,7 @@ type RevokeNodeKeyRequest struct { func (x *RevokeNodeKeyRequest) Reset() { *x = RevokeNodeKeyRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[26] + mi := &file_ktms_v1_admin_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1535,7 +1726,7 @@ func (x *RevokeNodeKeyRequest) String() string { func (*RevokeNodeKeyRequest) ProtoMessage() {} func (x *RevokeNodeKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[26] + mi := &file_ktms_v1_admin_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1548,7 +1739,7 @@ func (x *RevokeNodeKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeNodeKeyRequest.ProtoReflect.Descriptor instead. func (*RevokeNodeKeyRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{26} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{29} } func (x *RevokeNodeKeyRequest) GetId() int64 { @@ -1566,7 +1757,7 @@ type RevokeNodeKeyResponse struct { func (x *RevokeNodeKeyResponse) Reset() { *x = RevokeNodeKeyResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[27] + mi := &file_ktms_v1_admin_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1578,7 +1769,7 @@ func (x *RevokeNodeKeyResponse) String() string { func (*RevokeNodeKeyResponse) ProtoMessage() {} func (x *RevokeNodeKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[27] + mi := &file_ktms_v1_admin_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1591,7 +1782,7 @@ func (x *RevokeNodeKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeNodeKeyResponse.ProtoReflect.Descriptor instead. func (*RevokeNodeKeyResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{27} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{30} } type ListNodeKeysRequest struct { @@ -1602,7 +1793,7 @@ type ListNodeKeysRequest struct { func (x *ListNodeKeysRequest) Reset() { *x = ListNodeKeysRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[28] + mi := &file_ktms_v1_admin_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1614,7 +1805,7 @@ func (x *ListNodeKeysRequest) String() string { func (*ListNodeKeysRequest) ProtoMessage() {} func (x *ListNodeKeysRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[28] + mi := &file_ktms_v1_admin_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1627,7 +1818,7 @@ func (x *ListNodeKeysRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNodeKeysRequest.ProtoReflect.Descriptor instead. func (*ListNodeKeysRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{28} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{31} } type NodeKeyInfo struct { @@ -1647,7 +1838,7 @@ type NodeKeyInfo struct { func (x *NodeKeyInfo) Reset() { *x = NodeKeyInfo{} - mi := &file_ktms_v1_admin_proto_msgTypes[29] + mi := &file_ktms_v1_admin_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1659,7 +1850,7 @@ func (x *NodeKeyInfo) String() string { func (*NodeKeyInfo) ProtoMessage() {} func (x *NodeKeyInfo) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[29] + mi := &file_ktms_v1_admin_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1672,7 +1863,7 @@ func (x *NodeKeyInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use NodeKeyInfo.ProtoReflect.Descriptor instead. func (*NodeKeyInfo) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{29} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{32} } func (x *NodeKeyInfo) GetId() int64 { @@ -1747,7 +1938,7 @@ type ListNodeKeysResponse struct { func (x *ListNodeKeysResponse) Reset() { *x = ListNodeKeysResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[30] + mi := &file_ktms_v1_admin_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1759,7 +1950,7 @@ func (x *ListNodeKeysResponse) String() string { func (*ListNodeKeysResponse) ProtoMessage() {} func (x *ListNodeKeysResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[30] + mi := &file_ktms_v1_admin_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1772,7 +1963,7 @@ func (x *ListNodeKeysResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNodeKeysResponse.ProtoReflect.Descriptor instead. func (*ListNodeKeysResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{30} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{33} } func (x *ListNodeKeysResponse) GetKeys() []*NodeKeyInfo { @@ -1790,7 +1981,7 @@ type ListNodesRequest struct { func (x *ListNodesRequest) Reset() { *x = ListNodesRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[31] + mi := &file_ktms_v1_admin_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1802,7 +1993,7 @@ func (x *ListNodesRequest) String() string { func (*ListNodesRequest) ProtoMessage() {} func (x *ListNodesRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[31] + mi := &file_ktms_v1_admin_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1815,7 +2006,7 @@ func (x *ListNodesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNodesRequest.ProtoReflect.Descriptor instead. func (*ListNodesRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{31} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{34} } type NodeInfo struct { @@ -1844,7 +2035,7 @@ type NodeInfo struct { func (x *NodeInfo) Reset() { *x = NodeInfo{} - mi := &file_ktms_v1_admin_proto_msgTypes[32] + mi := &file_ktms_v1_admin_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1856,7 +2047,7 @@ func (x *NodeInfo) String() string { func (*NodeInfo) ProtoMessage() {} func (x *NodeInfo) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[32] + mi := &file_ktms_v1_admin_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1869,7 +2060,7 @@ func (x *NodeInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use NodeInfo.ProtoReflect.Descriptor instead. func (*NodeInfo) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{32} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{35} } func (x *NodeInfo) GetNodeId() string { @@ -2007,7 +2198,7 @@ type ListNodesResponse struct { func (x *ListNodesResponse) Reset() { *x = ListNodesResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[33] + mi := &file_ktms_v1_admin_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2019,7 +2210,7 @@ func (x *ListNodesResponse) String() string { func (*ListNodesResponse) ProtoMessage() {} func (x *ListNodesResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[33] + mi := &file_ktms_v1_admin_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2032,7 +2223,7 @@ func (x *ListNodesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNodesResponse.ProtoReflect.Descriptor instead. func (*ListNodesResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{33} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{36} } func (x *ListNodesResponse) GetNodes() []*NodeInfo { @@ -2051,7 +2242,7 @@ type DrainNodeRequest struct { func (x *DrainNodeRequest) Reset() { *x = DrainNodeRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[34] + mi := &file_ktms_v1_admin_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2063,7 +2254,7 @@ func (x *DrainNodeRequest) String() string { func (*DrainNodeRequest) ProtoMessage() {} func (x *DrainNodeRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[34] + mi := &file_ktms_v1_admin_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2076,7 +2267,7 @@ func (x *DrainNodeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DrainNodeRequest.ProtoReflect.Descriptor instead. func (*DrainNodeRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{34} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{37} } func (x *DrainNodeRequest) GetNodeId() string { @@ -2094,7 +2285,7 @@ type DrainNodeResponse struct { func (x *DrainNodeResponse) Reset() { *x = DrainNodeResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[35] + mi := &file_ktms_v1_admin_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2106,7 +2297,7 @@ func (x *DrainNodeResponse) String() string { func (*DrainNodeResponse) ProtoMessage() {} func (x *DrainNodeResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[35] + mi := &file_ktms_v1_admin_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2119,7 +2310,7 @@ func (x *DrainNodeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DrainNodeResponse.ProtoReflect.Descriptor instead. func (*DrainNodeResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{35} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{38} } type UserRateLimitSpec struct { @@ -2136,7 +2327,7 @@ type UserRateLimitSpec struct { func (x *UserRateLimitSpec) Reset() { *x = UserRateLimitSpec{} - mi := &file_ktms_v1_admin_proto_msgTypes[36] + mi := &file_ktms_v1_admin_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2148,7 +2339,7 @@ func (x *UserRateLimitSpec) String() string { func (*UserRateLimitSpec) ProtoMessage() {} func (x *UserRateLimitSpec) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[36] + mi := &file_ktms_v1_admin_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2161,7 +2352,7 @@ func (x *UserRateLimitSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use UserRateLimitSpec.ProtoReflect.Descriptor instead. func (*UserRateLimitSpec) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{36} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{39} } func (x *UserRateLimitSpec) GetUserId() string { @@ -2215,7 +2406,7 @@ type SetUserRateLimitRequest struct { func (x *SetUserRateLimitRequest) Reset() { *x = SetUserRateLimitRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[37] + mi := &file_ktms_v1_admin_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2227,7 +2418,7 @@ func (x *SetUserRateLimitRequest) String() string { func (*SetUserRateLimitRequest) ProtoMessage() {} func (x *SetUserRateLimitRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[37] + mi := &file_ktms_v1_admin_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2240,7 +2431,7 @@ func (x *SetUserRateLimitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetUserRateLimitRequest.ProtoReflect.Descriptor instead. func (*SetUserRateLimitRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{37} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{40} } func (x *SetUserRateLimitRequest) GetRateLimit() *UserRateLimitSpec { @@ -2258,7 +2449,7 @@ type SetUserRateLimitResponse struct { func (x *SetUserRateLimitResponse) Reset() { *x = SetUserRateLimitResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[38] + mi := &file_ktms_v1_admin_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2270,7 +2461,7 @@ func (x *SetUserRateLimitResponse) String() string { func (*SetUserRateLimitResponse) ProtoMessage() {} func (x *SetUserRateLimitResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[38] + mi := &file_ktms_v1_admin_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2283,7 +2474,7 @@ func (x *SetUserRateLimitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetUserRateLimitResponse.ProtoReflect.Descriptor instead. func (*SetUserRateLimitResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{38} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{41} } type DeleteUserRateLimitRequest struct { @@ -2296,7 +2487,7 @@ type DeleteUserRateLimitRequest struct { func (x *DeleteUserRateLimitRequest) Reset() { *x = DeleteUserRateLimitRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[39] + mi := &file_ktms_v1_admin_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2308,7 +2499,7 @@ func (x *DeleteUserRateLimitRequest) String() string { func (*DeleteUserRateLimitRequest) ProtoMessage() {} func (x *DeleteUserRateLimitRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[39] + mi := &file_ktms_v1_admin_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2321,7 +2512,7 @@ func (x *DeleteUserRateLimitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteUserRateLimitRequest.ProtoReflect.Descriptor instead. func (*DeleteUserRateLimitRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{39} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{42} } func (x *DeleteUserRateLimitRequest) GetUserId() string { @@ -2346,7 +2537,7 @@ type DeleteUserRateLimitResponse struct { func (x *DeleteUserRateLimitResponse) Reset() { *x = DeleteUserRateLimitResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[40] + mi := &file_ktms_v1_admin_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2358,7 +2549,7 @@ func (x *DeleteUserRateLimitResponse) String() string { func (*DeleteUserRateLimitResponse) ProtoMessage() {} func (x *DeleteUserRateLimitResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[40] + mi := &file_ktms_v1_admin_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2371,7 +2562,7 @@ func (x *DeleteUserRateLimitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteUserRateLimitResponse.ProtoReflect.Descriptor instead. func (*DeleteUserRateLimitResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{40} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{43} } type ListUserRateLimitsRequest struct { @@ -2383,7 +2574,7 @@ type ListUserRateLimitsRequest struct { func (x *ListUserRateLimitsRequest) Reset() { *x = ListUserRateLimitsRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[41] + mi := &file_ktms_v1_admin_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2395,7 +2586,7 @@ func (x *ListUserRateLimitsRequest) String() string { func (*ListUserRateLimitsRequest) ProtoMessage() {} func (x *ListUserRateLimitsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[41] + mi := &file_ktms_v1_admin_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2408,7 +2599,7 @@ func (x *ListUserRateLimitsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUserRateLimitsRequest.ProtoReflect.Descriptor instead. func (*ListUserRateLimitsRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{41} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{44} } func (x *ListUserRateLimitsRequest) GetUserId() string { @@ -2427,7 +2618,7 @@ type ListUserRateLimitsResponse struct { func (x *ListUserRateLimitsResponse) Reset() { *x = ListUserRateLimitsResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[42] + mi := &file_ktms_v1_admin_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2439,7 +2630,7 @@ func (x *ListUserRateLimitsResponse) String() string { func (*ListUserRateLimitsResponse) ProtoMessage() {} func (x *ListUserRateLimitsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[42] + mi := &file_ktms_v1_admin_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2452,7 +2643,7 @@ func (x *ListUserRateLimitsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUserRateLimitsResponse.ProtoReflect.Descriptor instead. func (*ListUserRateLimitsResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{42} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{45} } func (x *ListUserRateLimitsResponse) GetRateLimits() []*UserRateLimitSpec { @@ -2480,7 +2671,7 @@ type SpendLimitSpec struct { func (x *SpendLimitSpec) Reset() { *x = SpendLimitSpec{} - mi := &file_ktms_v1_admin_proto_msgTypes[43] + mi := &file_ktms_v1_admin_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2492,7 +2683,7 @@ func (x *SpendLimitSpec) String() string { func (*SpendLimitSpec) ProtoMessage() {} func (x *SpendLimitSpec) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[43] + mi := &file_ktms_v1_admin_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2505,7 +2696,7 @@ func (x *SpendLimitSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use SpendLimitSpec.ProtoReflect.Descriptor instead. func (*SpendLimitSpec) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{43} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{46} } func (x *SpendLimitSpec) GetSubjectType() string { @@ -2545,7 +2736,7 @@ type SetSpendLimitRequest struct { func (x *SetSpendLimitRequest) Reset() { *x = SetSpendLimitRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[44] + mi := &file_ktms_v1_admin_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2557,7 +2748,7 @@ func (x *SetSpendLimitRequest) String() string { func (*SetSpendLimitRequest) ProtoMessage() {} func (x *SetSpendLimitRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[44] + mi := &file_ktms_v1_admin_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2570,7 +2761,7 @@ func (x *SetSpendLimitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetSpendLimitRequest.ProtoReflect.Descriptor instead. func (*SetSpendLimitRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{44} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{47} } func (x *SetSpendLimitRequest) GetSpendLimit() *SpendLimitSpec { @@ -2588,7 +2779,7 @@ type SetSpendLimitResponse struct { func (x *SetSpendLimitResponse) Reset() { *x = SetSpendLimitResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[45] + mi := &file_ktms_v1_admin_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2600,7 +2791,7 @@ func (x *SetSpendLimitResponse) String() string { func (*SetSpendLimitResponse) ProtoMessage() {} func (x *SetSpendLimitResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[45] + mi := &file_ktms_v1_admin_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2613,7 +2804,7 @@ func (x *SetSpendLimitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetSpendLimitResponse.ProtoReflect.Descriptor instead. func (*SetSpendLimitResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{45} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{48} } type DeleteSpendLimitRequest struct { @@ -2627,7 +2818,7 @@ type DeleteSpendLimitRequest struct { func (x *DeleteSpendLimitRequest) Reset() { *x = DeleteSpendLimitRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[46] + mi := &file_ktms_v1_admin_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2639,7 +2830,7 @@ func (x *DeleteSpendLimitRequest) String() string { func (*DeleteSpendLimitRequest) ProtoMessage() {} func (x *DeleteSpendLimitRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[46] + mi := &file_ktms_v1_admin_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2652,7 +2843,7 @@ func (x *DeleteSpendLimitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSpendLimitRequest.ProtoReflect.Descriptor instead. func (*DeleteSpendLimitRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{46} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{49} } func (x *DeleteSpendLimitRequest) GetSubjectType() string { @@ -2684,7 +2875,7 @@ type DeleteSpendLimitResponse struct { func (x *DeleteSpendLimitResponse) Reset() { *x = DeleteSpendLimitResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[47] + mi := &file_ktms_v1_admin_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2696,7 +2887,7 @@ func (x *DeleteSpendLimitResponse) String() string { func (*DeleteSpendLimitResponse) ProtoMessage() {} func (x *DeleteSpendLimitResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[47] + mi := &file_ktms_v1_admin_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2709,7 +2900,7 @@ func (x *DeleteSpendLimitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSpendLimitResponse.ProtoReflect.Descriptor instead. func (*DeleteSpendLimitResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{47} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{50} } type ListSpendLimitsRequest struct { @@ -2722,7 +2913,7 @@ type ListSpendLimitsRequest struct { func (x *ListSpendLimitsRequest) Reset() { *x = ListSpendLimitsRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[48] + mi := &file_ktms_v1_admin_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2734,7 +2925,7 @@ func (x *ListSpendLimitsRequest) String() string { func (*ListSpendLimitsRequest) ProtoMessage() {} func (x *ListSpendLimitsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[48] + mi := &file_ktms_v1_admin_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2747,7 +2938,7 @@ func (x *ListSpendLimitsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSpendLimitsRequest.ProtoReflect.Descriptor instead. func (*ListSpendLimitsRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{48} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{51} } func (x *ListSpendLimitsRequest) GetSubjectType() string { @@ -2773,7 +2964,7 @@ type ListSpendLimitsResponse struct { func (x *ListSpendLimitsResponse) Reset() { *x = ListSpendLimitsResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[49] + mi := &file_ktms_v1_admin_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2785,7 +2976,7 @@ func (x *ListSpendLimitsResponse) String() string { func (*ListSpendLimitsResponse) ProtoMessage() {} func (x *ListSpendLimitsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[49] + mi := &file_ktms_v1_admin_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2798,7 +2989,7 @@ func (x *ListSpendLimitsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSpendLimitsResponse.ProtoReflect.Descriptor instead. func (*ListSpendLimitsResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{49} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{52} } func (x *ListSpendLimitsResponse) GetSpendLimits() []*SpendLimitSpec { @@ -2819,7 +3010,7 @@ type Organization struct { func (x *Organization) Reset() { *x = Organization{} - mi := &file_ktms_v1_admin_proto_msgTypes[50] + mi := &file_ktms_v1_admin_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2831,7 +3022,7 @@ func (x *Organization) String() string { func (*Organization) ProtoMessage() {} func (x *Organization) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[50] + mi := &file_ktms_v1_admin_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2844,7 +3035,7 @@ func (x *Organization) ProtoReflect() protoreflect.Message { // Deprecated: Use Organization.ProtoReflect.Descriptor instead. func (*Organization) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{50} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{53} } func (x *Organization) GetId() string { @@ -2885,7 +3076,7 @@ type Team struct { func (x *Team) Reset() { *x = Team{} - mi := &file_ktms_v1_admin_proto_msgTypes[51] + mi := &file_ktms_v1_admin_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2897,7 +3088,7 @@ func (x *Team) String() string { func (*Team) ProtoMessage() {} func (x *Team) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[51] + mi := &file_ktms_v1_admin_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2910,7 +3101,7 @@ func (x *Team) ProtoReflect() protoreflect.Message { // Deprecated: Use Team.ProtoReflect.Descriptor instead. func (*Team) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{51} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{54} } func (x *Team) GetId() string { @@ -2964,7 +3155,7 @@ type CreateOrganizationRequest struct { func (x *CreateOrganizationRequest) Reset() { *x = CreateOrganizationRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[52] + mi := &file_ktms_v1_admin_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2976,7 +3167,7 @@ func (x *CreateOrganizationRequest) String() string { func (*CreateOrganizationRequest) ProtoMessage() {} func (x *CreateOrganizationRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[52] + mi := &file_ktms_v1_admin_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2989,7 +3180,7 @@ func (x *CreateOrganizationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateOrganizationRequest.ProtoReflect.Descriptor instead. func (*CreateOrganizationRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{52} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{55} } func (x *CreateOrganizationRequest) GetName() string { @@ -3008,7 +3199,7 @@ type CreateOrganizationResponse struct { func (x *CreateOrganizationResponse) Reset() { *x = CreateOrganizationResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[53] + mi := &file_ktms_v1_admin_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3020,7 +3211,7 @@ func (x *CreateOrganizationResponse) String() string { func (*CreateOrganizationResponse) ProtoMessage() {} func (x *CreateOrganizationResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[53] + mi := &file_ktms_v1_admin_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3033,7 +3224,7 @@ func (x *CreateOrganizationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateOrganizationResponse.ProtoReflect.Descriptor instead. func (*CreateOrganizationResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{53} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{56} } func (x *CreateOrganizationResponse) GetOrganization() *Organization { @@ -3051,7 +3242,7 @@ type ListOrganizationsRequest struct { func (x *ListOrganizationsRequest) Reset() { *x = ListOrganizationsRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[54] + mi := &file_ktms_v1_admin_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3063,7 +3254,7 @@ func (x *ListOrganizationsRequest) String() string { func (*ListOrganizationsRequest) ProtoMessage() {} func (x *ListOrganizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[54] + mi := &file_ktms_v1_admin_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3076,7 +3267,7 @@ func (x *ListOrganizationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListOrganizationsRequest.ProtoReflect.Descriptor instead. func (*ListOrganizationsRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{54} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{57} } type ListOrganizationsResponse struct { @@ -3088,7 +3279,7 @@ type ListOrganizationsResponse struct { func (x *ListOrganizationsResponse) Reset() { *x = ListOrganizationsResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[55] + mi := &file_ktms_v1_admin_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3100,7 +3291,7 @@ func (x *ListOrganizationsResponse) String() string { func (*ListOrganizationsResponse) ProtoMessage() {} func (x *ListOrganizationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[55] + mi := &file_ktms_v1_admin_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3113,7 +3304,7 @@ func (x *ListOrganizationsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListOrganizationsResponse.ProtoReflect.Descriptor instead. func (*ListOrganizationsResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{55} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{58} } func (x *ListOrganizationsResponse) GetOrganizations() []*Organization { @@ -3132,7 +3323,7 @@ type DeleteOrganizationRequest struct { func (x *DeleteOrganizationRequest) Reset() { *x = DeleteOrganizationRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[56] + mi := &file_ktms_v1_admin_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3144,7 +3335,7 @@ func (x *DeleteOrganizationRequest) String() string { func (*DeleteOrganizationRequest) ProtoMessage() {} func (x *DeleteOrganizationRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[56] + mi := &file_ktms_v1_admin_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3157,7 +3348,7 @@ func (x *DeleteOrganizationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteOrganizationRequest.ProtoReflect.Descriptor instead. func (*DeleteOrganizationRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{56} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{59} } func (x *DeleteOrganizationRequest) GetId() string { @@ -3175,7 +3366,7 @@ type DeleteOrganizationResponse struct { func (x *DeleteOrganizationResponse) Reset() { *x = DeleteOrganizationResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[57] + mi := &file_ktms_v1_admin_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3187,7 +3378,7 @@ func (x *DeleteOrganizationResponse) String() string { func (*DeleteOrganizationResponse) ProtoMessage() {} func (x *DeleteOrganizationResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[57] + mi := &file_ktms_v1_admin_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3200,7 +3391,7 @@ func (x *DeleteOrganizationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteOrganizationResponse.ProtoReflect.Descriptor instead. func (*DeleteOrganizationResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{57} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{60} } type CreateTeamRequest struct { @@ -3215,7 +3406,7 @@ type CreateTeamRequest struct { func (x *CreateTeamRequest) Reset() { *x = CreateTeamRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[58] + mi := &file_ktms_v1_admin_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3227,7 +3418,7 @@ func (x *CreateTeamRequest) String() string { func (*CreateTeamRequest) ProtoMessage() {} func (x *CreateTeamRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[58] + mi := &file_ktms_v1_admin_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3240,7 +3431,7 @@ func (x *CreateTeamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateTeamRequest.ProtoReflect.Descriptor instead. func (*CreateTeamRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{58} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{61} } func (x *CreateTeamRequest) GetOrgId() string { @@ -3280,7 +3471,7 @@ type CreateTeamResponse struct { func (x *CreateTeamResponse) Reset() { *x = CreateTeamResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[59] + mi := &file_ktms_v1_admin_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3292,7 +3483,7 @@ func (x *CreateTeamResponse) String() string { func (*CreateTeamResponse) ProtoMessage() {} func (x *CreateTeamResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[59] + mi := &file_ktms_v1_admin_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3305,7 +3496,7 @@ func (x *CreateTeamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateTeamResponse.ProtoReflect.Descriptor instead. func (*CreateTeamResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{59} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{62} } func (x *CreateTeamResponse) GetTeam() *Team { @@ -3324,7 +3515,7 @@ type GetTeamRequest struct { func (x *GetTeamRequest) Reset() { *x = GetTeamRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[60] + mi := &file_ktms_v1_admin_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3336,7 +3527,7 @@ func (x *GetTeamRequest) String() string { func (*GetTeamRequest) ProtoMessage() {} func (x *GetTeamRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[60] + mi := &file_ktms_v1_admin_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3349,7 +3540,7 @@ func (x *GetTeamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTeamRequest.ProtoReflect.Descriptor instead. func (*GetTeamRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{60} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{63} } func (x *GetTeamRequest) GetId() string { @@ -3368,7 +3559,7 @@ type GetTeamResponse struct { func (x *GetTeamResponse) Reset() { *x = GetTeamResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[61] + mi := &file_ktms_v1_admin_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3380,7 +3571,7 @@ func (x *GetTeamResponse) String() string { func (*GetTeamResponse) ProtoMessage() {} func (x *GetTeamResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[61] + mi := &file_ktms_v1_admin_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3393,7 +3584,7 @@ func (x *GetTeamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTeamResponse.ProtoReflect.Descriptor instead. func (*GetTeamResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{61} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{64} } func (x *GetTeamResponse) GetTeam() *Team { @@ -3412,7 +3603,7 @@ type ListTeamsRequest struct { func (x *ListTeamsRequest) Reset() { *x = ListTeamsRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[62] + mi := &file_ktms_v1_admin_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3424,7 +3615,7 @@ func (x *ListTeamsRequest) String() string { func (*ListTeamsRequest) ProtoMessage() {} func (x *ListTeamsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[62] + mi := &file_ktms_v1_admin_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3437,7 +3628,7 @@ func (x *ListTeamsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTeamsRequest.ProtoReflect.Descriptor instead. func (*ListTeamsRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{62} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{65} } func (x *ListTeamsRequest) GetOrgId() string { @@ -3456,7 +3647,7 @@ type ListTeamsResponse struct { func (x *ListTeamsResponse) Reset() { *x = ListTeamsResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[63] + mi := &file_ktms_v1_admin_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3468,7 +3659,7 @@ func (x *ListTeamsResponse) String() string { func (*ListTeamsResponse) ProtoMessage() {} func (x *ListTeamsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[63] + mi := &file_ktms_v1_admin_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3481,7 +3672,7 @@ func (x *ListTeamsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTeamsResponse.ProtoReflect.Descriptor instead. func (*ListTeamsResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{63} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{66} } func (x *ListTeamsResponse) GetTeams() []*Team { @@ -3505,7 +3696,7 @@ type UpdateTeamRequest struct { func (x *UpdateTeamRequest) Reset() { *x = UpdateTeamRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[64] + mi := &file_ktms_v1_admin_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3517,7 +3708,7 @@ func (x *UpdateTeamRequest) String() string { func (*UpdateTeamRequest) ProtoMessage() {} func (x *UpdateTeamRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[64] + mi := &file_ktms_v1_admin_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3530,7 +3721,7 @@ func (x *UpdateTeamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateTeamRequest.ProtoReflect.Descriptor instead. func (*UpdateTeamRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{64} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{67} } func (x *UpdateTeamRequest) GetId() string { @@ -3570,7 +3761,7 @@ type UpdateTeamResponse struct { func (x *UpdateTeamResponse) Reset() { *x = UpdateTeamResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[65] + mi := &file_ktms_v1_admin_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3582,7 +3773,7 @@ func (x *UpdateTeamResponse) String() string { func (*UpdateTeamResponse) ProtoMessage() {} func (x *UpdateTeamResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[65] + mi := &file_ktms_v1_admin_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3595,7 +3786,7 @@ func (x *UpdateTeamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateTeamResponse.ProtoReflect.Descriptor instead. func (*UpdateTeamResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{65} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{68} } func (x *UpdateTeamResponse) GetTeam() *Team { @@ -3614,7 +3805,7 @@ type DeleteTeamRequest struct { func (x *DeleteTeamRequest) Reset() { *x = DeleteTeamRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[66] + mi := &file_ktms_v1_admin_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3626,7 +3817,7 @@ func (x *DeleteTeamRequest) String() string { func (*DeleteTeamRequest) ProtoMessage() {} func (x *DeleteTeamRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[66] + mi := &file_ktms_v1_admin_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3639,7 +3830,7 @@ func (x *DeleteTeamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteTeamRequest.ProtoReflect.Descriptor instead. func (*DeleteTeamRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{66} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{69} } func (x *DeleteTeamRequest) GetId() string { @@ -3657,7 +3848,7 @@ type DeleteTeamResponse struct { func (x *DeleteTeamResponse) Reset() { *x = DeleteTeamResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[67] + mi := &file_ktms_v1_admin_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3669,7 +3860,7 @@ func (x *DeleteTeamResponse) String() string { func (*DeleteTeamResponse) ProtoMessage() {} func (x *DeleteTeamResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[67] + mi := &file_ktms_v1_admin_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3682,7 +3873,7 @@ func (x *DeleteTeamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteTeamResponse.ProtoReflect.Descriptor instead. func (*DeleteTeamResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{67} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{70} } type GetTeamSpendRequest struct { @@ -3694,7 +3885,7 @@ type GetTeamSpendRequest struct { func (x *GetTeamSpendRequest) Reset() { *x = GetTeamSpendRequest{} - mi := &file_ktms_v1_admin_proto_msgTypes[68] + mi := &file_ktms_v1_admin_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3706,7 +3897,7 @@ func (x *GetTeamSpendRequest) String() string { func (*GetTeamSpendRequest) ProtoMessage() {} func (x *GetTeamSpendRequest) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[68] + mi := &file_ktms_v1_admin_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3719,7 +3910,7 @@ func (x *GetTeamSpendRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTeamSpendRequest.ProtoReflect.Descriptor instead. func (*GetTeamSpendRequest) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{68} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{71} } func (x *GetTeamSpendRequest) GetId() string { @@ -3744,7 +3935,7 @@ type GetTeamSpendResponse struct { func (x *GetTeamSpendResponse) Reset() { *x = GetTeamSpendResponse{} - mi := &file_ktms_v1_admin_proto_msgTypes[69] + mi := &file_ktms_v1_admin_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3756,7 +3947,7 @@ func (x *GetTeamSpendResponse) String() string { func (*GetTeamSpendResponse) ProtoMessage() {} func (x *GetTeamSpendResponse) ProtoReflect() protoreflect.Message { - mi := &file_ktms_v1_admin_proto_msgTypes[69] + mi := &file_ktms_v1_admin_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3769,7 +3960,7 @@ func (x *GetTeamSpendResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTeamSpendResponse.ProtoReflect.Descriptor instead. func (*GetTeamSpendResponse) Descriptor() ([]byte, []int) { - return file_ktms_v1_admin_proto_rawDescGZIP(), []int{69} + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{72} } func (x *GetTeamSpendResponse) GetSpentNanousd() int64 { @@ -3800,11 +3991,193 @@ func (x *GetTeamSpendResponse) GetMetered() bool { return false } +// GetSpend reports a key's, end user's or team's spend as the gateway enforces +// it: one window per spend limit period that applies to the subject. +type GetSpendRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SubjectType string `protobuf:"bytes,1,opt,name=subject_type,json=subjectType,proto3" json:"subject_type,omitempty"` // "key" (subject_id = key name) | "user" | "team" + SubjectId string `protobuf:"bytes,2,opt,name=subject_id,json=subjectId,proto3" json:"subject_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSpendRequest) Reset() { + *x = GetSpendRequest{} + mi := &file_ktms_v1_admin_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSpendRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSpendRequest) ProtoMessage() {} + +func (x *GetSpendRequest) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[73] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSpendRequest.ProtoReflect.Descriptor instead. +func (*GetSpendRequest) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{73} +} + +func (x *GetSpendRequest) GetSubjectType() string { + if x != nil { + return x.SubjectType + } + return "" +} + +func (x *GetSpendRequest) GetSubjectId() string { + if x != nil { + return x.SubjectId + } + return "" +} + +type SpendWindow struct { + state protoimpl.MessageState `protogen:"open.v1"` + Period string `protobuf:"bytes,1,opt,name=period,proto3" json:"period,omitempty"` // "day" | "month" (UTC) + SpentNanousd int64 `protobuf:"varint,2,opt,name=spent_nanousd,json=spentNanousd,proto3" json:"spent_nanousd,omitempty"` + LimitNanousd int64 `protobuf:"varint,3,opt,name=limit_nanousd,json=limitNanousd,proto3" json:"limit_nanousd,omitempty"` // 0 = no cap + RemainingNanousd int64 `protobuf:"varint,4,opt,name=remaining_nanousd,json=remainingNanousd,proto3" json:"remaining_nanousd,omitempty"` // limit - spent, never below 0; 0 without a cap + ResetsAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=resets_at,json=resetsAt,proto3" json:"resets_at,omitempty"` + Metered bool `protobuf:"varint,6,opt,name=metered,proto3" json:"metered,omitempty"` // false: no cap (spend is counted only under a cap), or no Redis + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SpendWindow) Reset() { + *x = SpendWindow{} + mi := &file_ktms_v1_admin_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SpendWindow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SpendWindow) ProtoMessage() {} + +func (x *SpendWindow) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[74] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SpendWindow.ProtoReflect.Descriptor instead. +func (*SpendWindow) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{74} +} + +func (x *SpendWindow) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *SpendWindow) GetSpentNanousd() int64 { + if x != nil { + return x.SpentNanousd + } + return 0 +} + +func (x *SpendWindow) GetLimitNanousd() int64 { + if x != nil { + return x.LimitNanousd + } + return 0 +} + +func (x *SpendWindow) GetRemainingNanousd() int64 { + if x != nil { + return x.RemainingNanousd + } + return 0 +} + +func (x *SpendWindow) GetResetsAt() *timestamppb.Timestamp { + if x != nil { + return x.ResetsAt + } + return nil +} + +func (x *SpendWindow) GetMetered() bool { + if x != nil { + return x.Metered + } + return false +} + +type GetSpendResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Windows []*SpendWindow `protobuf:"bytes,1,rep,name=windows,proto3" json:"windows,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSpendResponse) Reset() { + *x = GetSpendResponse{} + mi := &file_ktms_v1_admin_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSpendResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSpendResponse) ProtoMessage() {} + +func (x *GetSpendResponse) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[75] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSpendResponse.ProtoReflect.Descriptor instead. +func (*GetSpendResponse) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{75} +} + +func (x *GetSpendResponse) GetWindows() []*SpendWindow { + if x != nil { + return x.Windows + } + return nil +} + var File_ktms_v1_admin_proto protoreflect.FileDescriptor const file_ktms_v1_admin_proto_rawDesc = "" + "\n" + - "\x13ktms/v1/admin.proto\x12\aktms.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x15ktms/v1/gateway.proto\"\xd5\x05\n" + + "\x13ktms/v1/admin.proto\x12\aktms.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x15ktms/v1/gateway.proto\"\xcb\x06\n" + "\tModelSpec\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + "\x06vendor\x18\x02 \x01(\tR\x06vendor\x12\x1a\n" + @@ -3824,7 +4197,9 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "\aenabled\x18\v \x01(\bR\aenabled\x12\x1b\n" + "\trpm_burst\x18\x10 \x01(\x05R\brpmBurst\x12(\n" + "\x10queue_timeout_ms\x18\x11 \x01(\x05R\x0equeueTimeoutMs\x12.\n" + - "\x13budget_grace_margin\x18\x12 \x01(\x01R\x11budgetGraceMargin\"\x9c\x03\n" + + "\x13budget_grace_margin\x18\x12 \x01(\x01R\x11budgetGraceMargin\x128\n" + + "\x19price_cache_read_per_mtok\x18\x13 \x01(\x01R\x15priceCacheReadPerMtok\x12:\n" + + "\x1aprice_cache_write_per_mtok\x18\x14 \x01(\x01R\x16priceCacheWritePerMtok\"\x9c\x03\n" + "\bRuleSpec\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" + "\bpriority\x18\x02 \x01(\x05R\bpriority\x12'\n" + @@ -3879,18 +4254,27 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "vendorKeys\x12\x18\n" + "\aversion\x18\x04 \x01(\x03R\aversion\x12D\n" + "\x10user_rate_limits\x18\x05 \x03(\v2\x1a.ktms.v1.UserRateLimitSpecR\x0euserRateLimits\x12:\n" + - "\fspend_limits\x18\x06 \x03(\v2\x17.ktms.v1.SpendLimitSpecR\vspendLimits\"o\n" + + "\fspend_limits\x18\x06 \x03(\v2\x17.ktms.v1.SpendLimitSpecR\vspendLimits\"\x9e\x01\n" + "\x11MintAPIKeyRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + "\x06scopes\x18\x02 \x03(\tR\x06scopes\x12\x15\n" + "\x06org_id\x18\x03 \x01(\tR\x05orgId\x12\x17\n" + - "\ateam_id\x18\x04 \x01(\tR\x06teamId\"1\n" + + "\ateam_id\x18\x04 \x01(\tR\x06teamId\x12-\n" + + "\x06policy\x18\x05 \x01(\v2\x15.ktms.v1.APIKeyPolicyR\x06policy\"o\n" + + "\fAPIKeyPolicy\x12%\n" + + "\x0eallowed_models\x18\x01 \x03(\tR\rallowedModels\x12\x1b\n" + + "\trpm_limit\x18\x02 \x01(\x05R\brpmLimit\x12\x1b\n" + + "\ttpm_limit\x18\x03 \x01(\x03R\btpmLimit\"[\n" + + "\x16SetAPIKeyPolicyRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12-\n" + + "\x06policy\x18\x02 \x01(\v2\x15.ktms.v1.APIKeyPolicyR\x06policy\"\x19\n" + + "\x17SetAPIKeyPolicyResponse\"1\n" + "\x12MintAPIKeyResponse\x12\x1b\n" + "\tplain_key\x18\x01 \x01(\tR\bplainKey\")\n" + "\x13RevokeAPIKeyRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"\x16\n" + "\x14RevokeAPIKeyResponse\"\x14\n" + - "\x12ListAPIKeysRequest\"\xbf\x01\n" + + "\x12ListAPIKeysRequest\"\xee\x01\n" + "\n" + "APIKeyInfo\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + @@ -3899,7 +4283,8 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x1a\n" + "\bdisabled\x18\x04 \x01(\bR\bdisabled\x12\x15\n" + "\x06org_id\x18\x05 \x01(\tR\x05orgId\x12\x17\n" + - "\ateam_id\x18\x06 \x01(\tR\x06teamId\">\n" + + "\ateam_id\x18\x06 \x01(\tR\x06teamId\x12-\n" + + "\x06policy\x18\a \x01(\v2\x15.ktms.v1.APIKeyPolicyR\x06policy\">\n" + "\x13ListAPIKeysResponse\x12'\n" + "\x04keys\x18\x01 \x03(\v2\x13.ktms.v1.APIKeyInfoR\x04keys\"\x19\n" + "\x17RotateEncryptionRequest\"<\n" + @@ -4058,7 +4443,20 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "\rspent_nanousd\x18\x01 \x01(\x03R\fspentNanousd\x12#\n" + "\rlimit_nanousd\x18\x02 \x01(\x03R\flimitNanousd\x127\n" + "\tresets_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\bresetsAt\x12\x18\n" + - "\ametered\x18\x04 \x01(\bR\ametered2\xd3\x12\n" + + "\ametered\x18\x04 \x01(\bR\ametered\"S\n" + + "\x0fGetSpendRequest\x12!\n" + + "\fsubject_type\x18\x01 \x01(\tR\vsubjectType\x12\x1d\n" + + "\n" + + "subject_id\x18\x02 \x01(\tR\tsubjectId\"\xef\x01\n" + + "\vSpendWindow\x12\x16\n" + + "\x06period\x18\x01 \x01(\tR\x06period\x12#\n" + + "\rspent_nanousd\x18\x02 \x01(\x03R\fspentNanousd\x12#\n" + + "\rlimit_nanousd\x18\x03 \x01(\x03R\flimitNanousd\x12+\n" + + "\x11remaining_nanousd\x18\x04 \x01(\x03R\x10remainingNanousd\x127\n" + + "\tresets_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\bresetsAt\x12\x18\n" + + "\ametered\x18\x06 \x01(\bR\ametered\"B\n" + + "\x10GetSpendResponse\x12.\n" + + "\awindows\x18\x01 \x03(\v2\x14.ktms.v1.SpendWindowR\awindows2\xea\x13\n" + "\fAdminService\x12H\n" + "\vUpsertModel\x12\x1b.ktms.v1.UpsertModelRequest\x1a\x1c.ktms.v1.UpsertModelResponse\x12H\n" + "\vDeleteModel\x12\x1b.ktms.v1.DeleteModelRequest\x1a\x1c.ktms.v1.DeleteModelResponse\x12K\n" + @@ -4069,7 +4467,8 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "\n" + "MintAPIKey\x12\x1a.ktms.v1.MintAPIKeyRequest\x1a\x1b.ktms.v1.MintAPIKeyResponse\x12K\n" + "\fRevokeAPIKey\x12\x1c.ktms.v1.RevokeAPIKeyRequest\x1a\x1d.ktms.v1.RevokeAPIKeyResponse\x12H\n" + - "\vListAPIKeys\x12\x1b.ktms.v1.ListAPIKeysRequest\x1a\x1c.ktms.v1.ListAPIKeysResponse\x12W\n" + + "\vListAPIKeys\x12\x1b.ktms.v1.ListAPIKeysRequest\x1a\x1c.ktms.v1.ListAPIKeysResponse\x12T\n" + + "\x0fSetAPIKeyPolicy\x12\x1f.ktms.v1.SetAPIKeyPolicyRequest\x1a .ktms.v1.SetAPIKeyPolicyResponse\x12W\n" + "\x10RotateEncryption\x12 .ktms.v1.RotateEncryptionRequest\x1a!.ktms.v1.RotateEncryptionResponse\x12H\n" + "\vMintNodeKey\x12\x1b.ktms.v1.MintNodeKeyRequest\x1a\x1c.ktms.v1.MintNodeKeyResponse\x12N\n" + "\rRevokeNodeKey\x12\x1d.ktms.v1.RevokeNodeKeyRequest\x1a\x1e.ktms.v1.RevokeNodeKeyResponse\x12K\n" + @@ -4093,7 +4492,8 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "UpdateTeam\x12\x1a.ktms.v1.UpdateTeamRequest\x1a\x1b.ktms.v1.UpdateTeamResponse\x12E\n" + "\n" + "DeleteTeam\x12\x1a.ktms.v1.DeleteTeamRequest\x1a\x1b.ktms.v1.DeleteTeamResponse\x12K\n" + - "\fGetTeamSpend\x12\x1c.ktms.v1.GetTeamSpendRequest\x1a\x1d.ktms.v1.GetTeamSpendResponseB\x94\x01\n" + + "\fGetTeamSpend\x12\x1c.ktms.v1.GetTeamSpendRequest\x1a\x1d.ktms.v1.GetTeamSpendResponse\x12?\n" + + "\bGetSpend\x12\x18.ktms.v1.GetSpendRequest\x1a\x19.ktms.v1.GetSpendResponseB\x94\x01\n" + "\vcom.ktms.v1B\n" + "AdminProtoP\x01Z ktms.v1.Effort - 71, // 1: ktms.v1.RuleSpec.effort:type_name -> ktms.v1.Effort - 70, // 2: ktms.v1.RuleSpec.attributes:type_name -> ktms.v1.RuleSpec.AttributesEntry + 77, // 0: ktms.v1.ModelSpec.efforts:type_name -> ktms.v1.Effort + 77, // 1: ktms.v1.RuleSpec.effort:type_name -> ktms.v1.Effort + 76, // 2: ktms.v1.RuleSpec.attributes:type_name -> ktms.v1.RuleSpec.AttributesEntry 0, // 3: ktms.v1.UpsertModelRequest.model:type_name -> ktms.v1.ModelSpec 1, // 4: ktms.v1.ReplaceRulesRequest.rules:type_name -> ktms.v1.RuleSpec - 72, // 5: ktms.v1.VendorKeyInfo.created_at:type_name -> google.protobuf.Timestamp + 78, // 5: ktms.v1.VendorKeyInfo.created_at:type_name -> google.protobuf.Timestamp 0, // 6: ktms.v1.GetConfigResponse.models:type_name -> ktms.v1.ModelSpec 1, // 7: ktms.v1.GetConfigResponse.rules:type_name -> ktms.v1.RuleSpec 12, // 8: ktms.v1.GetConfigResponse.vendor_keys:type_name -> ktms.v1.VendorKeyInfo - 36, // 9: ktms.v1.GetConfigResponse.user_rate_limits:type_name -> ktms.v1.UserRateLimitSpec - 43, // 10: ktms.v1.GetConfigResponse.spend_limits:type_name -> ktms.v1.SpendLimitSpec - 72, // 11: ktms.v1.APIKeyInfo.created_at:type_name -> google.protobuf.Timestamp - 20, // 12: ktms.v1.ListAPIKeysResponse.keys:type_name -> ktms.v1.APIKeyInfo - 72, // 13: ktms.v1.NodeKeyInfo.created_at:type_name -> google.protobuf.Timestamp - 29, // 14: ktms.v1.ListNodeKeysResponse.keys:type_name -> ktms.v1.NodeKeyInfo - 72, // 15: ktms.v1.NodeInfo.last_heartbeat:type_name -> google.protobuf.Timestamp - 32, // 16: ktms.v1.ListNodesResponse.nodes:type_name -> ktms.v1.NodeInfo - 36, // 17: ktms.v1.SetUserRateLimitRequest.rate_limit:type_name -> ktms.v1.UserRateLimitSpec - 36, // 18: ktms.v1.ListUserRateLimitsResponse.rate_limits:type_name -> ktms.v1.UserRateLimitSpec - 43, // 19: ktms.v1.SetSpendLimitRequest.spend_limit:type_name -> ktms.v1.SpendLimitSpec - 43, // 20: ktms.v1.ListSpendLimitsResponse.spend_limits:type_name -> ktms.v1.SpendLimitSpec - 72, // 21: ktms.v1.Organization.created_at:type_name -> google.protobuf.Timestamp - 72, // 22: ktms.v1.Team.created_at:type_name -> google.protobuf.Timestamp - 50, // 23: ktms.v1.CreateOrganizationResponse.organization:type_name -> ktms.v1.Organization - 50, // 24: ktms.v1.ListOrganizationsResponse.organizations:type_name -> ktms.v1.Organization - 51, // 25: ktms.v1.CreateTeamResponse.team:type_name -> ktms.v1.Team - 51, // 26: ktms.v1.GetTeamResponse.team:type_name -> ktms.v1.Team - 51, // 27: ktms.v1.ListTeamsResponse.teams:type_name -> ktms.v1.Team - 51, // 28: ktms.v1.UpdateTeamResponse.team:type_name -> ktms.v1.Team - 72, // 29: ktms.v1.GetTeamSpendResponse.resets_at:type_name -> google.protobuf.Timestamp - 2, // 30: ktms.v1.AdminService.UpsertModel:input_type -> ktms.v1.UpsertModelRequest - 4, // 31: ktms.v1.AdminService.DeleteModel:input_type -> ktms.v1.DeleteModelRequest - 6, // 32: ktms.v1.AdminService.ReplaceRules:input_type -> ktms.v1.ReplaceRulesRequest - 8, // 33: ktms.v1.AdminService.SetVendorKey:input_type -> ktms.v1.SetVendorKeyRequest - 10, // 34: ktms.v1.AdminService.DeleteVendorKey:input_type -> ktms.v1.DeleteVendorKeyRequest - 13, // 35: ktms.v1.AdminService.GetConfig:input_type -> ktms.v1.GetConfigRequest - 15, // 36: ktms.v1.AdminService.MintAPIKey:input_type -> ktms.v1.MintAPIKeyRequest - 17, // 37: ktms.v1.AdminService.RevokeAPIKey:input_type -> ktms.v1.RevokeAPIKeyRequest - 19, // 38: ktms.v1.AdminService.ListAPIKeys:input_type -> ktms.v1.ListAPIKeysRequest - 22, // 39: ktms.v1.AdminService.RotateEncryption:input_type -> ktms.v1.RotateEncryptionRequest - 24, // 40: ktms.v1.AdminService.MintNodeKey:input_type -> ktms.v1.MintNodeKeyRequest - 26, // 41: ktms.v1.AdminService.RevokeNodeKey:input_type -> ktms.v1.RevokeNodeKeyRequest - 28, // 42: ktms.v1.AdminService.ListNodeKeys:input_type -> ktms.v1.ListNodeKeysRequest - 31, // 43: ktms.v1.AdminService.ListNodes:input_type -> ktms.v1.ListNodesRequest - 34, // 44: ktms.v1.AdminService.DrainNode:input_type -> ktms.v1.DrainNodeRequest - 37, // 45: ktms.v1.AdminService.SetUserRateLimit:input_type -> ktms.v1.SetUserRateLimitRequest - 39, // 46: ktms.v1.AdminService.DeleteUserRateLimit:input_type -> ktms.v1.DeleteUserRateLimitRequest - 41, // 47: ktms.v1.AdminService.ListUserRateLimits:input_type -> ktms.v1.ListUserRateLimitsRequest - 44, // 48: ktms.v1.AdminService.SetSpendLimit:input_type -> ktms.v1.SetSpendLimitRequest - 46, // 49: ktms.v1.AdminService.DeleteSpendLimit:input_type -> ktms.v1.DeleteSpendLimitRequest - 48, // 50: ktms.v1.AdminService.ListSpendLimits:input_type -> ktms.v1.ListSpendLimitsRequest - 52, // 51: ktms.v1.AdminService.CreateOrganization:input_type -> ktms.v1.CreateOrganizationRequest - 54, // 52: ktms.v1.AdminService.ListOrganizations:input_type -> ktms.v1.ListOrganizationsRequest - 56, // 53: ktms.v1.AdminService.DeleteOrganization:input_type -> ktms.v1.DeleteOrganizationRequest - 58, // 54: ktms.v1.AdminService.CreateTeam:input_type -> ktms.v1.CreateTeamRequest - 60, // 55: ktms.v1.AdminService.GetTeam:input_type -> ktms.v1.GetTeamRequest - 62, // 56: ktms.v1.AdminService.ListTeams:input_type -> ktms.v1.ListTeamsRequest - 64, // 57: ktms.v1.AdminService.UpdateTeam:input_type -> ktms.v1.UpdateTeamRequest - 66, // 58: ktms.v1.AdminService.DeleteTeam:input_type -> ktms.v1.DeleteTeamRequest - 68, // 59: ktms.v1.AdminService.GetTeamSpend:input_type -> ktms.v1.GetTeamSpendRequest - 3, // 60: ktms.v1.AdminService.UpsertModel:output_type -> ktms.v1.UpsertModelResponse - 5, // 61: ktms.v1.AdminService.DeleteModel:output_type -> ktms.v1.DeleteModelResponse - 7, // 62: ktms.v1.AdminService.ReplaceRules:output_type -> ktms.v1.ReplaceRulesResponse - 9, // 63: ktms.v1.AdminService.SetVendorKey:output_type -> ktms.v1.SetVendorKeyResponse - 11, // 64: ktms.v1.AdminService.DeleteVendorKey:output_type -> ktms.v1.DeleteVendorKeyResponse - 14, // 65: ktms.v1.AdminService.GetConfig:output_type -> ktms.v1.GetConfigResponse - 16, // 66: ktms.v1.AdminService.MintAPIKey:output_type -> ktms.v1.MintAPIKeyResponse - 18, // 67: ktms.v1.AdminService.RevokeAPIKey:output_type -> ktms.v1.RevokeAPIKeyResponse - 21, // 68: ktms.v1.AdminService.ListAPIKeys:output_type -> ktms.v1.ListAPIKeysResponse - 23, // 69: ktms.v1.AdminService.RotateEncryption:output_type -> ktms.v1.RotateEncryptionResponse - 25, // 70: ktms.v1.AdminService.MintNodeKey:output_type -> ktms.v1.MintNodeKeyResponse - 27, // 71: ktms.v1.AdminService.RevokeNodeKey:output_type -> ktms.v1.RevokeNodeKeyResponse - 30, // 72: ktms.v1.AdminService.ListNodeKeys:output_type -> ktms.v1.ListNodeKeysResponse - 33, // 73: ktms.v1.AdminService.ListNodes:output_type -> ktms.v1.ListNodesResponse - 35, // 74: ktms.v1.AdminService.DrainNode:output_type -> ktms.v1.DrainNodeResponse - 38, // 75: ktms.v1.AdminService.SetUserRateLimit:output_type -> ktms.v1.SetUserRateLimitResponse - 40, // 76: ktms.v1.AdminService.DeleteUserRateLimit:output_type -> ktms.v1.DeleteUserRateLimitResponse - 42, // 77: ktms.v1.AdminService.ListUserRateLimits:output_type -> ktms.v1.ListUserRateLimitsResponse - 45, // 78: ktms.v1.AdminService.SetSpendLimit:output_type -> ktms.v1.SetSpendLimitResponse - 47, // 79: ktms.v1.AdminService.DeleteSpendLimit:output_type -> ktms.v1.DeleteSpendLimitResponse - 49, // 80: ktms.v1.AdminService.ListSpendLimits:output_type -> ktms.v1.ListSpendLimitsResponse - 53, // 81: ktms.v1.AdminService.CreateOrganization:output_type -> ktms.v1.CreateOrganizationResponse - 55, // 82: ktms.v1.AdminService.ListOrganizations:output_type -> ktms.v1.ListOrganizationsResponse - 57, // 83: ktms.v1.AdminService.DeleteOrganization:output_type -> ktms.v1.DeleteOrganizationResponse - 59, // 84: ktms.v1.AdminService.CreateTeam:output_type -> ktms.v1.CreateTeamResponse - 61, // 85: ktms.v1.AdminService.GetTeam:output_type -> ktms.v1.GetTeamResponse - 63, // 86: ktms.v1.AdminService.ListTeams:output_type -> ktms.v1.ListTeamsResponse - 65, // 87: ktms.v1.AdminService.UpdateTeam:output_type -> ktms.v1.UpdateTeamResponse - 67, // 88: ktms.v1.AdminService.DeleteTeam:output_type -> ktms.v1.DeleteTeamResponse - 69, // 89: ktms.v1.AdminService.GetTeamSpend:output_type -> ktms.v1.GetTeamSpendResponse - 60, // [60:90] is the sub-list for method output_type - 30, // [30:60] is the sub-list for method input_type - 30, // [30:30] is the sub-list for extension type_name - 30, // [30:30] is the sub-list for extension extendee - 0, // [0:30] is the sub-list for field type_name + 39, // 9: ktms.v1.GetConfigResponse.user_rate_limits:type_name -> ktms.v1.UserRateLimitSpec + 46, // 10: ktms.v1.GetConfigResponse.spend_limits:type_name -> ktms.v1.SpendLimitSpec + 16, // 11: ktms.v1.MintAPIKeyRequest.policy:type_name -> ktms.v1.APIKeyPolicy + 16, // 12: ktms.v1.SetAPIKeyPolicyRequest.policy:type_name -> ktms.v1.APIKeyPolicy + 78, // 13: ktms.v1.APIKeyInfo.created_at:type_name -> google.protobuf.Timestamp + 16, // 14: ktms.v1.APIKeyInfo.policy:type_name -> ktms.v1.APIKeyPolicy + 23, // 15: ktms.v1.ListAPIKeysResponse.keys:type_name -> ktms.v1.APIKeyInfo + 78, // 16: ktms.v1.NodeKeyInfo.created_at:type_name -> google.protobuf.Timestamp + 32, // 17: ktms.v1.ListNodeKeysResponse.keys:type_name -> ktms.v1.NodeKeyInfo + 78, // 18: ktms.v1.NodeInfo.last_heartbeat:type_name -> google.protobuf.Timestamp + 35, // 19: ktms.v1.ListNodesResponse.nodes:type_name -> ktms.v1.NodeInfo + 39, // 20: ktms.v1.SetUserRateLimitRequest.rate_limit:type_name -> ktms.v1.UserRateLimitSpec + 39, // 21: ktms.v1.ListUserRateLimitsResponse.rate_limits:type_name -> ktms.v1.UserRateLimitSpec + 46, // 22: ktms.v1.SetSpendLimitRequest.spend_limit:type_name -> ktms.v1.SpendLimitSpec + 46, // 23: ktms.v1.ListSpendLimitsResponse.spend_limits:type_name -> ktms.v1.SpendLimitSpec + 78, // 24: ktms.v1.Organization.created_at:type_name -> google.protobuf.Timestamp + 78, // 25: ktms.v1.Team.created_at:type_name -> google.protobuf.Timestamp + 53, // 26: ktms.v1.CreateOrganizationResponse.organization:type_name -> ktms.v1.Organization + 53, // 27: ktms.v1.ListOrganizationsResponse.organizations:type_name -> ktms.v1.Organization + 54, // 28: ktms.v1.CreateTeamResponse.team:type_name -> ktms.v1.Team + 54, // 29: ktms.v1.GetTeamResponse.team:type_name -> ktms.v1.Team + 54, // 30: ktms.v1.ListTeamsResponse.teams:type_name -> ktms.v1.Team + 54, // 31: ktms.v1.UpdateTeamResponse.team:type_name -> ktms.v1.Team + 78, // 32: ktms.v1.GetTeamSpendResponse.resets_at:type_name -> google.protobuf.Timestamp + 78, // 33: ktms.v1.SpendWindow.resets_at:type_name -> google.protobuf.Timestamp + 74, // 34: ktms.v1.GetSpendResponse.windows:type_name -> ktms.v1.SpendWindow + 2, // 35: ktms.v1.AdminService.UpsertModel:input_type -> ktms.v1.UpsertModelRequest + 4, // 36: ktms.v1.AdminService.DeleteModel:input_type -> ktms.v1.DeleteModelRequest + 6, // 37: ktms.v1.AdminService.ReplaceRules:input_type -> ktms.v1.ReplaceRulesRequest + 8, // 38: ktms.v1.AdminService.SetVendorKey:input_type -> ktms.v1.SetVendorKeyRequest + 10, // 39: ktms.v1.AdminService.DeleteVendorKey:input_type -> ktms.v1.DeleteVendorKeyRequest + 13, // 40: ktms.v1.AdminService.GetConfig:input_type -> ktms.v1.GetConfigRequest + 15, // 41: ktms.v1.AdminService.MintAPIKey:input_type -> ktms.v1.MintAPIKeyRequest + 20, // 42: ktms.v1.AdminService.RevokeAPIKey:input_type -> ktms.v1.RevokeAPIKeyRequest + 22, // 43: ktms.v1.AdminService.ListAPIKeys:input_type -> ktms.v1.ListAPIKeysRequest + 17, // 44: ktms.v1.AdminService.SetAPIKeyPolicy:input_type -> ktms.v1.SetAPIKeyPolicyRequest + 25, // 45: ktms.v1.AdminService.RotateEncryption:input_type -> ktms.v1.RotateEncryptionRequest + 27, // 46: ktms.v1.AdminService.MintNodeKey:input_type -> ktms.v1.MintNodeKeyRequest + 29, // 47: ktms.v1.AdminService.RevokeNodeKey:input_type -> ktms.v1.RevokeNodeKeyRequest + 31, // 48: ktms.v1.AdminService.ListNodeKeys:input_type -> ktms.v1.ListNodeKeysRequest + 34, // 49: ktms.v1.AdminService.ListNodes:input_type -> ktms.v1.ListNodesRequest + 37, // 50: ktms.v1.AdminService.DrainNode:input_type -> ktms.v1.DrainNodeRequest + 40, // 51: ktms.v1.AdminService.SetUserRateLimit:input_type -> ktms.v1.SetUserRateLimitRequest + 42, // 52: ktms.v1.AdminService.DeleteUserRateLimit:input_type -> ktms.v1.DeleteUserRateLimitRequest + 44, // 53: ktms.v1.AdminService.ListUserRateLimits:input_type -> ktms.v1.ListUserRateLimitsRequest + 47, // 54: ktms.v1.AdminService.SetSpendLimit:input_type -> ktms.v1.SetSpendLimitRequest + 49, // 55: ktms.v1.AdminService.DeleteSpendLimit:input_type -> ktms.v1.DeleteSpendLimitRequest + 51, // 56: ktms.v1.AdminService.ListSpendLimits:input_type -> ktms.v1.ListSpendLimitsRequest + 55, // 57: ktms.v1.AdminService.CreateOrganization:input_type -> ktms.v1.CreateOrganizationRequest + 57, // 58: ktms.v1.AdminService.ListOrganizations:input_type -> ktms.v1.ListOrganizationsRequest + 59, // 59: ktms.v1.AdminService.DeleteOrganization:input_type -> ktms.v1.DeleteOrganizationRequest + 61, // 60: ktms.v1.AdminService.CreateTeam:input_type -> ktms.v1.CreateTeamRequest + 63, // 61: ktms.v1.AdminService.GetTeam:input_type -> ktms.v1.GetTeamRequest + 65, // 62: ktms.v1.AdminService.ListTeams:input_type -> ktms.v1.ListTeamsRequest + 67, // 63: ktms.v1.AdminService.UpdateTeam:input_type -> ktms.v1.UpdateTeamRequest + 69, // 64: ktms.v1.AdminService.DeleteTeam:input_type -> ktms.v1.DeleteTeamRequest + 71, // 65: ktms.v1.AdminService.GetTeamSpend:input_type -> ktms.v1.GetTeamSpendRequest + 73, // 66: ktms.v1.AdminService.GetSpend:input_type -> ktms.v1.GetSpendRequest + 3, // 67: ktms.v1.AdminService.UpsertModel:output_type -> ktms.v1.UpsertModelResponse + 5, // 68: ktms.v1.AdminService.DeleteModel:output_type -> ktms.v1.DeleteModelResponse + 7, // 69: ktms.v1.AdminService.ReplaceRules:output_type -> ktms.v1.ReplaceRulesResponse + 9, // 70: ktms.v1.AdminService.SetVendorKey:output_type -> ktms.v1.SetVendorKeyResponse + 11, // 71: ktms.v1.AdminService.DeleteVendorKey:output_type -> ktms.v1.DeleteVendorKeyResponse + 14, // 72: ktms.v1.AdminService.GetConfig:output_type -> ktms.v1.GetConfigResponse + 19, // 73: ktms.v1.AdminService.MintAPIKey:output_type -> ktms.v1.MintAPIKeyResponse + 21, // 74: ktms.v1.AdminService.RevokeAPIKey:output_type -> ktms.v1.RevokeAPIKeyResponse + 24, // 75: ktms.v1.AdminService.ListAPIKeys:output_type -> ktms.v1.ListAPIKeysResponse + 18, // 76: ktms.v1.AdminService.SetAPIKeyPolicy:output_type -> ktms.v1.SetAPIKeyPolicyResponse + 26, // 77: ktms.v1.AdminService.RotateEncryption:output_type -> ktms.v1.RotateEncryptionResponse + 28, // 78: ktms.v1.AdminService.MintNodeKey:output_type -> ktms.v1.MintNodeKeyResponse + 30, // 79: ktms.v1.AdminService.RevokeNodeKey:output_type -> ktms.v1.RevokeNodeKeyResponse + 33, // 80: ktms.v1.AdminService.ListNodeKeys:output_type -> ktms.v1.ListNodeKeysResponse + 36, // 81: ktms.v1.AdminService.ListNodes:output_type -> ktms.v1.ListNodesResponse + 38, // 82: ktms.v1.AdminService.DrainNode:output_type -> ktms.v1.DrainNodeResponse + 41, // 83: ktms.v1.AdminService.SetUserRateLimit:output_type -> ktms.v1.SetUserRateLimitResponse + 43, // 84: ktms.v1.AdminService.DeleteUserRateLimit:output_type -> ktms.v1.DeleteUserRateLimitResponse + 45, // 85: ktms.v1.AdminService.ListUserRateLimits:output_type -> ktms.v1.ListUserRateLimitsResponse + 48, // 86: ktms.v1.AdminService.SetSpendLimit:output_type -> ktms.v1.SetSpendLimitResponse + 50, // 87: ktms.v1.AdminService.DeleteSpendLimit:output_type -> ktms.v1.DeleteSpendLimitResponse + 52, // 88: ktms.v1.AdminService.ListSpendLimits:output_type -> ktms.v1.ListSpendLimitsResponse + 56, // 89: ktms.v1.AdminService.CreateOrganization:output_type -> ktms.v1.CreateOrganizationResponse + 58, // 90: ktms.v1.AdminService.ListOrganizations:output_type -> ktms.v1.ListOrganizationsResponse + 60, // 91: ktms.v1.AdminService.DeleteOrganization:output_type -> ktms.v1.DeleteOrganizationResponse + 62, // 92: ktms.v1.AdminService.CreateTeam:output_type -> ktms.v1.CreateTeamResponse + 64, // 93: ktms.v1.AdminService.GetTeam:output_type -> ktms.v1.GetTeamResponse + 66, // 94: ktms.v1.AdminService.ListTeams:output_type -> ktms.v1.ListTeamsResponse + 68, // 95: ktms.v1.AdminService.UpdateTeam:output_type -> ktms.v1.UpdateTeamResponse + 70, // 96: ktms.v1.AdminService.DeleteTeam:output_type -> ktms.v1.DeleteTeamResponse + 72, // 97: ktms.v1.AdminService.GetTeamSpend:output_type -> ktms.v1.GetTeamSpendResponse + 75, // 98: ktms.v1.AdminService.GetSpend:output_type -> ktms.v1.GetSpendResponse + 67, // [67:99] is the sub-list for method output_type + 35, // [35:67] is the sub-list for method input_type + 35, // [35:35] is the sub-list for extension type_name + 35, // [35:35] is the sub-list for extension extendee + 0, // [0:35] is the sub-list for field type_name } func init() { file_ktms_v1_admin_proto_init() } @@ -4295,7 +4710,7 @@ func file_ktms_v1_admin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ktms_v1_admin_proto_rawDesc), len(file_ktms_v1_admin_proto_rawDesc)), NumEnums: 0, - NumMessages: 71, + NumMessages: 77, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/gen/ktms/v1/admin_grpc.pb.go b/internal/gen/ktms/v1/admin_grpc.pb.go index 17008d2..7b6f2a4 100644 --- a/internal/gen/ktms/v1/admin_grpc.pb.go +++ b/internal/gen/ktms/v1/admin_grpc.pb.go @@ -28,6 +28,7 @@ const ( AdminService_MintAPIKey_FullMethodName = "/ktms.v1.AdminService/MintAPIKey" AdminService_RevokeAPIKey_FullMethodName = "/ktms.v1.AdminService/RevokeAPIKey" AdminService_ListAPIKeys_FullMethodName = "/ktms.v1.AdminService/ListAPIKeys" + AdminService_SetAPIKeyPolicy_FullMethodName = "/ktms.v1.AdminService/SetAPIKeyPolicy" AdminService_RotateEncryption_FullMethodName = "/ktms.v1.AdminService/RotateEncryption" AdminService_MintNodeKey_FullMethodName = "/ktms.v1.AdminService/MintNodeKey" AdminService_RevokeNodeKey_FullMethodName = "/ktms.v1.AdminService/RevokeNodeKey" @@ -49,6 +50,7 @@ const ( AdminService_UpdateTeam_FullMethodName = "/ktms.v1.AdminService/UpdateTeam" AdminService_DeleteTeam_FullMethodName = "/ktms.v1.AdminService/DeleteTeam" AdminService_GetTeamSpend_FullMethodName = "/ktms.v1.AdminService/GetTeamSpend" + AdminService_GetSpend_FullMethodName = "/ktms.v1.AdminService/GetSpend" ) // AdminServiceClient is the client API for AdminService service. @@ -67,6 +69,7 @@ type AdminServiceClient interface { MintAPIKey(ctx context.Context, in *MintAPIKeyRequest, opts ...grpc.CallOption) (*MintAPIKeyResponse, error) RevokeAPIKey(ctx context.Context, in *RevokeAPIKeyRequest, opts ...grpc.CallOption) (*RevokeAPIKeyResponse, error) ListAPIKeys(ctx context.Context, in *ListAPIKeysRequest, opts ...grpc.CallOption) (*ListAPIKeysResponse, error) + SetAPIKeyPolicy(ctx context.Context, in *SetAPIKeyPolicyRequest, opts ...grpc.CallOption) (*SetAPIKeyPolicyResponse, error) RotateEncryption(ctx context.Context, in *RotateEncryptionRequest, opts ...grpc.CallOption) (*RotateEncryptionResponse, error) MintNodeKey(ctx context.Context, in *MintNodeKeyRequest, opts ...grpc.CallOption) (*MintNodeKeyResponse, error) RevokeNodeKey(ctx context.Context, in *RevokeNodeKeyRequest, opts ...grpc.CallOption) (*RevokeNodeKeyResponse, error) @@ -88,6 +91,7 @@ type AdminServiceClient interface { UpdateTeam(ctx context.Context, in *UpdateTeamRequest, opts ...grpc.CallOption) (*UpdateTeamResponse, error) DeleteTeam(ctx context.Context, in *DeleteTeamRequest, opts ...grpc.CallOption) (*DeleteTeamResponse, error) GetTeamSpend(ctx context.Context, in *GetTeamSpendRequest, opts ...grpc.CallOption) (*GetTeamSpendResponse, error) + GetSpend(ctx context.Context, in *GetSpendRequest, opts ...grpc.CallOption) (*GetSpendResponse, error) } type adminServiceClient struct { @@ -188,6 +192,16 @@ func (c *adminServiceClient) ListAPIKeys(ctx context.Context, in *ListAPIKeysReq return out, nil } +func (c *adminServiceClient) SetAPIKeyPolicy(ctx context.Context, in *SetAPIKeyPolicyRequest, opts ...grpc.CallOption) (*SetAPIKeyPolicyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SetAPIKeyPolicyResponse) + err := c.cc.Invoke(ctx, AdminService_SetAPIKeyPolicy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *adminServiceClient) RotateEncryption(ctx context.Context, in *RotateEncryptionRequest, opts ...grpc.CallOption) (*RotateEncryptionResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(RotateEncryptionResponse) @@ -398,6 +412,16 @@ func (c *adminServiceClient) GetTeamSpend(ctx context.Context, in *GetTeamSpendR return out, nil } +func (c *adminServiceClient) GetSpend(ctx context.Context, in *GetSpendRequest, opts ...grpc.CallOption) (*GetSpendResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSpendResponse) + err := c.cc.Invoke(ctx, AdminService_GetSpend_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AdminServiceServer is the server API for AdminService service. // All implementations must embed UnimplementedAdminServiceServer // for forward compatibility. @@ -414,6 +438,7 @@ type AdminServiceServer interface { MintAPIKey(context.Context, *MintAPIKeyRequest) (*MintAPIKeyResponse, error) RevokeAPIKey(context.Context, *RevokeAPIKeyRequest) (*RevokeAPIKeyResponse, error) ListAPIKeys(context.Context, *ListAPIKeysRequest) (*ListAPIKeysResponse, error) + SetAPIKeyPolicy(context.Context, *SetAPIKeyPolicyRequest) (*SetAPIKeyPolicyResponse, error) RotateEncryption(context.Context, *RotateEncryptionRequest) (*RotateEncryptionResponse, error) MintNodeKey(context.Context, *MintNodeKeyRequest) (*MintNodeKeyResponse, error) RevokeNodeKey(context.Context, *RevokeNodeKeyRequest) (*RevokeNodeKeyResponse, error) @@ -435,6 +460,7 @@ type AdminServiceServer interface { UpdateTeam(context.Context, *UpdateTeamRequest) (*UpdateTeamResponse, error) DeleteTeam(context.Context, *DeleteTeamRequest) (*DeleteTeamResponse, error) GetTeamSpend(context.Context, *GetTeamSpendRequest) (*GetTeamSpendResponse, error) + GetSpend(context.Context, *GetSpendRequest) (*GetSpendResponse, error) mustEmbedUnimplementedAdminServiceServer() } @@ -472,6 +498,9 @@ func (UnimplementedAdminServiceServer) RevokeAPIKey(context.Context, *RevokeAPIK func (UnimplementedAdminServiceServer) ListAPIKeys(context.Context, *ListAPIKeysRequest) (*ListAPIKeysResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListAPIKeys not implemented") } +func (UnimplementedAdminServiceServer) SetAPIKeyPolicy(context.Context, *SetAPIKeyPolicyRequest) (*SetAPIKeyPolicyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SetAPIKeyPolicy not implemented") +} func (UnimplementedAdminServiceServer) RotateEncryption(context.Context, *RotateEncryptionRequest) (*RotateEncryptionResponse, error) { return nil, status.Error(codes.Unimplemented, "method RotateEncryption not implemented") } @@ -535,6 +564,9 @@ func (UnimplementedAdminServiceServer) DeleteTeam(context.Context, *DeleteTeamRe func (UnimplementedAdminServiceServer) GetTeamSpend(context.Context, *GetTeamSpendRequest) (*GetTeamSpendResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetTeamSpend not implemented") } +func (UnimplementedAdminServiceServer) GetSpend(context.Context, *GetSpendRequest) (*GetSpendResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSpend not implemented") +} func (UnimplementedAdminServiceServer) mustEmbedUnimplementedAdminServiceServer() {} func (UnimplementedAdminServiceServer) testEmbeddedByValue() {} @@ -718,6 +750,24 @@ func _AdminService_ListAPIKeys_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _AdminService_SetAPIKeyPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetAPIKeyPolicyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServiceServer).SetAPIKeyPolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminService_SetAPIKeyPolicy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServiceServer).SetAPIKeyPolicy(ctx, req.(*SetAPIKeyPolicyRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _AdminService_RotateEncryption_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RotateEncryptionRequest) if err := dec(in); err != nil { @@ -1096,6 +1146,24 @@ func _AdminService_GetTeamSpend_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _AdminService_GetSpend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSpendRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServiceServer).GetSpend(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminService_GetSpend_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServiceServer).GetSpend(ctx, req.(*GetSpendRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AdminService_ServiceDesc is the grpc.ServiceDesc for AdminService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1139,6 +1207,10 @@ var AdminService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListAPIKeys", Handler: _AdminService_ListAPIKeys_Handler, }, + { + MethodName: "SetAPIKeyPolicy", + Handler: _AdminService_SetAPIKeyPolicy_Handler, + }, { MethodName: "RotateEncryption", Handler: _AdminService_RotateEncryption_Handler, @@ -1223,6 +1295,10 @@ var AdminService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetTeamSpend", Handler: _AdminService_GetTeamSpend_Handler, }, + { + MethodName: "GetSpend", + Handler: _AdminService_GetSpend_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "ktms/v1/admin.proto", diff --git a/internal/gen/ktms/v1/gateway.pb.go b/internal/gen/ktms/v1/gateway.pb.go index 710e296..ed541ef 100644 --- a/internal/gen/ktms/v1/gateway.pb.go +++ b/internal/gen/ktms/v1/gateway.pb.go @@ -1451,9 +1451,12 @@ func (x *GenerateTextRequest) GetEffortSpecified() bool { } type TokensUsage struct { - state protoimpl.MessageState `protogen:"open.v1"` - Input int64 `protobuf:"varint,1,opt,name=input,proto3" json:"input,omitempty"` - Output int64 `protobuf:"varint,2,opt,name=output,proto3" json:"output,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Input int64 `protobuf:"varint,1,opt,name=input,proto3" json:"input,omitempty"` + Output int64 `protobuf:"varint,2,opt,name=output,proto3" json:"output,omitempty"` + // Parts of input read from and written to the vendor's prompt cache. + CacheRead int64 `protobuf:"varint,3,opt,name=cache_read,json=cacheRead,proto3" json:"cache_read,omitempty"` + CacheWrite int64 `protobuf:"varint,4,opt,name=cache_write,json=cacheWrite,proto3" json:"cache_write,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1502,6 +1505,20 @@ func (x *TokensUsage) GetOutput() int64 { return 0 } +func (x *TokensUsage) GetCacheRead() int64 { + if x != nil { + return x.CacheRead + } + return 0 +} + +func (x *TokensUsage) GetCacheWrite() int64 { + if x != nil { + return x.CacheWrite + } + return 0 +} + type Choice struct { state protoimpl.MessageState `protogen:"open.v1"` Index int32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"` @@ -1873,8 +1890,11 @@ type ModelInfo struct { PriceOutPerMtok float64 `protobuf:"fixed64,7,opt,name=price_out_per_mtok,json=priceOutPerMtok,proto3" json:"price_out_per_mtok,omitempty"` PriceInBatchPerMtok float64 `protobuf:"fixed64,8,opt,name=price_in_batch_per_mtok,json=priceInBatchPerMtok,proto3" json:"price_in_batch_per_mtok,omitempty"` PriceOutBatchPerMtok float64 `protobuf:"fixed64,9,opt,name=price_out_batch_per_mtok,json=priceOutBatchPerMtok,proto3" json:"price_out_batch_per_mtok,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Effective prompt-cache rates, vendor defaults applied. + PriceCacheReadPerMtok float64 `protobuf:"fixed64,10,opt,name=price_cache_read_per_mtok,json=priceCacheReadPerMtok,proto3" json:"price_cache_read_per_mtok,omitempty"` + PriceCacheWritePerMtok float64 `protobuf:"fixed64,11,opt,name=price_cache_write_per_mtok,json=priceCacheWritePerMtok,proto3" json:"price_cache_write_per_mtok,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ModelInfo) Reset() { @@ -1970,6 +1990,20 @@ func (x *ModelInfo) GetPriceOutBatchPerMtok() float64 { return 0 } +func (x *ModelInfo) GetPriceCacheReadPerMtok() float64 { + if x != nil { + return x.PriceCacheReadPerMtok + } + return 0 +} + +func (x *ModelInfo) GetPriceCacheWritePerMtok() float64 { + if x != nil { + return x.PriceCacheWritePerMtok + } + return 0 +} + type ListModelsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Models the calling client can call: those it holds a vendor key for, plus every local model (no key needed), @@ -4242,10 +4276,14 @@ const file_ktms_v1_gateway_proto_rawDesc = "" + "\x10effort_specified\x18\x10 \x01(\bR\x0feffortSpecified\x1a=\n" + "\x0fAttributesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\";\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"{\n" + "\vTokensUsage\x12\x14\n" + "\x05input\x18\x01 \x01(\x03R\x05input\x12\x16\n" + - "\x06output\x18\x02 \x01(\x03R\x06output\"\x89\x01\n" + + "\x06output\x18\x02 \x01(\x03R\x06output\x12\x1d\n" + + "\n" + + "cache_read\x18\x03 \x01(\x03R\tcacheRead\x12\x1f\n" + + "\vcache_write\x18\x04 \x01(\x03R\n" + + "cacheWrite\"\x89\x01\n" + "\x06Choice\x12\x14\n" + "\x05index\x18\x01 \x01(\x05R\x05index\x12\x12\n" + "\x04text\x18\x02 \x01(\tR\x04text\x120\n" + @@ -4285,7 +4323,7 @@ const file_ktms_v1_gateway_proto_rawDesc = "" + "judgeScore\x12\x1f\n" + "\vjudge_model\x18\f \x01(\tR\n" + "judgeModel\"\x13\n" + - "\x11ListModelsRequest\"\xda\x02\n" + + "\x11ListModelsRequest\"\xd0\x03\n" + "\tModelInfo\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + "\x06vendor\x18\x02 \x01(\tR\x06vendor\x12)\n" + @@ -4295,7 +4333,10 @@ const file_ktms_v1_gateway_proto_rawDesc = "" + "\x11price_in_per_mtok\x18\x06 \x01(\x01R\x0epriceInPerMtok\x12+\n" + "\x12price_out_per_mtok\x18\a \x01(\x01R\x0fpriceOutPerMtok\x124\n" + "\x17price_in_batch_per_mtok\x18\b \x01(\x01R\x13priceInBatchPerMtok\x126\n" + - "\x18price_out_batch_per_mtok\x18\t \x01(\x01R\x14priceOutBatchPerMtok\"@\n" + + "\x18price_out_batch_per_mtok\x18\t \x01(\x01R\x14priceOutBatchPerMtok\x128\n" + + "\x19price_cache_read_per_mtok\x18\n" + + " \x01(\x01R\x15priceCacheReadPerMtok\x12:\n" + + "\x1aprice_cache_write_per_mtok\x18\v \x01(\x01R\x16priceCacheWritePerMtok\"@\n" + "\x12ListModelsResponse\x12*\n" + "\x06models\x18\x01 \x03(\v2\x12.ktms.v1.ModelInfoR\x06models\"S\n" + "\x19GenerateTextStreamRequest\x126\n" + diff --git a/internal/gen/ktms/v1/gateway_grpc.pb.go b/internal/gen/ktms/v1/gateway_grpc.pb.go index 4c64f43..a3b75a9 100644 --- a/internal/gen/ktms/v1/gateway_grpc.pb.go +++ b/internal/gen/ktms/v1/gateway_grpc.pb.go @@ -44,14 +44,15 @@ const ( // paused every model in the chain with Retry-After, "vendors_unavailable" // carries RetryInfo with the soonest one frees up: wait that long first. // RESOURCE_EXHAUSTED = throttled ("throttled:rpm" | "throttled:budget_key" | -// "throttled:budget_user") — back off, do not hot-retry; or a spend limit +// "throttled:budget_user"), or the key's own rate ("throttled:key_rpm" | +// "throttled:key_tpm", with RetryInfo) — back off, do not hot-retry; or a spend limit // ("throttled:spend__", "throttled:spend_team_monthly") // with ErrorInfo "insufficient_quota" and RetryInfo until the UTC period resets. -// PERMISSION_DENIED: the key's team may not use the model ("model \"\" not -// allowed for team", "no models in chain allowed for team", "model access -// denied by team policy"); do not retry. +// PERMISSION_DENIED: the key or its team may not use the model ("model \"\" +// not allowed for ", "no models in chain allowed for ", +// "model access denied by key or team policy" for batches); do not retry. // FAILED_PRECONDITION: "output_truncated" (raise max_output_tokens, do not -// retry as-is), "no_capable_model", "no_vendor_key", "not_configured", +// retry as-is), "request_too_large" (above a minute of the key's tpm_limit), "no_capable_model", "no_vendor_key", "not_configured", // "batch not configured". // INVALID_ARGUMENT: "guardrail_violation" (PII or prompt injection detected), // "bad_request: " (validation errors, empty batch, duplicate custom_id). @@ -252,14 +253,15 @@ func (c *gatewayServiceClient) CancelBatch(ctx context.Context, in *CancelBatchR // paused every model in the chain with Retry-After, "vendors_unavailable" // carries RetryInfo with the soonest one frees up: wait that long first. // RESOURCE_EXHAUSTED = throttled ("throttled:rpm" | "throttled:budget_key" | -// "throttled:budget_user") — back off, do not hot-retry; or a spend limit +// "throttled:budget_user"), or the key's own rate ("throttled:key_rpm" | +// "throttled:key_tpm", with RetryInfo) — back off, do not hot-retry; or a spend limit // ("throttled:spend__", "throttled:spend_team_monthly") // with ErrorInfo "insufficient_quota" and RetryInfo until the UTC period resets. -// PERMISSION_DENIED: the key's team may not use the model ("model \"\" not -// allowed for team", "no models in chain allowed for team", "model access -// denied by team policy"); do not retry. +// PERMISSION_DENIED: the key or its team may not use the model ("model \"\" +// not allowed for ", "no models in chain allowed for ", +// "model access denied by key or team policy" for batches); do not retry. // FAILED_PRECONDITION: "output_truncated" (raise max_output_tokens, do not -// retry as-is), "no_capable_model", "no_vendor_key", "not_configured", +// retry as-is), "request_too_large" (above a minute of the key's tpm_limit), "no_capable_model", "no_vendor_key", "not_configured", // "batch not configured". // INVALID_ARGUMENT: "guardrail_violation" (PII or prompt injection detected), // "bad_request: " (validation errors, empty batch, duplicate custom_id). diff --git a/internal/web/static/js/pages/keys.js b/internal/web/static/js/pages/keys.js index c67041d..f8a10fa 100644 --- a/internal/web/static/js/pages/keys.js +++ b/internal/web/static/js/pages/keys.js @@ -16,9 +16,14 @@ export function KeysPage({ params }) { canReadGlobal() ? api('GET', '/api/proxy/spend-limits?subject_type=key').catch((error) => ({ error })) : { spend_limits: [] }, canReadGlobal() ? loadCfg(true).catch((error) => ({ error })) : null, ]); + // Where a key is capped, what it has spent so far; a failure shows as unknown. + const capped = [...new Set((limits.spend_limits || []).map((l) => l.subject_id).filter((id) => id !== '*'))]; + const spend = Object.fromEntries(await Promise.all(capped.map((id) => + api('GET', `/api/proxy/spend?subject_type=key&subject_id=${encodeURIComponent(id)}`).then((r) => [id, r.windows || []], () => [id, null])))); // Side panels fail on their own: the key list still shows, and an // unreadable panel says so instead of looking empty. - return { keys: keys.keys || [], teams: teams.teams || [], limits: limits.spend_limits || [], limitsError: limits.error, vendorKeys: cfg?.vendor_keys || [], cfgError: cfg?.error }; + return { keys: keys.keys || [], teams: teams.teams || [], limits: limits.spend_limits || [], limitsError: limits.error, spend, + vendorKeys: cfg?.vendor_keys || [], models: (cfg?.models || []).map((m) => m.id), cfgError: cfg?.error }; }, []); const teamName = useMemo(() => Object.fromEntries((st.data?.teams || []).map((t) => [t.id, t.name])), [st.data]); const caps = useMemo(() => { @@ -33,14 +38,16 @@ export function KeysPage({ params }) { action: () => api('DELETE', '/api/proxy/keys/' + encodeURIComponent(k.name)), }).then((ok) => ok && st.reload()); + const editPolicy = (k) => openDialog(PolicyForm, { k, models: modelChoices(st.data, k) }).then((ok) => ok && st.reload()); const keys = st.data?.keys || []; const rows = keys.map((k) => html`
${k.name}${k.team_id ? 'team ' + (teamName[k.team_id] || k.team_id.slice(0, 8) + '…') : 'no team'}
${(k.scopes || []).map((s) => html`${Object.fromEntries(SCOPES.map(([id, t]) => [id, t]))[s] || s}`)}
- ${canReadGlobal() && html`${(caps[k.name] || []).length ? html`
${caps[k.name].map((l) => html`${usd(nanoUSD(l.limit_nanousd))} / ${l.period}`)}
` : html`${st.data?.limitsError ? 'unknown' : '—'}`}`} + <${PolicyChips} policy=${k.policy} /> + ${canReadGlobal() && html`${(caps[k.name] || []).length ? html`
${caps[k.name].map((l) => html`${spentText(st.data?.spend?.[k.name], l)}`)}
` : html`${st.data?.limitsError ? 'unknown' : '—'}`}`} ${when(k.created_at)} ${k.disabled ? html`<${Badge}>Revoked` : html`<${Badge} tone="ok">Active`} - ${canMint && !k.disabled && html``} + ${canMint && !k.disabled && html``} `); return html` @@ -50,7 +57,7 @@ export function KeysPage({ params }) {
<${Card} flush label="API keys"> <${Table} loading=${st.loading} rows=${rows} - head=${['Name', 'Access', ...(canReadGlobal() ? ['Spend caps'] : []), 'Created', 'Status', { label: 'Actions', sr: true }]} + head=${['Name', 'Access', 'Policy', ...(canReadGlobal() ? ['Spend today / this month'] : []), 'Created', 'Status', { label: 'Actions', sr: true }]} empty=${{ title: 'No API keys yet', children: canMint ? 'Create one with the form.' : 'Ask a team lead for one.' }} />
@@ -64,7 +71,7 @@ export function KeysPage({ params }) { function Wizard({ teams, team, onDone }) { const lead = !isAdmin(); const labels = lead ? ['Name', 'Done'] : ['Name', 'Access', 'Limits', 'Done']; - const fresh = () => ({ step: 0, name: '', team: lead ? session.me.user.team_id : team, scopes: ['generate'], daily: '', monthly: '', key: '', warn: '', copied: false, err: '', busy: false }); + const fresh = () => ({ step: 0, name: '', team: lead ? session.me.user.team_id : team, scopes: ['generate'], daily: '', monthly: '', rpm: '', tpm: '', key: '', warn: '', copied: false, err: '', busy: false }); const [s, setS] = useState(fresh); const set = (p) => setS((x) => ({ ...x, ...p })); const last = labels.length - 1; @@ -74,6 +81,7 @@ function Wizard({ teams, team, onDone }) { if (s.step === 0 && !s.name.trim()) return 'Give the key a name.'; if (labels[s.step] === 'Access' && !s.scopes.length) return 'Pick at least one kind of access.'; if (labels[s.step] === 'Limits' && [s.daily, s.monthly].some((v) => v !== '' && !(num(v) > 0))) return 'A cap must be above $0, or left empty.'; + if (labels[s.step] === 'Limits' && [s.rpm, s.tpm].some((v) => v !== '' && !(Number.isInteger(num(v)) && num(v) > 0))) return 'A rate must be a whole number above 0, or left empty.'; return ''; }; @@ -81,6 +89,7 @@ function Wizard({ teams, team, onDone }) { const team = teams.find((t) => t.id === s.team); const body = { name: s.name.trim(), scopes: lead ? ['generate'] : s.scopes }; if (team) { body.team_id = team.id; body.org_id = team.org_id; } + if (s.rpm !== '' || s.tpm !== '') body.policy = { rpm_limit: num(s.rpm) || 0, tpm_limit: String(num(s.tpm) || 0) }; const res = await api('POST', '/api/proxy/keys', body); // The key exists now; a cap that fails to save must not hide it. const failed = []; @@ -121,7 +130,9 @@ function Wizard({ teams, team, onDone }) { `} ${at === 'Limits' && html` <${Field} label="Daily spend cap (optional)" hint="Calls are refused once the key has spent this much since 00:00 UTC.">
$ set({ daily: e.currentTarget.value })} />
- <${Field} label="Monthly spend cap (optional)" hint="Counted per UTC calendar month.">
$ set({ monthly: e.currentTarget.value })} />
`} + <${Field} label="Monthly spend cap (optional)" hint="Counted per UTC calendar month.">
$ set({ monthly: e.currentTarget.value })} />
+ <${Field} label="Requests per minute (optional)" hint="Over all models. Allowed models are set with Policy after the key exists."> set({ rpm: e.currentTarget.value })} /> + <${Field} label="Tokens per minute (optional)" hint="Input and output over all models."> set({ tpm: e.currentTarget.value })} />`} ${at === 'Done' && html`
Key created. This is the only time it is shown. Store it in your secret manager now; KTMS keeps only a hash.
${s.key}
@@ -135,6 +146,55 @@ function Wizard({ teams, team, onDone }) { `; } +// spentText shows a cap with what the key has spent against it, as KTMS counts it. +function spentText(windows, limit) { + const w = (windows || []).find((x) => x.period === limit.period); + const cap = `${usd(nanoUSD(limit.limit_nanousd))} / ${limit.period}`; + return w?.metered ? `${usd(nanoUSD(w.spent_nanousd))} of ${cap}` : cap; +} + +function PolicyChips({ policy }) { + const p = policy || {}; + const chips = []; + if ((p.allowed_models || []).length) chips.push(html`${p.allowed_models.length === 1 ? p.allowed_models[0] : p.allowed_models.length + ' models'}`); + if (num(p.rpm_limit) > 0) chips.push(html`${compactNum(p.rpm_limit)} rpm`); + if (num(p.tpm_limit) > 0) chips.push(html`${compactNum(p.tpm_limit)} tpm`); + return chips.length ? html`
${chips}
` : html`—`; +} + +const compactNum = (v) => new Intl.NumberFormat('en', { notation: 'compact' }).format(num(v)); + +// modelChoices lists the models a key's allowlist can name: the gateway's for +// admins, the team's allowed models for a team lead, plus whatever it names now. +function modelChoices(data, k) { + const team = (data?.teams || []).find((t) => t.id === k.team_id); + const base = data?.models?.length ? data.models : (team?.allowed_models || []); + return [...new Set([...base, ...(k.policy?.allowed_models || [])])].sort(); +} + +function PolicyForm({ k, models, close }) { + const [picked, setPicked] = useState(k.policy?.allowed_models || []); + const toggle = (m) => setPicked((p) => (p.includes(m) ? p.filter((x) => x !== m) : [...p, m])); + const submit = async (fd) => { + const rpm = num(fd.get('rpm')), tpm = num(fd.get('tpm')); + if (![rpm, tpm].every((v) => Number.isInteger(v) && v >= 0)) throw new Error('Rates are whole numbers, 0 for no limit.'); + await api('PUT', `/api/proxy/keys/${encodeURIComponent(k.name)}/policy`, { allowed_models: picked, rpm_limit: rpm, tpm_limit: String(tpm) }); + toast('Policy saved'); + }; + return html`<${FormDialog} title=${'Policy of ' + k.name} sub="Applies from the key's next call, on top of its team's models and each model's limits." close=${close} onSubmit=${submit}> +
Allowed models + ${models.length + ? html`
${models.map((m) => html``)}
` + : html`

No models to choose from.

`} +

${picked.length ? 'Only these, and only where the team allows them too.' : 'None picked: any model the team allows.'}

+
+
+ <${Field} label="Requests per minute" hint="Over all models; 0 = no limit."> + <${Field} label="Tokens per minute" hint="Input and output over all models; 0 = no limit."> +
+ `; +} + function VendorKeys({ vendorKeys, error, keys, loading, reload }) { const active = keys.filter((k) => !k.disabled).map((k) => k.name); const setKey = () => openDialog(VendorForm, { names: active }).then((ok) => ok && reload()); diff --git a/internal/web/static/js/pages/models.js b/internal/web/static/js/pages/models.js index b3075c2..b9d034b 100644 --- a/internal/web/static/js/pages/models.js +++ b/internal/web/static/js/pages/models.js @@ -8,13 +8,15 @@ const CAPS = [ ['tts', 'Speech synthesis'], ['image', 'Image generation'], ['judge', 'Judge'], + ['prompt_cache', 'Anthropic: cache tools, system prompt and conversation'], ]; const BLANK = { id: '', vendor: 'openai', endpoint: '', efforts: ['EFFORT_LOW', 'EFFORT_MEDIUM', 'EFFORT_HIGH'], capabilities: [], rpm: 0, rpm_burst: 0, queue_timeout_ms: 0, budget_grace_margin: 0, price_in_per_mtok: 0, price_out_per_mtok: 0, price_in_peak_per_mtok: 0, price_out_peak_per_mtok: 0, - price_in_batch_per_mtok: 0, price_out_batch_per_mtok: 0, daily_tokens_per_key: 0, daily_tokens_per_user: 0, enabled: true, + price_in_batch_per_mtok: 0, price_out_batch_per_mtok: 0, price_cache_read_per_mtok: 0, price_cache_write_per_mtok: 0, + daily_tokens_per_key: 0, daily_tokens_per_user: 0, enabled: true, }; const rpmText = (m) => { @@ -74,7 +76,7 @@ function ModelForm({ model, close }) { if (!/^[A-Za-z0-9._:/-]+$/.test(id)) throw new Error('The model id may contain letters, digits and . _ : / - only.'); const efforts = ['LOW', 'MEDIUM', 'HIGH'].filter((e) => fd.get('effort_' + e)).map((e) => 'EFFORT_' + e); const spec = { ...m, id, vendor: fd.get('vendor'), endpoint: String(fd.get('endpoint')).trim(), efforts, capabilities: caps, enabled: !!fd.get('enabled') }; - for (const k of ['rpm', 'rpm_burst', 'queue_timeout_ms', 'budget_grace_margin', 'price_in_per_mtok', 'price_out_per_mtok', 'price_in_peak_per_mtok', 'price_out_peak_per_mtok', 'price_in_batch_per_mtok', 'price_out_batch_per_mtok', 'daily_tokens_per_key', 'daily_tokens_per_user']) spec[k] = num(fd.get(k)); + for (const k of ['rpm', 'rpm_burst', 'queue_timeout_ms', 'budget_grace_margin', 'price_in_per_mtok', 'price_out_per_mtok', 'price_in_peak_per_mtok', 'price_out_peak_per_mtok', 'price_in_batch_per_mtok', 'price_out_batch_per_mtok', 'price_cache_read_per_mtok', 'price_cache_write_per_mtok', 'daily_tokens_per_key', 'daily_tokens_per_user']) spec[k] = num(fd.get(k)); await api('PUT', '/api/proxy/models/' + encodeURIComponent(id), spec); toast('Model saved'); }; @@ -97,6 +99,7 @@ function ModelForm({ model, close }) { ${n('price_in_per_mtok', 'Input', '', { min: 0 })}${n('price_out_per_mtok', 'Output', '', { min: 0 })} ${n('price_in_peak_per_mtok', 'Peak input', '0 = one price all day', { min: 0 })}${n('price_out_peak_per_mtok', 'Peak output', '', { min: 0 })} ${n('price_in_batch_per_mtok', 'Batch input', '0 = half the input price', { min: 0 })}${n('price_out_batch_per_mtok', 'Batch output', '', { min: 0 })} + ${n('price_cache_read_per_mtok', 'Cached input (read)', '0 = vendor default (Anthropic 0.1x input)', { min: 0 })}${n('price_cache_write_per_mtok', 'Cache write', '0 = vendor default (Anthropic 1.25x input)', { min: 0 })}

Limits

diff --git a/proto/ktms/v1/admin.proto b/proto/ktms/v1/admin.proto index 934259c..f4fdcd6 100644 --- a/proto/ktms/v1/admin.proto +++ b/proto/ktms/v1/admin.proto @@ -30,6 +30,12 @@ message ModelSpec { int32 rpm_burst = 16; // extra burst allowed above steady-state rpm (0 = default burst of rpm - 1, <0 = burst disabled) int32 queue_timeout_ms = 17; // max wait for pool inference slot in ms (0 = fallback immediately; capped to 250ms for interactive non-svc users) double budget_grace_margin = 18; // allowed daily budget overdraft (0.0 = system default 0.05, <0.0 = disabled) + // Prompt-cache rates, standard (off-peak) like price_in_per_mtok; peak and batch + // calls scale them by the same ratio as input. 0 = the vendor default: for + // anthropic 0.1x input to read and 1.25x to write, for bedrock 0.25x and 1.25x, + // for every other vendor 1x. + double price_cache_read_per_mtok = 19; + double price_cache_write_per_mtok = 20; } message RuleSpec { @@ -104,8 +110,29 @@ message MintAPIKeyRequest { repeated string scopes = 2; // "generate" | "admin" string org_id = 3; string team_id = 4; + APIKeyPolicy policy = 5; } +// APIKeyPolicy limits one client key, on top of its team's limits and each +// model's rpm. Refusals: PERMISSION_DENIED for a model outside allowed_models, +// RESOURCE_EXHAUSTED throttled:key_rpm / throttled:key_tpm for the rates, and +// FAILED_PRECONDITION request_too_large for a call above a minute of tokens. +message APIKeyPolicy { + repeated string allowed_models = 1; // model ids the key may call; empty = any + int32 rpm_limit = 2; // calls per minute over all models, 0 = unlimited + // Tokens per minute over all models, 0 = unlimited. A call books its input + // estimate when admitted and its real input + output when it ends. + int64 tpm_limit = 3; +} + +// SetAPIKeyPolicy replaces a key's policy; it applies from the key's next call. +message SetAPIKeyPolicyRequest { + string name = 1; + APIKeyPolicy policy = 2; +} + +message SetAPIKeyPolicyResponse {} + message MintAPIKeyResponse { string plain_key = 1; // returned exactly once, never stored or logged } @@ -125,6 +152,7 @@ message APIKeyInfo { bool disabled = 4; string org_id = 5; string team_id = 6; + APIKeyPolicy policy = 7; } message ListAPIKeysResponse { @@ -377,6 +405,26 @@ message GetTeamSpendResponse { bool metered = 4; // false: no budget set, or no Redis (spend is not counted) } +// GetSpend reports a key's, end user's or team's spend as the gateway enforces +// it: one window per spend limit period that applies to the subject. +message GetSpendRequest { + string subject_type = 1; // "key" (subject_id = key name) | "user" | "team" + string subject_id = 2; +} + +message SpendWindow { + string period = 1; // "day" | "month" (UTC) + int64 spent_nanousd = 2; + int64 limit_nanousd = 3; // 0 = no cap + int64 remaining_nanousd = 4; // limit - spent, never below 0; 0 without a cap + google.protobuf.Timestamp resets_at = 5; + bool metered = 6; // false: no cap (spend is counted only under a cap), or no Redis +} + +message GetSpendResponse { + repeated SpendWindow windows = 1; +} + // All admin RPCs require the "admin" scope. Every config mutation validates // the resulting full config in one transaction and bumps the config version. service AdminService { @@ -389,6 +437,7 @@ service AdminService { rpc MintAPIKey(MintAPIKeyRequest) returns (MintAPIKeyResponse); rpc RevokeAPIKey(RevokeAPIKeyRequest) returns (RevokeAPIKeyResponse); rpc ListAPIKeys(ListAPIKeysRequest) returns (ListAPIKeysResponse); + rpc SetAPIKeyPolicy(SetAPIKeyPolicyRequest) returns (SetAPIKeyPolicyResponse); rpc RotateEncryption(RotateEncryptionRequest) returns (RotateEncryptionResponse); rpc MintNodeKey(MintNodeKeyRequest) returns (MintNodeKeyResponse); rpc RevokeNodeKey(RevokeNodeKeyRequest) returns (RevokeNodeKeyResponse); @@ -411,4 +460,5 @@ service AdminService { rpc UpdateTeam(UpdateTeamRequest) returns (UpdateTeamResponse); rpc DeleteTeam(DeleteTeamRequest) returns (DeleteTeamResponse); rpc GetTeamSpend(GetTeamSpendRequest) returns (GetTeamSpendResponse); + rpc GetSpend(GetSpendRequest) returns (GetSpendResponse); } diff --git a/proto/ktms/v1/gateway.proto b/proto/ktms/v1/gateway.proto index 94ca09f..23d8b6a 100644 --- a/proto/ktms/v1/gateway.proto +++ b/proto/ktms/v1/gateway.proto @@ -169,6 +169,9 @@ message GenerateTextRequest { message TokensUsage { int64 input = 1; int64 output = 2; + // Parts of input read from and written to the vendor's prompt cache. + int64 cache_read = 3; + int64 cache_write = 4; } message Choice { @@ -219,6 +222,9 @@ message ModelInfo { double price_out_per_mtok = 7; double price_in_batch_per_mtok = 8; double price_out_batch_per_mtok = 9; + // Effective prompt-cache rates, vendor defaults applied. + double price_cache_read_per_mtok = 10; + double price_cache_write_per_mtok = 11; } message ListModelsResponse { @@ -237,14 +243,15 @@ message GenerateTextStreamRequest { // paused every model in the chain with Retry-After, "vendors_unavailable" // carries RetryInfo with the soonest one frees up: wait that long first. // RESOURCE_EXHAUSTED = throttled ("throttled:rpm" | "throttled:budget_key" | -// "throttled:budget_user") — back off, do not hot-retry; or a spend limit +// "throttled:budget_user"), or the key's own rate ("throttled:key_rpm" | +// "throttled:key_tpm", with RetryInfo) — back off, do not hot-retry; or a spend limit // ("throttled:spend__", "throttled:spend_team_monthly") // with ErrorInfo "insufficient_quota" and RetryInfo until the UTC period resets. -// PERMISSION_DENIED: the key's team may not use the model ("model \"\" not -// allowed for team", "no models in chain allowed for team", "model access -// denied by team policy"); do not retry. +// PERMISSION_DENIED: the key or its team may not use the model ("model \"\" +// not allowed for ", "no models in chain allowed for ", +// "model access denied by key or team policy" for batches); do not retry. // FAILED_PRECONDITION: "output_truncated" (raise max_output_tokens, do not -// retry as-is), "no_capable_model", "no_vendor_key", "not_configured", +// retry as-is), "request_too_large" (above a minute of the key's tpm_limit), "no_capable_model", "no_vendor_key", "not_configured", // "batch not configured". // INVALID_ARGUMENT: "guardrail_violation" (PII or prompt injection detected), // "bad_request: " (validation errors, empty batch, duplicate custom_id). diff --git a/scripts/e2e.refs b/scripts/e2e.refs index 69025d5..5b20113 100644 --- a/scripts/e2e.refs +++ b/scripts/e2e.refs @@ -1,4 +1,4 @@ # Refs scripts/e2e.sh tests against unless KTMS_REF / TOKENMETER_REF are set. -# Switch to the release tags (v1.0.0) once they exist. -E2E_KTMS_REF=main -E2E_TOKENMETER_REF=main +# The 1.0 release branches until the v1.0.0 tags exist, then the tags. +E2E_KTMS_REF=release/v1.0.0 +E2E_TOKENMETER_REF=release/v1.0.0 diff --git a/scripts/e2e.sh b/scripts/e2e.sh index bcb2522..17ccf8b 100755 --- a/scripts/e2e.sh +++ b/scripts/e2e.sh @@ -5,8 +5,8 @@ set -eu cd "$(dirname "$0")/.." -# The tested pair: main until 1.0 is tagged, then the release tags, so a pull -# request is not failed by an unrelated upstream change. The daily run uses main. +# The tested pair comes from e2e.refs, so a pull request is not failed by an +# unrelated upstream change. The daily run uses main. . ./scripts/e2e.refs KTMS_REF=${KTMS_REF:-$E2E_KTMS_REF} TOKENMETER_REF=${TOKENMETER_REF:-$E2E_TOKENMETER_REF} @@ -134,6 +134,15 @@ api GET /api/proxy/keys | jq -e '.keys | map(.name) | index("e2e-client")' >/dev api PUT /api/proxy/spend-limits '{"subjectType":"key","subjectId":"e2e-client","period":"day","limitNanousd":"5000000000"}' >/dev/null || fail "set spend limit" api GET /api/proxy/spend-limits | jq -e '.spend_limits | map(.subject_id) | index("e2e-client")' >/dev/null || fail "spend limit not listed: $(api GET /api/proxy/spend-limits)" +api PUT /api/proxy/keys/e2e-client/policy '{"allowedModels":["mock-gpt"],"rpmLimit":60,"tpmLimit":"100000"}' >/dev/null || fail "set key policy" +api GET /api/proxy/keys | jq -e '.keys[] | select(.name == "e2e-client") | .policy.rpm_limit == 60 and .policy.allowed_models == ["mock-gpt"]' >/dev/null || + fail "key policy not listed: $(api GET /api/proxy/keys)" +i=0 +until api GET "/api/proxy/spend?subject_type=key&subject_id=e2e-client" 2>/dev/null | jq -e '.windows[] | select(.period == "day") | .limit_nanousd == "5000000000"' >/dev/null; do + i=$((i + 1)) + [ "$i" -gt 15 ] && fail "spend of the key: $(api GET "/api/proxy/spend?subject_type=key&subject_id=e2e-client")" + sleep 1 +done ORG=$(api POST /api/proxy/organizations '{"name":"e2e-org"}' | jq -r .organization.id) || true [ -n "$ORG" ] && [ "$ORG" != null ] || fail "create organization" api POST /api/proxy/teams '{"orgId":"'"$ORG"'","name":"e2e-team","monthlyBudgetUsd":10,"allowedModels":["mock-gpt"]}' >/dev/null || fail "create team" @@ -141,7 +150,7 @@ api GET /api/proxy/teams | jq -e '.teams | map(.name) | index("e2e-team")' >/dev api DELETE /api/proxy/keys/e2e-client >/dev/null || fail "revoke client key" api GET /api/proxy/keys | jq -e '.keys | map(select(.name == "e2e-client" and (.disabled | not))) | length == 0' >/dev/null || fail "the revoked key is still active: $(api GET /api/proxy/keys)" -echo "e2e: keys, spend limits, organizations and teams work" +echo "e2e: keys, key policies, spend limits and spend, organizations and teams work" # Before the call: a run that crosses midnight UTC still finds the event. SINCE=$(date -u +%F) From c09be89952a420b48a891f64b093be097d02d3a7 Mon Sep 17 00:00:00 2001 From: Vasil Date: Fri, 23 Oct 2026 18:05:59 +0200 Subject: [PATCH 2/2] fix(keys): key spend from one bounded endpoint; model choices follow the team GET /api/proxy/key-spend reads every visible active key's spend at most eight at a time, with caps from GetSpend, so wildcard caps show and team leads see their keys' spend; an unreadable key says unknown. The policy dialog offers the team's allowed models when it has a list, else the gateway's, else a text field. --- README.md | 4 +- internal/api/api.go | 1 + internal/api/keypolicy_test.go | 32 +++++++++++++++ internal/api/proxy.go | 55 +++++++++++++++++++++++--- internal/gen/ktms/v1/admin.pb.go | 14 +++---- internal/web/static/js/pages/keys.js | 58 +++++++++++++--------------- proto/ktms/v1/admin.proto | 14 +++---- scripts/e2e.sh | 4 +- 8 files changed, 126 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 62bcda0..b36e438 100644 --- a/README.md +++ b/README.md @@ -318,8 +318,8 @@ and users. **Keys.** A wizard mints client API keys: name and team, access, optional daily and monthly spend caps and request and token rates; the secret is shown once with a copy button. Each key's policy (allowed models, -requests and tokens per minute) is edited in place, and its spend shows against its caps as KTMS counts it; -team leads do both for their team's keys. Revoke keys, assign vendor credentials to clients (write-only: +requests and tokens per minute) is edited in place, and its spend shows against its caps as KTMS counts it, +wildcard caps included; team leads see and edit their team's keys. Revoke keys, assign vendor credentials to clients (write-only: stored encrypted by the gateway and never shown again) and re-encrypt all credentials after adding a new encryption key version. diff --git a/internal/api/api.go b/internal/api/api.go index f260380..4276cfc 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -112,6 +112,7 @@ func (s *Server) Handler() http.Handler { api("DELETE /api/proxy/keys/{name}", team, s.proxyRevokeKey) api("PUT /api/proxy/keys/{name}/policy", team, s.proxySetKeyPolicy) api("GET /api/proxy/spend", team, s.proxySpend) + api("GET /api/proxy/key-spend", team, s.proxyKeysSpend) api("GET /api/proxy/nodes", globalRead, s.proxyListNodes) api("POST /api/proxy/nodes/{id}/drain", adminOnly, s.proxyDrainNode) api("GET /api/proxy/node-keys", globalRead, s.proxyListNodeKeys) diff --git a/internal/api/keypolicy_test.go b/internal/api/keypolicy_test.go index 478c5bd..22d34ed 100644 --- a/internal/api/keypolicy_test.go +++ b/internal/api/keypolicy_test.go @@ -2,8 +2,10 @@ package api import ( "context" + "encoding/json" "net/http" "strings" + "sync" "testing" "google.golang.org/grpc" @@ -11,13 +13,17 @@ import ( pb "github.com/ai-process/ktms-admin/internal/gen/ktms/v1" ) +var spendMu sync.Mutex + func (f *fakeAdmin) SetAPIKeyPolicy(_ context.Context, r *pb.SetAPIKeyPolicyRequest, _ ...grpc.CallOption) (*pb.SetAPIKeyPolicyResponse, error) { f.lastPolicy = r return &pb.SetAPIKeyPolicyResponse{}, nil } func (f *fakeAdmin) GetSpend(_ context.Context, r *pb.GetSpendRequest, _ ...grpc.CallOption) (*pb.GetSpendResponse, error) { + spendMu.Lock() // key-spend calls it concurrently f.lastSpend = r + spendMu.Unlock() return &pb.GetSpendResponse{Windows: []*pb.SpendWindow{{Period: "day", SpentNanousd: 1e9, LimitNanousd: 5e9, RemainingNanousd: 4e9, Metered: true}}}, nil } @@ -62,3 +68,29 @@ func TestSpendAccess(t *testing.T) { t.Fatalf("viewer = %d %s", w.Code, w.Body) } } + +func TestKeySpendIsScopedAndBounded(t *testing.T) { + h, fa := tenantServer(t) + fa.keys = append(fa.keys, &pb.APIKeyInfo{Name: "old-search", TeamId: "team-1", Disabled: true}) + var got struct { + Keys map[string]struct { + Windows []struct { + Period string `json:"period"` + } `json:"windows"` + } `json:"keys"` + } + w := call(h, "GET", "/api/proxy/key-spend", "", sessionCookie(t, "lead@corp.com"), nil) + if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil || w.Code != http.StatusOK { + t.Fatalf("lead = %d %s", w.Code, w.Body) + } + if _, ok := got.Keys["search-service"]; !ok || len(got.Keys) != 1 { + t.Fatalf("lead sees %v, want only the team's active key", got.Keys) + } + if got.Keys["search-service"].Windows[0].Period != "day" { + t.Fatalf("windows = %v", got.Keys["search-service"].Windows) + } + w = call(h, "GET", "/api/proxy/key-spend", "", sessionCookie(t, "boss@x.com"), nil) + if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil || len(got.Keys) != 3 { + t.Fatalf("admin sees %v (%v), want the three active keys", got.Keys, err) + } +} diff --git a/internal/api/proxy.go b/internal/api/proxy.go index aa7a7c2..699f1e6 100644 --- a/internal/api/proxy.go +++ b/internal/api/proxy.go @@ -130,7 +130,8 @@ func (s *Server) ownsKey(ctx context.Context, u *auth.User, name string) (bool, } // proxySetKeyPolicy replaces a key's allowed models and rates. A team lead may -// set them on the team's keys: they narrow what the team allows, never widen it. +// set them on the team's keys, as they may mint keys without any: a key's policy +// only narrows its team's models and budget. func (s *Server) proxySetKeyPolicy(w http.ResponseWriter, r *http.Request, u *auth.User) { var policy pb.APIKeyPolicy if err := readProto(r, &policy); err != nil { @@ -141,10 +142,6 @@ func (s *Server) proxySetKeyPolicy(w http.ResponseWriter, r *http.Request, u *au ctx, cancel := context.WithTimeout(r.Context(), rpcTimeout) defer cancel() if !u.IsSuperAdmin() { - if !u.IsTeamLead() { - writeErr(w, http.StatusForbidden, "forbidden", "only team leads and admins set key policies") - return - } ok, err := s.ownsKey(ctx, u, name) if err != nil { backendErr(w, err) @@ -990,6 +987,54 @@ func countersDown(err error) bool { return c == codes.Unavailable || c == codes.DeadlineExceeded } +// proxyKeysSpend reads the spend of every active key the user may see in one +// browser request, at most maxSpendReads at a time against the gateway. Caps +// come from GetSpend itself, so a wildcard cap counts like a key's own. +func (s *Server) proxyKeysSpend(w http.ResponseWriter, r *http.Request, u *auth.User) { + ctx, cancel := context.WithTimeout(r.Context(), rpcTimeout) + defer cancel() + resp, err := s.admin.ListAPIKeys(ctx, &pb.ListAPIKeysRequest{}) + if err != nil { + backendErr(w, err) + return + } + keys := resp.Keys + if !u.CanReadGlobal() { + keys = keysOfTeam(keys, u.TeamID) + } + out := map[string]any{} + sem := make(chan struct{}, maxSpendReads) + var ( + wg sync.WaitGroup + mu sync.Mutex + ) + for _, k := range keys { + if k.Disabled { + continue + } + wg.Add(1) + go func() { + defer wg.Done() + sem <- struct{}{} + defer func() { <-sem }() + spend, err := s.admin.GetSpend(ctx, &pb.GetSpendRequest{SubjectType: "key", SubjectId: k.Name}) + var row any = map[string]any{"error": "unavailable"} + if err == nil { + if v, encErr := protoValue(spend); encErr == nil { + row = v + } + } else { + log.Warn().Err(err).Str("key", k.Name).Msg("key spend unavailable") + } + mu.Lock() + out[k.Name] = row + mu.Unlock() + }() + } + wg.Wait() + writeJSON(w, http.StatusOK, map[string]any{"keys": out}) +} + // maxSpendReads bounds the GetTeamSpend calls one Usage page load can cause. const maxSpendReads = 8 diff --git a/internal/gen/ktms/v1/admin.pb.go b/internal/gen/ktms/v1/admin.pb.go index 757590b..3339214 100644 --- a/internal/gen/ktms/v1/admin.pb.go +++ b/internal/gen/ktms/v1/admin.pb.go @@ -46,10 +46,9 @@ type ModelSpec struct { RpmBurst int32 `protobuf:"varint,16,opt,name=rpm_burst,json=rpmBurst,proto3" json:"rpm_burst,omitempty"` // extra burst allowed above steady-state rpm (0 = default burst of rpm - 1, <0 = burst disabled) QueueTimeoutMs int32 `protobuf:"varint,17,opt,name=queue_timeout_ms,json=queueTimeoutMs,proto3" json:"queue_timeout_ms,omitempty"` // max wait for pool inference slot in ms (0 = fallback immediately; capped to 250ms for interactive non-svc users) BudgetGraceMargin float64 `protobuf:"fixed64,18,opt,name=budget_grace_margin,json=budgetGraceMargin,proto3" json:"budget_grace_margin,omitempty"` // allowed daily budget overdraft (0.0 = system default 0.05, <0.0 = disabled) - // Prompt-cache rates, standard (off-peak) like price_in_per_mtok; peak and batch - // calls scale them by the same ratio as input. 0 = the vendor default: for - // anthropic 0.1x input to read and 1.25x to write, for bedrock 0.25x and 1.25x, - // for every other vendor 1x. + // Prompt-cache rates at the standard rate, scaled like input for peak and batch. + // 0 = a vendor default that never undercharges: anthropic 0.1x input to read + // and 1.25x to write, bedrock 0.25x and 1.25x, any other vendor 1x. PriceCacheReadPerMtok float64 `protobuf:"fixed64,19,opt,name=price_cache_read_per_mtok,json=priceCacheReadPerMtok,proto3" json:"price_cache_read_per_mtok,omitempty"` PriceCacheWritePerMtok float64 `protobuf:"fixed64,20,opt,name=price_cache_write_per_mtok,json=priceCacheWritePerMtok,proto3" json:"price_cache_write_per_mtok,omitempty"` unknownFields protoimpl.UnknownFields @@ -1033,10 +1032,9 @@ func (x *MintAPIKeyRequest) GetPolicy() *APIKeyPolicy { return nil } -// APIKeyPolicy limits one client key, on top of its team's limits and each -// model's rpm. Refusals: PERMISSION_DENIED for a model outside allowed_models, -// RESOURCE_EXHAUSTED throttled:key_rpm / throttled:key_tpm for the rates, and -// FAILED_PRECONDITION request_too_large for a call above a minute of tokens. +// APIKeyPolicy limits one key on top of its team and each model's rpm. Refusals: +// PERMISSION_DENIED (model not allowed), RESOURCE_EXHAUSTED throttled:key_rpm | +// key_tpm, FAILED_PRECONDITION request_too_large (above a minute of tokens). type APIKeyPolicy struct { state protoimpl.MessageState `protogen:"open.v1"` AllowedModels []string `protobuf:"bytes,1,rep,name=allowed_models,json=allowedModels,proto3" json:"allowed_models,omitempty"` // model ids the key may call; empty = any diff --git a/internal/web/static/js/pages/keys.js b/internal/web/static/js/pages/keys.js index f8a10fa..4d8aabe 100644 --- a/internal/web/static/js/pages/keys.js +++ b/internal/web/static/js/pages/keys.js @@ -1,4 +1,4 @@ -import { html, useState, useMemo, api, useAsync, openDialog, confirmAction, toast, FormDialog, PageHead, Card, Table, Field, Badge, LoadError, when, usd, num, cx } from '../lib.js'; +import { html, useState, useMemo, api, useAsync, openDialog, confirmAction, toast, FormDialog, PageHead, Card, Table, Field, Badge, LoadError, when, usd, num, cx, compact } from '../lib.js'; import { isAdmin, isTeamLead, canReadGlobal, session, loadCfg } from '../state.js'; import { VENDORS } from './models.js'; @@ -10,27 +10,18 @@ const nanoUSD = (v) => num(v) / 1e9; export function KeysPage({ params }) { const st = useAsync(async () => { - const [keys, teams, limits, cfg] = await Promise.all([ + const [keys, teams, spend, cfg] = await Promise.all([ api('GET', '/api/proxy/keys'), api('GET', '/api/proxy/teams').catch(() => ({ teams: [] })), - canReadGlobal() ? api('GET', '/api/proxy/spend-limits?subject_type=key').catch((error) => ({ error })) : { spend_limits: [] }, + api('GET', '/api/proxy/key-spend').catch((error) => ({ error })), canReadGlobal() ? loadCfg(true).catch((error) => ({ error })) : null, ]); - // Where a key is capped, what it has spent so far; a failure shows as unknown. - const capped = [...new Set((limits.spend_limits || []).map((l) => l.subject_id).filter((id) => id !== '*'))]; - const spend = Object.fromEntries(await Promise.all(capped.map((id) => - api('GET', `/api/proxy/spend?subject_type=key&subject_id=${encodeURIComponent(id)}`).then((r) => [id, r.windows || []], () => [id, null])))); // Side panels fail on their own: the key list still shows, and an // unreadable panel says so instead of looking empty. - return { keys: keys.keys || [], teams: teams.teams || [], limits: limits.spend_limits || [], limitsError: limits.error, spend, + return { keys: keys.keys || [], teams: teams.teams || [], spend: spend.keys || {}, spendError: spend.error, vendorKeys: cfg?.vendor_keys || [], models: (cfg?.models || []).map((m) => m.id), cfgError: cfg?.error }; }, []); const teamName = useMemo(() => Object.fromEntries((st.data?.teams || []).map((t) => [t.id, t.name])), [st.data]); - const caps = useMemo(() => { - const m = {}; - for (const l of st.data?.limits || []) (m[l.subject_id] ||= []).push(l); - return m; - }, [st.data]); const canMint = isAdmin() || isTeamLead(); const revoke = (k) => confirmAction({ @@ -44,7 +35,7 @@ export function KeysPage({ params }) {
${k.name}${k.team_id ? 'team ' + (teamName[k.team_id] || k.team_id.slice(0, 8) + '…') : 'no team'}
${(k.scopes || []).map((s) => html`${Object.fromEntries(SCOPES.map(([id, t]) => [id, t]))[s] || s}`)}
<${PolicyChips} policy=${k.policy} /> - ${canReadGlobal() && html`${(caps[k.name] || []).length ? html`
${caps[k.name].map((l) => html`${spentText(st.data?.spend?.[k.name], l)}`)}
` : html`${st.data?.limitsError ? 'unknown' : '—'}`}`} + <${SpendChips} spend=${k.disabled ? {} : st.data?.spend?.[k.name]} failed=${!!st.data?.spendError} /> ${when(k.created_at)} ${k.disabled ? html`<${Badge}>Revoked` : html`<${Badge} tone="ok">Active`} ${canMint && !k.disabled && html``} @@ -57,13 +48,13 @@ export function KeysPage({ params }) {
<${Card} flush label="API keys"> <${Table} loading=${st.loading} rows=${rows} - head=${['Name', 'Access', 'Policy', ...(canReadGlobal() ? ['Spend today / this month'] : []), 'Created', 'Status', { label: 'Actions', sr: true }]} + head=${['Name', 'Access', 'Policy', 'Spend against caps', 'Created', 'Status', { label: 'Actions', sr: true }]} empty=${{ title: 'No API keys yet', children: canMint ? 'Create one with the form.' : 'Ask a team lead for one.' }} />
${canMint && html`
<${Wizard} teams=${st.data?.teams || []} team=${params.get('team_id') || ''} onDone=${st.reload} />
`}
- ${st.data?.limitsError && html`
Spend caps could not be read: ${st.data.limitsError.message}
`} + ${st.data?.spendError && html`
Key spend could not be read: ${st.data.spendError.message}
`} ${canReadGlobal() && html`<${VendorKeys} vendorKeys=${st.data?.vendorKeys || []} error=${st.data?.cfgError} keys=${keys} loading=${st.loading} reload=${st.reload} />`} `; } @@ -146,29 +137,33 @@ function Wizard({ teams, team, onDone }) { `; } -// spentText shows a cap with what the key has spent against it, as KTMS counts it. -function spentText(windows, limit) { - const w = (windows || []).find((x) => x.period === limit.period); - const cap = `${usd(nanoUSD(limit.limit_nanousd))} / ${limit.period}`; - return w?.metered ? `${usd(nanoUSD(w.spent_nanousd))} of ${cap}` : cap; +// SpendChips shows each capped period as KTMS counts it: spent of the cap, or +// only the cap when nothing is counted (no Redis), or unknown when unreadable. +function SpendChips({ spend, failed }) { + if (failed || spend?.error) return html`unknown`; + const capped = (spend?.windows || []).filter((w) => num(w.limit_nanousd) > 0); + if (!capped.length) return html`—`; + return html`
${capped.map((w) => { + const cap = `${usd(nanoUSD(w.limit_nanousd))} / ${w.period}`; + return html`${w.metered ? `${usd(nanoUSD(w.spent_nanousd))} of ${cap}` : cap}`; + })}
`; } function PolicyChips({ policy }) { const p = policy || {}; const chips = []; if ((p.allowed_models || []).length) chips.push(html`${p.allowed_models.length === 1 ? p.allowed_models[0] : p.allowed_models.length + ' models'}`); - if (num(p.rpm_limit) > 0) chips.push(html`${compactNum(p.rpm_limit)} rpm`); - if (num(p.tpm_limit) > 0) chips.push(html`${compactNum(p.tpm_limit)} tpm`); + if (num(p.rpm_limit) > 0) chips.push(html`${compact(num(p.rpm_limit))} rpm`); + if (num(p.tpm_limit) > 0) chips.push(html`${compact(num(p.tpm_limit))} tpm`); return chips.length ? html`
${chips}
` : html`—`; } -const compactNum = (v) => new Intl.NumberFormat('en', { notation: 'compact' }).format(num(v)); - -// modelChoices lists the models a key's allowlist can name: the gateway's for -// admins, the team's allowed models for a team lead, plus whatever it names now. +// modelChoices lists the models a key's allowlist can usefully name: its team's +// allowed models when the team has a list (KTMS intersects the two), else the +// gateway's, plus whatever it names now. Empty means type the ids. function modelChoices(data, k) { const team = (data?.teams || []).find((t) => t.id === k.team_id); - const base = data?.models?.length ? data.models : (team?.allowed_models || []); + const base = team?.allowed_models?.length ? team.allowed_models : (data?.models || []); return [...new Set([...base, ...(k.policy?.allowed_models || [])])].sort(); } @@ -178,15 +173,16 @@ function PolicyForm({ k, models, close }) { const submit = async (fd) => { const rpm = num(fd.get('rpm')), tpm = num(fd.get('tpm')); if (![rpm, tpm].every((v) => Number.isInteger(v) && v >= 0)) throw new Error('Rates are whole numbers, 0 for no limit.'); - await api('PUT', `/api/proxy/keys/${encodeURIComponent(k.name)}/policy`, { allowed_models: picked, rpm_limit: rpm, tpm_limit: String(tpm) }); + const allowed = models.length ? picked : String(fd.get('models') || '').split(',').map((m) => m.trim()).filter(Boolean); + await api('PUT', `/api/proxy/keys/${encodeURIComponent(k.name)}/policy`, { allowed_models: allowed, rpm_limit: rpm, tpm_limit: String(tpm) }); toast('Policy saved'); }; return html`<${FormDialog} title=${'Policy of ' + k.name} sub="Applies from the key's next call, on top of its team's models and each model's limits." close=${close} onSubmit=${submit}>
Allowed models ${models.length ? html`
${models.map((m) => html``)}
` - : html`

No models to choose from.

`} -

${picked.length ? 'Only these, and only where the team allows them too.' : 'None picked: any model the team allows.'}

+ : html`<${Field} label="Model ids" hint="Comma-separated; empty allows any model the team allows.">`} + ${models.length > 0 && html`

${picked.length ? 'Only these, and only where the team allows them too.' : 'None picked: any model the team allows.'}

`}
<${Field} label="Requests per minute" hint="Over all models; 0 = no limit."> diff --git a/proto/ktms/v1/admin.proto b/proto/ktms/v1/admin.proto index f4fdcd6..c9ee402 100644 --- a/proto/ktms/v1/admin.proto +++ b/proto/ktms/v1/admin.proto @@ -30,10 +30,9 @@ message ModelSpec { int32 rpm_burst = 16; // extra burst allowed above steady-state rpm (0 = default burst of rpm - 1, <0 = burst disabled) int32 queue_timeout_ms = 17; // max wait for pool inference slot in ms (0 = fallback immediately; capped to 250ms for interactive non-svc users) double budget_grace_margin = 18; // allowed daily budget overdraft (0.0 = system default 0.05, <0.0 = disabled) - // Prompt-cache rates, standard (off-peak) like price_in_per_mtok; peak and batch - // calls scale them by the same ratio as input. 0 = the vendor default: for - // anthropic 0.1x input to read and 1.25x to write, for bedrock 0.25x and 1.25x, - // for every other vendor 1x. + // Prompt-cache rates at the standard rate, scaled like input for peak and batch. + // 0 = a vendor default that never undercharges: anthropic 0.1x input to read + // and 1.25x to write, bedrock 0.25x and 1.25x, any other vendor 1x. double price_cache_read_per_mtok = 19; double price_cache_write_per_mtok = 20; } @@ -113,10 +112,9 @@ message MintAPIKeyRequest { APIKeyPolicy policy = 5; } -// APIKeyPolicy limits one client key, on top of its team's limits and each -// model's rpm. Refusals: PERMISSION_DENIED for a model outside allowed_models, -// RESOURCE_EXHAUSTED throttled:key_rpm / throttled:key_tpm for the rates, and -// FAILED_PRECONDITION request_too_large for a call above a minute of tokens. +// APIKeyPolicy limits one key on top of its team and each model's rpm. Refusals: +// PERMISSION_DENIED (model not allowed), RESOURCE_EXHAUSTED throttled:key_rpm | +// key_tpm, FAILED_PRECONDITION request_too_large (above a minute of tokens). message APIKeyPolicy { repeated string allowed_models = 1; // model ids the key may call; empty = any int32 rpm_limit = 2; // calls per minute over all models, 0 = unlimited diff --git a/scripts/e2e.sh b/scripts/e2e.sh index 17ccf8b..eace462 100755 --- a/scripts/e2e.sh +++ b/scripts/e2e.sh @@ -138,9 +138,9 @@ api PUT /api/proxy/keys/e2e-client/policy '{"allowedModels":["mock-gpt"],"rpmLim api GET /api/proxy/keys | jq -e '.keys[] | select(.name == "e2e-client") | .policy.rpm_limit == 60 and .policy.allowed_models == ["mock-gpt"]' >/dev/null || fail "key policy not listed: $(api GET /api/proxy/keys)" i=0 -until api GET "/api/proxy/spend?subject_type=key&subject_id=e2e-client" 2>/dev/null | jq -e '.windows[] | select(.period == "day") | .limit_nanousd == "5000000000"' >/dev/null; do +until api GET /api/proxy/key-spend 2>/dev/null | jq -e '.keys["e2e-client"].windows[] | select(.period == "day") | .limit_nanousd == "5000000000"' >/dev/null; do i=$((i + 1)) - [ "$i" -gt 15 ] && fail "spend of the key: $(api GET "/api/proxy/spend?subject_type=key&subject_id=e2e-client")" + [ "$i" -gt 15 ] && fail "spend of the key: $(api GET /api/proxy/key-spend)" sleep 1 done ORG=$(api POST /api/proxy/organizations '{"name":"e2e-org"}' | jq -r .organization.id) || true