From 239eb6645ed146a60862cf63fad48cc55d264f6a Mon Sep 17 00:00:00 2001 From: Vasil Date: Sat, 24 Oct 2026 15:37:56 +0200 Subject: [PATCH] chore(release): 1.1.0 --- CHANGELOG.md | 6 + README.md | 2 +- SECURITY.md | 2 +- internal/gen/ktms/v1/admin.pb.go | 566 +++++++++++++++++++++----- internal/gen/ktms/v1/admin_grpc.pb.go | 54 ++- internal/gen/ktms/v1/node.pb.go | 42 +- proto/ktms/v1/admin.proto | 56 ++- proto/ktms/v1/node.proto | 7 +- scripts/e2e.refs | 4 +- 9 files changed, 619 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fdbf6d..9eea16d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,18 @@ Notable changes to ktms-admin. The format follows ## [Unreleased] +## [1.1.0] - 2026-10-24 + ### Added - `KTMS_READ_KEY`: an optional Keystone key with the `admin:read` scope (Keystone 1.2+). Admin API reads and every call of a viewer go with it; changes keep `KTMS_ADMIN_KEY`. The sidebar's health shows whether each key is accepted for its job: the admin key may change Keystone, the read key may read and nothing more. +### Changed +- Tested against KTMS 1.2.0 and light-tokenmeter 1.1.0 (`scripts/e2e.refs`); the `ktms.v1` copy + follows KTMS 1.2.0. Every KTMS 1.x release still works. + ## [1.0.0] - 2026-10-23 The first stable release, paired with Keystone. The Model Switchboard. (KTMS) 1.0: ktms-admin diff --git a/README.md b/README.md index 49afc0d..571caf2 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ The console's own `/api` serves its web UI only and is not a public API. | ktms-admin | KTMS gateway | light-tokenmeter | | --- | --- | --- | -| 1.x | 1.x | 1.x; with its authentication on, set `TOKENMETER_TOKEN` to a `read` token | +| 1.x | 1.x; `KTMS_READ_KEY` needs 1.2 or later | 1.x; with its authentication on, set `TOKENMETER_TOKEN` to a `read` token | | 0.4.x | 0.14.x | 0.1.x | | 0.3.x and earlier | 0.13.x | 0.1.x | diff --git a/SECURITY.md b/SECURITY.md index 5752e1a..1fa9c36 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,7 +19,7 @@ From 1.0.0 on, images are signed with keyless [Sigstore cosign](https://docs.sig SBOM and SLSA provenance. Use cosign 3 and pin the identity to the tag you verify: ```bash -V=v1.0.0 +V=v1.1.0 cosign verify coyl/ktms-admin:${V#v} \ --certificate-identity "https://github.com/ai-process/ktms-admin/.github/workflows/docker-publish.yml@refs/tags/$V" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com diff --git a/internal/gen/ktms/v1/admin.pb.go b/internal/gen/ktms/v1/admin.pb.go index 3339214..2712034 100644 --- a/internal/gen/ktms/v1/admin.pb.go +++ b/internal/gen/ktms/v1/admin.pb.go @@ -9,6 +9,7 @@ package ktmsv1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" @@ -769,11 +770,14 @@ func (*DeleteVendorKeyResponse) Descriptor() ([]byte, []int) { } type VendorKeyInfo struct { - state protoimpl.MessageState `protogen:"open.v1"` - ApiKeyName string `protobuf:"bytes,1,opt,name=api_key_name,json=apiKeyName,proto3" json:"api_key_name,omitempty"` - Vendor string `protobuf:"bytes,2,opt,name=vendor,proto3" json:"vendor,omitempty"` - KeyVersion int32 `protobuf:"varint,3,opt,name=key_version,json=keyVersion,proto3" json:"key_version,omitempty"` // keyring version used to encrypt - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ApiKeyName string `protobuf:"bytes,1,opt,name=api_key_name,json=apiKeyName,proto3" json:"api_key_name,omitempty"` + Vendor string `protobuf:"bytes,2,opt,name=vendor,proto3" json:"vendor,omitempty"` + KeyVersion int32 `protobuf:"varint,3,opt,name=key_version,json=keyVersion,proto3" json:"key_version,omitempty"` // keyring version used to encrypt + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + // First 16 hex characters of the key's SHA-256, so a client can tell whether + // the stored key equals one it holds; empty when the keyring cannot read it. + Fingerprint string `protobuf:"bytes,5,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -836,6 +840,13 @@ func (x *VendorKeyInfo) GetCreatedAt() *timestamppb.Timestamp { return nil } +func (x *VendorKeyInfo) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + type GetConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -959,7 +970,7 @@ func (x *GetConfigResponse) GetSpendLimits() []*SpendLimitSpec { type MintAPIKeyRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Scopes []string `protobuf:"bytes,2,rep,name=scopes,proto3" json:"scopes,omitempty"` // "generate" | "admin" + Scopes []string `protobuf:"bytes,2,rep,name=scopes,proto3" json:"scopes,omitempty"` // "generate" | "admin" | "admin:read" 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"` @@ -2012,7 +2023,7 @@ type NodeInfo struct { NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"` - State string `protobuf:"bytes,4,opt,name=state,proto3" json:"state,omitempty"` // HEALTHY | SUSPECT | DRAINING | CIRCUIT_BROKEN | OFFLINE + State string `protobuf:"bytes,4,opt,name=state,proto3" json:"state,omitempty"` // HEALTHY | SUSPECT | DRAINING | PAUSED | CIRCUIT_BROKEN | OFFLINE Models []string `protobuf:"bytes,5,rep,name=models,proto3" json:"models,omitempty"` EffectiveWeight float64 `protobuf:"fixed64,6,opt,name=effective_weight,json=effectiveWeight,proto3" json:"effective_weight,omitempty"` ActiveRequests int32 `protobuf:"varint,7,opt,name=active_requests,json=activeRequests,proto3" json:"active_requests,omitempty"` @@ -2027,6 +2038,8 @@ type NodeInfo struct { BaseWeight float64 `protobuf:"fixed64,16,opt,name=base_weight,json=baseWeight,proto3" json:"base_weight,omitempty"` Engine string `protobuf:"bytes,17,opt,name=engine,proto3" json:"engine,omitempty"` WarmModels []string `protobuf:"bytes,18,rep,name=warm_models,json=warmModels,proto3" json:"warm_models,omitempty"` + CapacityPercent uint32 `protobuf:"varint,19,opt,name=capacity_percent,json=capacityPercent,proto3" json:"capacity_percent,omitempty"` // current level of the node's schedule + EffectiveSlots int32 `protobuf:"varint,20,opt,name=effective_slots,json=effectiveSlots,proto3" json:"effective_slots,omitempty"` // concurrent requests allowed at that level unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2187,6 +2200,20 @@ func (x *NodeInfo) GetWarmModels() []string { return nil } +func (x *NodeInfo) GetCapacityPercent() uint32 { + if x != nil { + return x.CapacityPercent + } + return 0 +} + +func (x *NodeInfo) GetEffectiveSlots() int32 { + if x != nil { + return x.EffectiveSlots + } + return 0 +} + type ListNodesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Nodes []*NodeInfo `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` @@ -4171,11 +4198,300 @@ func (x *GetSpendResponse) GetWindows() []*SpendWindow { return nil } +type ExplainRouteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ApiKeyName string `protobuf:"bytes,1,opt,name=api_key_name,json=apiKeyName,proto3" json:"api_key_name,omitempty"` // the client key the request would carry + Effort Effort `protobuf:"varint,2,opt,name=effort,proto3,enum=ktms.v1.Effort" json:"effort,omitempty"` // as in GenerateTextRequest; with auto_route, the effort the judge would otherwise pick + AutoRoute bool `protobuf:"varint,3,opt,name=auto_route,json=autoRoute,proto3" json:"auto_route,omitempty"` // as in GenerateTextRequest; without an effort the judge is not called (judge_skipped) + Attributes map[string]string `protobuf:"bytes,4,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // request attributes the rules match on, user_id and canary_key included + Placement string `protobuf:"bytes,5,opt,name=placement,proto3" json:"placement,omitempty"` // nodes_first | nodes_only | cloud_first | cloud_only, empty for the rule's default + ModelOverride string `protobuf:"bytes,6,opt,name=model_override,json=modelOverride,proto3" json:"model_override,omitempty"` // as in GenerateTextRequest + Capabilities []string `protobuf:"bytes,7,rep,name=capabilities,proto3" json:"capabilities,omitempty"` // capabilities the request needs, e.g. "google_search" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExplainRouteRequest) Reset() { + *x = ExplainRouteRequest{} + mi := &file_ktms_v1_admin_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExplainRouteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExplainRouteRequest) ProtoMessage() {} + +func (x *ExplainRouteRequest) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[76] + 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 ExplainRouteRequest.ProtoReflect.Descriptor instead. +func (*ExplainRouteRequest) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{76} +} + +func (x *ExplainRouteRequest) GetApiKeyName() string { + if x != nil { + return x.ApiKeyName + } + return "" +} + +func (x *ExplainRouteRequest) GetEffort() Effort { + if x != nil { + return x.Effort + } + return Effort_EFFORT_UNSPECIFIED +} + +func (x *ExplainRouteRequest) GetAutoRoute() bool { + if x != nil { + return x.AutoRoute + } + return false +} + +func (x *ExplainRouteRequest) GetAttributes() map[string]string { + if x != nil { + return x.Attributes + } + return nil +} + +func (x *ExplainRouteRequest) GetPlacement() string { + if x != nil { + return x.Placement + } + return "" +} + +func (x *ExplainRouteRequest) GetModelOverride() string { + if x != nil { + return x.ModelOverride + } + return "" +} + +func (x *ExplainRouteRequest) GetCapabilities() []string { + if x != nil { + return x.Capabilities + } + return nil +} + +type RouteCandidate struct { + state protoimpl.MessageState `protogen:"open.v1"` + ModelId string `protobuf:"bytes,1,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` + Vendor string `protobuf:"bytes,2,opt,name=vendor,proto3" json:"vendor,omitempty"` + // Why the model was dropped, empty if it stays: not_found, effort, capability, no_vendor_key, + // key_policy, team_policy, placement, circuit_open, vendor_paused (paused_for), + // node_unavailable, node_pool_disabled. New reasons may appear. + Dropped string `protobuf:"bytes,3,opt,name=dropped,proto3" json:"dropped,omitempty"` + PausedFor *durationpb.Duration `protobuf:"bytes,4,opt,name=paused_for,json=pausedFor,proto3" json:"paused_for,omitempty"` // vendor_paused: until the vendor's Retry-After ends + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouteCandidate) Reset() { + *x = RouteCandidate{} + mi := &file_ktms_v1_admin_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouteCandidate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouteCandidate) ProtoMessage() {} + +func (x *RouteCandidate) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[77] + 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 RouteCandidate.ProtoReflect.Descriptor instead. +func (*RouteCandidate) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{77} +} + +func (x *RouteCandidate) GetModelId() string { + if x != nil { + return x.ModelId + } + return "" +} + +func (x *RouteCandidate) GetVendor() string { + if x != nil { + return x.Vendor + } + return "" +} + +func (x *RouteCandidate) GetDropped() string { + if x != nil { + return x.Dropped + } + return "" +} + +func (x *RouteCandidate) GetPausedFor() *durationpb.Duration { + if x != nil { + return x.PausedFor + } + return nil +} + +type ExplainRouteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule string `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` // the matched rule; empty with model_override + Effort string `protobuf:"bytes,2,opt,name=effort,proto3" json:"effort,omitempty"` // the effort matched on + RouteMode string `protobuf:"bytes,3,opt,name=route_mode,json=routeMode,proto3" json:"route_mode,omitempty"` // direct | override | auto-effort | auto-default, as X-KTMS-Route-Mode + Chain []string `protobuf:"bytes,4,rep,name=chain,proto3" json:"chain,omitempty"` // the rule's chain after the canary draw + Candidates []*RouteCandidate `protobuf:"bytes,5,rep,name=candidates,proto3" json:"candidates,omitempty"` // every model of chain, in order + Models []string `protobuf:"bytes,6,rep,name=models,proto3" json:"models,omitempty"` // what a call would try, in order: the kept candidates + Weights []int32 `protobuf:"varint,7,rep,packed,name=weights,proto3" json:"weights,omitempty"` // the rule's canary weights over its use list; empty = none + ShadowModel string `protobuf:"bytes,8,opt,name=shadow_model,json=shadowModel,proto3" json:"shadow_model,omitempty"` // mirrored model, if the rule mirrors and it is callable + ShadowPercentage float64 `protobuf:"fixed64,9,opt,name=shadow_percentage,json=shadowPercentage,proto3" json:"shadow_percentage,omitempty"` + JudgeSkipped bool `protobuf:"varint,10,opt,name=judge_skipped,json=judgeSkipped,proto3" json:"judge_skipped,omitempty"` // auto_route without effort: a real call would ask the judge first + // The code and message a real call would fail with before any vendor, as in the + // error contract, e.g. "FAILED_PRECONDITION: no_vendor_key"; empty if it would go out. + Error string `protobuf:"bytes,11,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExplainRouteResponse) Reset() { + *x = ExplainRouteResponse{} + mi := &file_ktms_v1_admin_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExplainRouteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExplainRouteResponse) ProtoMessage() {} + +func (x *ExplainRouteResponse) ProtoReflect() protoreflect.Message { + mi := &file_ktms_v1_admin_proto_msgTypes[78] + 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 ExplainRouteResponse.ProtoReflect.Descriptor instead. +func (*ExplainRouteResponse) Descriptor() ([]byte, []int) { + return file_ktms_v1_admin_proto_rawDescGZIP(), []int{78} +} + +func (x *ExplainRouteResponse) GetRule() string { + if x != nil { + return x.Rule + } + return "" +} + +func (x *ExplainRouteResponse) GetEffort() string { + if x != nil { + return x.Effort + } + return "" +} + +func (x *ExplainRouteResponse) GetRouteMode() string { + if x != nil { + return x.RouteMode + } + return "" +} + +func (x *ExplainRouteResponse) GetChain() []string { + if x != nil { + return x.Chain + } + return nil +} + +func (x *ExplainRouteResponse) GetCandidates() []*RouteCandidate { + if x != nil { + return x.Candidates + } + return nil +} + +func (x *ExplainRouteResponse) GetModels() []string { + if x != nil { + return x.Models + } + return nil +} + +func (x *ExplainRouteResponse) GetWeights() []int32 { + if x != nil { + return x.Weights + } + return nil +} + +func (x *ExplainRouteResponse) GetShadowModel() string { + if x != nil { + return x.ShadowModel + } + return "" +} + +func (x *ExplainRouteResponse) GetShadowPercentage() float64 { + if x != nil { + return x.ShadowPercentage + } + return 0 +} + +func (x *ExplainRouteResponse) GetJudgeSkipped() bool { + if x != nil { + return x.JudgeSkipped + } + return false +} + +func (x *ExplainRouteResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + 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\"\xcb\x06\n" + + "\x13ktms/v1/admin.proto\x12\aktms.v1\x1a\x1egoogle/protobuf/duration.proto\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" + @@ -4235,7 +4551,7 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "\fapi_key_name\x18\x01 \x01(\tR\n" + "apiKeyName\x12\x16\n" + "\x06vendor\x18\x02 \x01(\tR\x06vendor\"\x19\n" + - "\x17DeleteVendorKeyResponse\"\xa5\x01\n" + + "\x17DeleteVendorKeyResponse\"\xc7\x01\n" + "\rVendorKeyInfo\x12 \n" + "\fapi_key_name\x18\x01 \x01(\tR\n" + "apiKeyName\x12\x16\n" + @@ -4243,7 +4559,8 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "\vkey_version\x18\x03 \x01(\x05R\n" + "keyVersion\x129\n" + "\n" + - "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\x12\n" + + "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12 \n" + + "\vfingerprint\x18\x05 \x01(\tR\vfingerprint\"\x12\n" + "\x10GetConfigRequest\"\xbd\x02\n" + "\x11GetConfigResponse\x12*\n" + "\x06models\x18\x01 \x03(\v2\x12.ktms.v1.ModelSpecR\x06models\x12'\n" + @@ -4315,7 +4632,7 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "\x0fmax_concurrency\x18\t \x01(\x05R\x0emaxConcurrency\"@\n" + "\x14ListNodeKeysResponse\x12(\n" + "\x04keys\x18\x01 \x03(\v2\x14.ktms.v1.NodeKeyInfoR\x04keys\"\x12\n" + - "\x10ListNodesRequest\"\xe0\x04\n" + + "\x10ListNodesRequest\"\xb4\x05\n" + "\bNodeInfo\x12\x17\n" + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1a\n" + @@ -4339,7 +4656,9 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "baseWeight\x12\x16\n" + "\x06engine\x18\x11 \x01(\tR\x06engine\x12\x1f\n" + "\vwarm_models\x18\x12 \x03(\tR\n" + - "warmModels\"<\n" + + "warmModels\x12)\n" + + "\x10capacity_percent\x18\x13 \x01(\rR\x0fcapacityPercent\x12'\n" + + "\x0feffective_slots\x18\x14 \x01(\x05R\x0eeffectiveSlots\"<\n" + "\x11ListNodesResponse\x12'\n" + "\x05nodes\x18\x01 \x03(\v2\x11.ktms.v1.NodeInfoR\x05nodes\"+\n" + "\x10DrainNodeRequest\x12\x17\n" + @@ -4454,7 +4773,44 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "\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" + + "\awindows\x18\x01 \x03(\v2\x14.ktms.v1.SpendWindowR\awindows\"\xf5\x02\n" + + "\x13ExplainRouteRequest\x12 \n" + + "\fapi_key_name\x18\x01 \x01(\tR\n" + + "apiKeyName\x12'\n" + + "\x06effort\x18\x02 \x01(\x0e2\x0f.ktms.v1.EffortR\x06effort\x12\x1d\n" + + "\n" + + "auto_route\x18\x03 \x01(\bR\tautoRoute\x12L\n" + + "\n" + + "attributes\x18\x04 \x03(\v2,.ktms.v1.ExplainRouteRequest.AttributesEntryR\n" + + "attributes\x12\x1c\n" + + "\tplacement\x18\x05 \x01(\tR\tplacement\x12%\n" + + "\x0emodel_override\x18\x06 \x01(\tR\rmodelOverride\x12\"\n" + + "\fcapabilities\x18\a \x03(\tR\fcapabilities\x1a=\n" + + "\x0fAttributesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x97\x01\n" + + "\x0eRouteCandidate\x12\x19\n" + + "\bmodel_id\x18\x01 \x01(\tR\amodelId\x12\x16\n" + + "\x06vendor\x18\x02 \x01(\tR\x06vendor\x12\x18\n" + + "\adropped\x18\x03 \x01(\tR\adropped\x128\n" + + "\n" + + "paused_for\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\tpausedFor\"\xed\x02\n" + + "\x14ExplainRouteResponse\x12\x12\n" + + "\x04rule\x18\x01 \x01(\tR\x04rule\x12\x16\n" + + "\x06effort\x18\x02 \x01(\tR\x06effort\x12\x1d\n" + + "\n" + + "route_mode\x18\x03 \x01(\tR\trouteMode\x12\x14\n" + + "\x05chain\x18\x04 \x03(\tR\x05chain\x127\n" + + "\n" + + "candidates\x18\x05 \x03(\v2\x17.ktms.v1.RouteCandidateR\n" + + "candidates\x12\x16\n" + + "\x06models\x18\x06 \x03(\tR\x06models\x12\x18\n" + + "\aweights\x18\a \x03(\x05R\aweights\x12!\n" + + "\fshadow_model\x18\b \x01(\tR\vshadowModel\x12+\n" + + "\x11shadow_percentage\x18\t \x01(\x01R\x10shadowPercentage\x12#\n" + + "\rjudge_skipped\x18\n" + + " \x01(\bR\fjudgeSkipped\x12\x14\n" + + "\x05error\x18\v \x01(\tR\x05error2\xb7\x14\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" + @@ -4491,7 +4847,8 @@ const file_ktms_v1_admin_proto_rawDesc = "" + "\n" + "DeleteTeam\x12\x1a.ktms.v1.DeleteTeamRequest\x1a\x1b.ktms.v1.DeleteTeamResponse\x12K\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" + + "\bGetSpend\x12\x18.ktms.v1.GetSpendRequest\x1a\x19.ktms.v1.GetSpendResponse\x12K\n" + + "\fExplainRoute\x12\x1c.ktms.v1.ExplainRouteRequest\x1a\x1d.ktms.v1.ExplainRouteResponseB\x94\x01\n" + "\vcom.ktms.v1B\n" + "AdminProtoP\x01Z 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 + 81, // 0: ktms.v1.ModelSpec.efforts:type_name -> ktms.v1.Effort + 81, // 1: ktms.v1.RuleSpec.effort:type_name -> ktms.v1.Effort + 79, // 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 - 78, // 5: ktms.v1.VendorKeyInfo.created_at:type_name -> google.protobuf.Timestamp + 82, // 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 @@ -4603,97 +4965,103 @@ var file_ktms_v1_admin_proto_depIdxs = []int32{ 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 + 82, // 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 + 82, // 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 + 82, // 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 + 82, // 24: ktms.v1.Organization.created_at:type_name -> google.protobuf.Timestamp + 82, // 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 + 82, // 32: ktms.v1.GetTeamSpendResponse.resets_at:type_name -> google.protobuf.Timestamp + 82, // 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 + 81, // 35: ktms.v1.ExplainRouteRequest.effort:type_name -> ktms.v1.Effort + 80, // 36: ktms.v1.ExplainRouteRequest.attributes:type_name -> ktms.v1.ExplainRouteRequest.AttributesEntry + 83, // 37: ktms.v1.RouteCandidate.paused_for:type_name -> google.protobuf.Duration + 77, // 38: ktms.v1.ExplainRouteResponse.candidates:type_name -> ktms.v1.RouteCandidate + 2, // 39: ktms.v1.AdminService.UpsertModel:input_type -> ktms.v1.UpsertModelRequest + 4, // 40: ktms.v1.AdminService.DeleteModel:input_type -> ktms.v1.DeleteModelRequest + 6, // 41: ktms.v1.AdminService.ReplaceRules:input_type -> ktms.v1.ReplaceRulesRequest + 8, // 42: ktms.v1.AdminService.SetVendorKey:input_type -> ktms.v1.SetVendorKeyRequest + 10, // 43: ktms.v1.AdminService.DeleteVendorKey:input_type -> ktms.v1.DeleteVendorKeyRequest + 13, // 44: ktms.v1.AdminService.GetConfig:input_type -> ktms.v1.GetConfigRequest + 15, // 45: ktms.v1.AdminService.MintAPIKey:input_type -> ktms.v1.MintAPIKeyRequest + 20, // 46: ktms.v1.AdminService.RevokeAPIKey:input_type -> ktms.v1.RevokeAPIKeyRequest + 22, // 47: ktms.v1.AdminService.ListAPIKeys:input_type -> ktms.v1.ListAPIKeysRequest + 17, // 48: ktms.v1.AdminService.SetAPIKeyPolicy:input_type -> ktms.v1.SetAPIKeyPolicyRequest + 25, // 49: ktms.v1.AdminService.RotateEncryption:input_type -> ktms.v1.RotateEncryptionRequest + 27, // 50: ktms.v1.AdminService.MintNodeKey:input_type -> ktms.v1.MintNodeKeyRequest + 29, // 51: ktms.v1.AdminService.RevokeNodeKey:input_type -> ktms.v1.RevokeNodeKeyRequest + 31, // 52: ktms.v1.AdminService.ListNodeKeys:input_type -> ktms.v1.ListNodeKeysRequest + 34, // 53: ktms.v1.AdminService.ListNodes:input_type -> ktms.v1.ListNodesRequest + 37, // 54: ktms.v1.AdminService.DrainNode:input_type -> ktms.v1.DrainNodeRequest + 40, // 55: ktms.v1.AdminService.SetUserRateLimit:input_type -> ktms.v1.SetUserRateLimitRequest + 42, // 56: ktms.v1.AdminService.DeleteUserRateLimit:input_type -> ktms.v1.DeleteUserRateLimitRequest + 44, // 57: ktms.v1.AdminService.ListUserRateLimits:input_type -> ktms.v1.ListUserRateLimitsRequest + 47, // 58: ktms.v1.AdminService.SetSpendLimit:input_type -> ktms.v1.SetSpendLimitRequest + 49, // 59: ktms.v1.AdminService.DeleteSpendLimit:input_type -> ktms.v1.DeleteSpendLimitRequest + 51, // 60: ktms.v1.AdminService.ListSpendLimits:input_type -> ktms.v1.ListSpendLimitsRequest + 55, // 61: ktms.v1.AdminService.CreateOrganization:input_type -> ktms.v1.CreateOrganizationRequest + 57, // 62: ktms.v1.AdminService.ListOrganizations:input_type -> ktms.v1.ListOrganizationsRequest + 59, // 63: ktms.v1.AdminService.DeleteOrganization:input_type -> ktms.v1.DeleteOrganizationRequest + 61, // 64: ktms.v1.AdminService.CreateTeam:input_type -> ktms.v1.CreateTeamRequest + 63, // 65: ktms.v1.AdminService.GetTeam:input_type -> ktms.v1.GetTeamRequest + 65, // 66: ktms.v1.AdminService.ListTeams:input_type -> ktms.v1.ListTeamsRequest + 67, // 67: ktms.v1.AdminService.UpdateTeam:input_type -> ktms.v1.UpdateTeamRequest + 69, // 68: ktms.v1.AdminService.DeleteTeam:input_type -> ktms.v1.DeleteTeamRequest + 71, // 69: ktms.v1.AdminService.GetTeamSpend:input_type -> ktms.v1.GetTeamSpendRequest + 73, // 70: ktms.v1.AdminService.GetSpend:input_type -> ktms.v1.GetSpendRequest + 76, // 71: ktms.v1.AdminService.ExplainRoute:input_type -> ktms.v1.ExplainRouteRequest + 3, // 72: ktms.v1.AdminService.UpsertModel:output_type -> ktms.v1.UpsertModelResponse + 5, // 73: ktms.v1.AdminService.DeleteModel:output_type -> ktms.v1.DeleteModelResponse + 7, // 74: ktms.v1.AdminService.ReplaceRules:output_type -> ktms.v1.ReplaceRulesResponse + 9, // 75: ktms.v1.AdminService.SetVendorKey:output_type -> ktms.v1.SetVendorKeyResponse + 11, // 76: ktms.v1.AdminService.DeleteVendorKey:output_type -> ktms.v1.DeleteVendorKeyResponse + 14, // 77: ktms.v1.AdminService.GetConfig:output_type -> ktms.v1.GetConfigResponse + 19, // 78: ktms.v1.AdminService.MintAPIKey:output_type -> ktms.v1.MintAPIKeyResponse + 21, // 79: ktms.v1.AdminService.RevokeAPIKey:output_type -> ktms.v1.RevokeAPIKeyResponse + 24, // 80: ktms.v1.AdminService.ListAPIKeys:output_type -> ktms.v1.ListAPIKeysResponse + 18, // 81: ktms.v1.AdminService.SetAPIKeyPolicy:output_type -> ktms.v1.SetAPIKeyPolicyResponse + 26, // 82: ktms.v1.AdminService.RotateEncryption:output_type -> ktms.v1.RotateEncryptionResponse + 28, // 83: ktms.v1.AdminService.MintNodeKey:output_type -> ktms.v1.MintNodeKeyResponse + 30, // 84: ktms.v1.AdminService.RevokeNodeKey:output_type -> ktms.v1.RevokeNodeKeyResponse + 33, // 85: ktms.v1.AdminService.ListNodeKeys:output_type -> ktms.v1.ListNodeKeysResponse + 36, // 86: ktms.v1.AdminService.ListNodes:output_type -> ktms.v1.ListNodesResponse + 38, // 87: ktms.v1.AdminService.DrainNode:output_type -> ktms.v1.DrainNodeResponse + 41, // 88: ktms.v1.AdminService.SetUserRateLimit:output_type -> ktms.v1.SetUserRateLimitResponse + 43, // 89: ktms.v1.AdminService.DeleteUserRateLimit:output_type -> ktms.v1.DeleteUserRateLimitResponse + 45, // 90: ktms.v1.AdminService.ListUserRateLimits:output_type -> ktms.v1.ListUserRateLimitsResponse + 48, // 91: ktms.v1.AdminService.SetSpendLimit:output_type -> ktms.v1.SetSpendLimitResponse + 50, // 92: ktms.v1.AdminService.DeleteSpendLimit:output_type -> ktms.v1.DeleteSpendLimitResponse + 52, // 93: ktms.v1.AdminService.ListSpendLimits:output_type -> ktms.v1.ListSpendLimitsResponse + 56, // 94: ktms.v1.AdminService.CreateOrganization:output_type -> ktms.v1.CreateOrganizationResponse + 58, // 95: ktms.v1.AdminService.ListOrganizations:output_type -> ktms.v1.ListOrganizationsResponse + 60, // 96: ktms.v1.AdminService.DeleteOrganization:output_type -> ktms.v1.DeleteOrganizationResponse + 62, // 97: ktms.v1.AdminService.CreateTeam:output_type -> ktms.v1.CreateTeamResponse + 64, // 98: ktms.v1.AdminService.GetTeam:output_type -> ktms.v1.GetTeamResponse + 66, // 99: ktms.v1.AdminService.ListTeams:output_type -> ktms.v1.ListTeamsResponse + 68, // 100: ktms.v1.AdminService.UpdateTeam:output_type -> ktms.v1.UpdateTeamResponse + 70, // 101: ktms.v1.AdminService.DeleteTeam:output_type -> ktms.v1.DeleteTeamResponse + 72, // 102: ktms.v1.AdminService.GetTeamSpend:output_type -> ktms.v1.GetTeamSpendResponse + 75, // 103: ktms.v1.AdminService.GetSpend:output_type -> ktms.v1.GetSpendResponse + 78, // 104: ktms.v1.AdminService.ExplainRoute:output_type -> ktms.v1.ExplainRouteResponse + 72, // [72:105] is the sub-list for method output_type + 39, // [39:72] is the sub-list for method input_type + 39, // [39:39] is the sub-list for extension type_name + 39, // [39:39] is the sub-list for extension extendee + 0, // [0:39] is the sub-list for field type_name } func init() { file_ktms_v1_admin_proto_init() } @@ -4708,7 +5076,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: 77, + NumMessages: 81, 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 7b6f2a4..f035053 100644 --- a/internal/gen/ktms/v1/admin_grpc.pb.go +++ b/internal/gen/ktms/v1/admin_grpc.pb.go @@ -51,14 +51,16 @@ const ( AdminService_DeleteTeam_FullMethodName = "/ktms.v1.AdminService/DeleteTeam" AdminService_GetTeamSpend_FullMethodName = "/ktms.v1.AdminService/GetTeamSpend" AdminService_GetSpend_FullMethodName = "/ktms.v1.AdminService/GetSpend" + AdminService_ExplainRoute_FullMethodName = "/ktms.v1.AdminService/ExplainRoute" ) // AdminServiceClient is the client API for AdminService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // -// All admin RPCs require the "admin" scope. Every config mutation validates -// the resulting full config in one transaction and bumps the config version. +// Admin RPCs require the "admin" scope; the Get* and List* reads and +// ExplainRoute also accept "admin:read". Every config mutation validates the +// resulting full config in one transaction and bumps the config version. type AdminServiceClient interface { UpsertModel(ctx context.Context, in *UpsertModelRequest, opts ...grpc.CallOption) (*UpsertModelResponse, error) DeleteModel(ctx context.Context, in *DeleteModelRequest, opts ...grpc.CallOption) (*DeleteModelResponse, error) @@ -92,6 +94,10 @@ type AdminServiceClient interface { 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) + // ExplainRoute dry-runs GenerateText's routing for a client key: the rule + // and every model of its chain, kept or dropped with the reason. No vendor + // or judge is called and nothing is spent; rate limits are not checked. + ExplainRoute(ctx context.Context, in *ExplainRouteRequest, opts ...grpc.CallOption) (*ExplainRouteResponse, error) } type adminServiceClient struct { @@ -422,12 +428,23 @@ func (c *adminServiceClient) GetSpend(ctx context.Context, in *GetSpendRequest, return out, nil } +func (c *adminServiceClient) ExplainRoute(ctx context.Context, in *ExplainRouteRequest, opts ...grpc.CallOption) (*ExplainRouteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExplainRouteResponse) + err := c.cc.Invoke(ctx, AdminService_ExplainRoute_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. // -// All admin RPCs require the "admin" scope. Every config mutation validates -// the resulting full config in one transaction and bumps the config version. +// Admin RPCs require the "admin" scope; the Get* and List* reads and +// ExplainRoute also accept "admin:read". Every config mutation validates the +// resulting full config in one transaction and bumps the config version. type AdminServiceServer interface { UpsertModel(context.Context, *UpsertModelRequest) (*UpsertModelResponse, error) DeleteModel(context.Context, *DeleteModelRequest) (*DeleteModelResponse, error) @@ -461,6 +478,10 @@ type AdminServiceServer interface { DeleteTeam(context.Context, *DeleteTeamRequest) (*DeleteTeamResponse, error) GetTeamSpend(context.Context, *GetTeamSpendRequest) (*GetTeamSpendResponse, error) GetSpend(context.Context, *GetSpendRequest) (*GetSpendResponse, error) + // ExplainRoute dry-runs GenerateText's routing for a client key: the rule + // and every model of its chain, kept or dropped with the reason. No vendor + // or judge is called and nothing is spent; rate limits are not checked. + ExplainRoute(context.Context, *ExplainRouteRequest) (*ExplainRouteResponse, error) mustEmbedUnimplementedAdminServiceServer() } @@ -567,6 +588,9 @@ func (UnimplementedAdminServiceServer) GetTeamSpend(context.Context, *GetTeamSpe func (UnimplementedAdminServiceServer) GetSpend(context.Context, *GetSpendRequest) (*GetSpendResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSpend not implemented") } +func (UnimplementedAdminServiceServer) ExplainRoute(context.Context, *ExplainRouteRequest) (*ExplainRouteResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExplainRoute not implemented") +} func (UnimplementedAdminServiceServer) mustEmbedUnimplementedAdminServiceServer() {} func (UnimplementedAdminServiceServer) testEmbeddedByValue() {} @@ -1164,6 +1188,24 @@ func _AdminService_GetSpend_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _AdminService_ExplainRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExplainRouteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServiceServer).ExplainRoute(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminService_ExplainRoute_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServiceServer).ExplainRoute(ctx, req.(*ExplainRouteRequest)) + } + 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) @@ -1299,6 +1341,10 @@ var AdminService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetSpend", Handler: _AdminService_GetSpend_Handler, }, + { + MethodName: "ExplainRoute", + Handler: _AdminService_ExplainRoute_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "ktms/v1/admin.proto", diff --git a/internal/gen/ktms/v1/node.pb.go b/internal/gen/ktms/v1/node.pb.go index c7fe184..7a2a047 100644 --- a/internal/gen/ktms/v1/node.pb.go +++ b/internal/gen/ktms/v1/node.pb.go @@ -30,6 +30,7 @@ const ( NodeState_NODE_STATE_OFFLINE NodeState = 3 NodeState_NODE_STATE_DRAINING NodeState = 4 NodeState_NODE_STATE_CIRCUIT_BROKEN NodeState = 5 + NodeState_NODE_STATE_PAUSED NodeState = 6 // the node's schedule is at 0%: up and reachable, not routed to ) // Enum value maps for NodeState. @@ -41,6 +42,7 @@ var ( 3: "NODE_STATE_OFFLINE", 4: "NODE_STATE_DRAINING", 5: "NODE_STATE_CIRCUIT_BROKEN", + 6: "NODE_STATE_PAUSED", } NodeState_value = map[string]int32{ "NODE_STATE_UNSPECIFIED": 0, @@ -49,6 +51,7 @@ var ( "NODE_STATE_OFFLINE": 3, "NODE_STATE_DRAINING": 4, "NODE_STATE_CIRCUIT_BROKEN": 5, + "NODE_STATE_PAUSED": 6, } ) @@ -560,8 +563,11 @@ type HeartbeatRequest struct { QueueDepth uint32 `protobuf:"varint,4,opt,name=queue_depth,json=queueDepth,proto3" json:"queue_depth,omitempty"` Resources *NodeResources `protobuf:"bytes,5,opt,name=resources,proto3" json:"resources,omitempty"` Models []*NodeModelSpec `protobuf:"bytes,6,rep,name=models,proto3" json:"models,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Share of time the node may spend on proxy requests, 0-100, from its local + // schedule. Unset means 100, so agents that predate it keep full capacity. + CapacityPercent *uint32 `protobuf:"varint,7,opt,name=capacity_percent,json=capacityPercent,proto3,oneof" json:"capacity_percent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HeartbeatRequest) Reset() { @@ -636,12 +642,20 @@ func (x *HeartbeatRequest) GetModels() []*NodeModelSpec { return nil } +func (x *HeartbeatRequest) GetCapacityPercent() uint32 { + if x != nil && x.CapacityPercent != nil { + return *x.CapacityPercent + } + return 0 +} + type HeartbeatResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` ReRegisterRequired bool `protobuf:"varint,2,opt,name=re_register_required,json=reRegisterRequired,proto3" json:"re_register_required,omitempty"` DrainRequested bool `protobuf:"varint,3,opt,name=drain_requested,json=drainRequested,proto3" json:"drain_requested,omitempty"` - ActiveProxyRequests int32 `protobuf:"varint,4,opt,name=active_proxy_requests,json=activeProxyRequests,proto3" json:"active_proxy_requests,omitempty"` + ActiveProxyRequests int32 `protobuf:"varint,4,opt,name=active_proxy_requests,json=activeProxyRequests,proto3" json:"active_proxy_requests,omitempty"` // requests still running, not counting post-request cooldown + CapacityApplied bool `protobuf:"varint,5,opt,name=capacity_applied,json=capacityApplied,proto3" json:"capacity_applied,omitempty"` // false from proxies that ignore capacity_percent unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -704,6 +718,13 @@ func (x *HeartbeatResponse) GetActiveProxyRequests() int32 { return 0 } +func (x *HeartbeatResponse) GetCapacityApplied() bool { + if x != nil { + return x.CapacityApplied + } + return false +} + type UnregisterNodeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` @@ -849,7 +870,7 @@ const file_ktms_v1_node_proto_rawDesc = "" + "\x06models\x18\x02 \x03(\v2\x16.ktms.v1.NodeModelSpecR\x06models\"f\n" + "\x14ValidateNodeResponse\x12'\n" + "\x0faccepted_models\x18\x01 \x03(\tR\x0eacceptedModels\x12%\n" + - "\x0edropped_models\x18\x02 \x03(\tR\rdroppedModels\"\xf7\x01\n" + + "\x0edropped_models\x18\x02 \x03(\tR\rdroppedModels\"\xbc\x02\n" + "\x10HeartbeatRequest\x12\x17\n" + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x1a\n" + "\bdraining\x18\x02 \x01(\bR\bdraining\x12'\n" + @@ -857,24 +878,28 @@ const file_ktms_v1_node_proto_rawDesc = "" + "\vqueue_depth\x18\x04 \x01(\rR\n" + "queueDepth\x124\n" + "\tresources\x18\x05 \x01(\v2\x16.ktms.v1.NodeResourcesR\tresources\x12.\n" + - "\x06models\x18\x06 \x03(\v2\x16.ktms.v1.NodeModelSpecR\x06models\"\xb2\x01\n" + + "\x06models\x18\x06 \x03(\v2\x16.ktms.v1.NodeModelSpecR\x06models\x12.\n" + + "\x10capacity_percent\x18\a \x01(\rH\x00R\x0fcapacityPercent\x88\x01\x01B\x13\n" + + "\x11_capacity_percent\"\xdd\x01\n" + "\x11HeartbeatResponse\x12\x0e\n" + "\x02ok\x18\x01 \x01(\bR\x02ok\x120\n" + "\x14re_register_required\x18\x02 \x01(\bR\x12reRegisterRequired\x12'\n" + "\x0fdrain_requested\x18\x03 \x01(\bR\x0edrainRequested\x122\n" + - "\x15active_proxy_requests\x18\x04 \x01(\x05R\x13activeProxyRequests\"H\n" + + "\x15active_proxy_requests\x18\x04 \x01(\x05R\x13activeProxyRequests\x12)\n" + + "\x10capacity_applied\x18\x05 \x01(\bR\x0fcapacityApplied\"H\n" + "\x15UnregisterNodeRequest\x12\x17\n" + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x16\n" + "\x06reason\x18\x02 \x01(\tR\x06reason\"(\n" + "\x16UnregisterNodeResponse\x12\x0e\n" + - "\x02ok\x18\x01 \x01(\bR\x02ok*\xa7\x01\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok*\xbe\x01\n" + "\tNodeState\x12\x1a\n" + "\x16NODE_STATE_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12NODE_STATE_HEALTHY\x10\x01\x12\x16\n" + "\x12NODE_STATE_SUSPECT\x10\x02\x12\x16\n" + "\x12NODE_STATE_OFFLINE\x10\x03\x12\x17\n" + "\x13NODE_STATE_DRAINING\x10\x04\x12\x1d\n" + - "\x19NODE_STATE_CIRCUIT_BROKEN\x10\x052\xc6\x02\n" + + "\x19NODE_STATE_CIRCUIT_BROKEN\x10\x05\x12\x15\n" + + "\x11NODE_STATE_PAUSED\x10\x062\xc6\x02\n" + "\x13NodeRegistryService\x12K\n" + "\fRegisterNode\x12\x1c.ktms.v1.RegisterNodeRequest\x1a\x1d.ktms.v1.RegisterNodeResponse\x12K\n" + "\fValidateNode\x12\x1c.ktms.v1.ValidateNodeRequest\x1a\x1d.ktms.v1.ValidateNodeResponse\x12B\n" + @@ -937,6 +962,7 @@ func file_ktms_v1_node_proto_init() { if File_ktms_v1_node_proto != nil { return } + file_ktms_v1_node_proto_msgTypes[6].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/proto/ktms/v1/admin.proto b/proto/ktms/v1/admin.proto index c9ee402..9c59582 100644 --- a/proto/ktms/v1/admin.proto +++ b/proto/ktms/v1/admin.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package ktms.v1; +import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; import "ktms/v1/gateway.proto"; @@ -91,6 +92,9 @@ message VendorKeyInfo { string vendor = 2; int32 key_version = 3; // keyring version used to encrypt google.protobuf.Timestamp created_at = 4; + // First 16 hex characters of the key's SHA-256, so a client can tell whether + // the stored key equals one it holds; empty when the keyring cannot read it. + string fingerprint = 5; } message GetConfigRequest {} @@ -106,7 +110,7 @@ message GetConfigResponse { message MintAPIKeyRequest { string name = 1; - repeated string scopes = 2; // "generate" | "admin" + repeated string scopes = 2; // "generate" | "admin" | "admin:read" string org_id = 3; string team_id = 4; APIKeyPolicy policy = 5; @@ -212,7 +216,7 @@ message NodeInfo { string node_id = 1; string name = 2; string endpoint = 3; - string state = 4; // HEALTHY | SUSPECT | DRAINING | CIRCUIT_BROKEN | OFFLINE + string state = 4; // HEALTHY | SUSPECT | DRAINING | PAUSED | CIRCUIT_BROKEN | OFFLINE repeated string models = 5; double effective_weight = 6; int32 active_requests = 7; @@ -227,6 +231,8 @@ message NodeInfo { double base_weight = 16; string engine = 17; repeated string warm_models = 18; + uint32 capacity_percent = 19; // current level of the node's schedule + int32 effective_slots = 20; // concurrent requests allowed at that level } message ListNodesResponse { @@ -423,8 +429,9 @@ 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. +// Admin RPCs require the "admin" scope; the Get* and List* reads and +// ExplainRoute also accept "admin:read". Every config mutation validates the +// resulting full config in one transaction and bumps the config version. service AdminService { rpc UpsertModel(UpsertModelRequest) returns (UpsertModelResponse); rpc DeleteModel(DeleteModelRequest) returns (DeleteModelResponse); @@ -459,4 +466,45 @@ service AdminService { rpc DeleteTeam(DeleteTeamRequest) returns (DeleteTeamResponse); rpc GetTeamSpend(GetTeamSpendRequest) returns (GetTeamSpendResponse); rpc GetSpend(GetSpendRequest) returns (GetSpendResponse); + + // ExplainRoute dry-runs GenerateText's routing for a client key: the rule + // and every model of its chain, kept or dropped with the reason. No vendor + // or judge is called and nothing is spent; rate limits are not checked. + rpc ExplainRoute(ExplainRouteRequest) returns (ExplainRouteResponse); +} + +message ExplainRouteRequest { + string api_key_name = 1; // the client key the request would carry + Effort effort = 2; // as in GenerateTextRequest; with auto_route, the effort the judge would otherwise pick + bool auto_route = 3; // as in GenerateTextRequest; without an effort the judge is not called (judge_skipped) + map attributes = 4; // request attributes the rules match on, user_id and canary_key included + string placement = 5; // nodes_first | nodes_only | cloud_first | cloud_only, empty for the rule's default + string model_override = 6; // as in GenerateTextRequest + repeated string capabilities = 7; // capabilities the request needs, e.g. "google_search" +} + +message RouteCandidate { + string model_id = 1; + string vendor = 2; + // Why the model was dropped, empty if it stays: not_found, effort, capability, no_vendor_key, + // key_policy, team_policy, placement, circuit_open, vendor_paused (paused_for), + // node_unavailable, node_pool_disabled. New reasons may appear. + string dropped = 3; + google.protobuf.Duration paused_for = 4; // vendor_paused: until the vendor's Retry-After ends +} + +message ExplainRouteResponse { + string rule = 1; // the matched rule; empty with model_override + string effort = 2; // the effort matched on + string route_mode = 3; // direct | override | auto-effort | auto-default, as X-KTMS-Route-Mode + repeated string chain = 4; // the rule's chain after the canary draw + repeated RouteCandidate candidates = 5; // every model of chain, in order + repeated string models = 6; // what a call would try, in order: the kept candidates + repeated int32 weights = 7; // the rule's canary weights over its use list; empty = none + string shadow_model = 8; // mirrored model, if the rule mirrors and it is callable + double shadow_percentage = 9; + bool judge_skipped = 10; // auto_route without effort: a real call would ask the judge first + // The code and message a real call would fail with before any vendor, as in the + // error contract, e.g. "FAILED_PRECONDITION: no_vendor_key"; empty if it would go out. + string error = 11; } diff --git a/proto/ktms/v1/node.proto b/proto/ktms/v1/node.proto index c3d5cb9..22b483f 100644 --- a/proto/ktms/v1/node.proto +++ b/proto/ktms/v1/node.proto @@ -11,6 +11,7 @@ enum NodeState { NODE_STATE_OFFLINE = 3; NODE_STATE_DRAINING = 4; NODE_STATE_CIRCUIT_BROKEN = 5; + NODE_STATE_PAUSED = 6; // the node's schedule is at 0%: up and reachable, not routed to } message NodeResources { @@ -71,13 +72,17 @@ message HeartbeatRequest { uint32 queue_depth = 4; NodeResources resources = 5; repeated NodeModelSpec models = 6; + // Share of time the node may spend on proxy requests, 0-100, from its local + // schedule. Unset means 100, so agents that predate it keep full capacity. + optional uint32 capacity_percent = 7; } message HeartbeatResponse { bool ok = 1; bool re_register_required = 2; bool drain_requested = 3; - int32 active_proxy_requests = 4; + int32 active_proxy_requests = 4; // requests still running, not counting post-request cooldown + bool capacity_applied = 5; // false from proxies that ignore capacity_percent } message UnregisterNodeRequest { diff --git a/scripts/e2e.refs b/scripts/e2e.refs index 4b60777..a37e8dd 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: # the released pair this console is tested with. The daily run uses main. -E2E_KTMS_REF=v1.0.0 -E2E_TOKENMETER_REF=v1.0.0 +E2E_KTMS_REF=v1.2.0 +E2E_TOKENMETER_REF=v1.1.0