diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index d0299fc..3b91eaa 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,7 +1,6 @@ name: Publish Docker image -# Every v* tag becomes an image on Docker Hub: coyl/ktms-admin:, also -# under the old name coyl/llm-proxy-admin so existing deployments keep pulling. +# Every v* tag becomes an image on Docker Hub: coyl/ktms-admin:. # Needs the repository secrets DOCKER_USERNAME and DOCKER_PASSWORD (an access token). on: push: @@ -44,9 +43,7 @@ jobs: id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: - images: | - coyl/ktms-admin - coyl/llm-proxy-admin + images: coyl/ktms-admin tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}},enable=${{ steps.newest.outputs.line }} @@ -71,14 +68,11 @@ jobs: with: cosign-release: v3.1.3 - # Signatures are stored per repository, so each name is signed; each is - # retried on its own so a Rekor or Fulcio blip leaves no tag unsigned. + # Retried so a Rekor or Fulcio blip does not leave the tag unsigned. - name: Sign the image run: | - for image in coyl/ktms-admin coyl/llm-proxy-admin; do - for i in 1 2 3; do - cosign sign --yes --recursive "$image@${{ steps.build.outputs.digest }}" && break - [ "$i" = 3 ] && exit 1 - sleep $((i * 15)) - done + for i in 1 2 3; do + cosign sign --yes --recursive "coyl/ktms-admin@${{ steps.build.outputs.digest }}" && exit 0 + sleep $((i * 15)) done + exit 1 diff --git a/.github/workflows/e2e-main.yml b/.github/workflows/e2e-main.yml new file mode 100644 index 0000000..e864766 --- /dev/null +++ b/.github/workflows/e2e-main.yml @@ -0,0 +1,32 @@ +name: E2E against main + +# KTMS and light-tokenmeter main move on their own; this daily run notices +# before the pinned pair in scripts/e2e.refs is moved forward. +on: + schedule: + - cron: "23 5 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + e2e: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: bufbuild/buf-action@v1 + with: + setup_only: true + version: 1.69.0 + - name: proto/ktms/v1 matches KTMS main + env: + KTMS_REF: main + run: ./scripts/proto-check.sh + - name: End-to-end with KTMS and light-tokenmeter main + if: success() || failure() + env: + KTMS_REF: main + TOKENMETER_REF: main + run: ./scripts/e2e.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8250ba1..a532bd6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,29 @@ jobs: - name: Check JavaScript syntax run: for f in internal/web/static/js/*.js internal/web/static/js/pages/*.js internal/web/static/vendor/*.mjs; do node --check "$f"; done + # KTMS owns ktms.v1: the copy here must match it at the ref the e2e test + # runs against, and internal/gen must be generated from the copy. + proto: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bufbuild/buf-action@v1 + with: + setup_only: true + version: 1.69.0 + - name: proto/ktms/v1 matches KTMS, internal/gen matches the copy + run: ./scripts/proto-check.sh + + # The console against KTMS and light-tokenmeter built from the refs in + # scripts/e2e.refs, with light-tokenmeter's authentication on: README "Compatibility". + e2e: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: End-to-end with KTMS and light-tokenmeter + run: ./scripts/e2e.sh + docker: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index ba05414..8fdc0a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /bin/ -/llm-proxy-admin +/ktms-admin .env .DS_Store .idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca8920..b825ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,34 +6,50 @@ Notable changes to ktms-admin. The format follows ## [Unreleased] +## [1.0.0] - 2026-10-23 + +The first stable release, paired with Keystone. The Model Switchboard. (KTMS) 1.0: ktms-admin +1.x works with every KTMS 1.x release (see [Compatibility](README.md#compatibility)). + ### Added -- `TOKENMETER_TOKEN`: a light-tokenmeter bearer token with the `read` scope, for its authentication. - The usage dot turns red and names the setting when the token is missing, rejected or lacks the scope; - `examples/compose.yaml` wires tokens through the whole stack. -- Any OpenID Connect provider by its generic settings (`OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_ISSUER`, - `OIDC_NAME`), several at once through `OIDC_PROVIDERS`, and GitHub sign-in (`GITHUB_CLIENT_ID`, - `GITHUB_CLIENT_SECRET`, `GITHUB_URL` for Enterprise Server). +- `TOKENMETER_TOKEN`: a light-tokenmeter 1.0 bearer token with the `read` scope, for its + authentication. The usage status in the sidebar says "refused" and names the setting when the token + is missing, rejected or lacks the scope; `examples/compose.yaml` wires tokens through the whole stack. - Password sign-in: `ADMIN_USERS` or `ADMIN_USERS_FILE` hold `email:hash` entries made by the new `ktms-admin hash-password` command (argon2id; bcrypt from `htpasswd -nB` works too). Failed attempts back off per account and per client address. +- Any OpenID Connect provider by its generic settings (`OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_ISSUER`, + `OIDC_NAME`), several at once through `OIDC_PROVIDERS`, and GitHub sign-in (`GITHUB_CLIENT_ID`, + `GITHUB_CLIENT_SECRET`, `GITHUB_URL` for Enterprise Server). - Sign-in through an access proxy: an e-mail header from `TRUSTED_PROXY_CIDRS` (oauth2-proxy, Authelia, - Authentik, Pomerium) or a verified Cloudflare Access token (`CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD`). - Google sign-in is now optional when one of these is set. + 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 + are pluggable adapters behind one core that owns sessions, roles and the sign-in page; Google works as + before, with the same redirect URI. - The console and sign-in page use the ai-process.dev palette and logo. The dark theme runs orange to yellow on warm near-black, and the light theme has orange accents. Warnings are yellow and errors red, so they no longer look like the accent; the third chart and rule colour is blue. -- Sign-in methods are pluggable adapters behind one core that owns sessions, roles and the - sign-in page. Google sign-in works as before, with the same redirect URI. +- The image is published only as `coyl/ktms-admin`. `coyl/llm-proxy-admin` stays at 0.4.0: switch + your deployments to the new name. +- The binary and Go module are named `ktms-admin`; the console's internal CSRF header value + changed with them (the server and its UI ship together). +- The README names the gateway Keystone. The Model Switchboard. (KTMS). +- The license is Apache 2.0, with a `NOTICE` file. ### Deprecated - `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`: use `OIDC_CLIENT_ID` and `OIDC_CLIENT_SECRET`. The old names keep working with a startup warning and are removed in 2.0. ### Fixed -- A profile picture the CSP blocks no longer shows as a broken image. - Sign-in error pages are styled again: the CSP blocked their inline styles. +- A profile picture the CSP blocks no longer shows as a broken image. ## [0.4.0] - 2026-10-22 diff --git a/Dockerfile b/Dockerfile index ad24158..6659994 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,13 +6,13 @@ COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download COPY . . RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \ - CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w" -o /out/llm-proxy-admin ./cmd/llm-proxy-admin + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w" -o /out/ktms-admin ./cmd/ktms-admin FROM alpine:3.21 # CA certificates are needed to reach identity providers for sign-in. RUN apk add --no-cache ca-certificates && adduser -D -u 10001 appuser -COPY --from=builder /out/llm-proxy-admin /llm-proxy-admin +COPY --from=builder /out/ktms-admin /ktms-admin USER appuser EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:8080/healthz >/dev/null || exit 1 -ENTRYPOINT ["/llm-proxy-admin"] +ENTRYPOINT ["/ktms-admin"] diff --git a/Makefile b/Makefile index e361656..a0c3cc9 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: build run test proto fmt dev build: - go build -trimpath -ldflags="-s -w" -o bin/llm-proxy-admin ./cmd/llm-proxy-admin + go build -trimpath -ldflags="-s -w" -o bin/ktms-admin ./cmd/ktms-admin test: go test -race ./... @@ -15,4 +15,4 @@ fmt: # Local run without sign-in (localhost only). Needs KTMS_ADDR + KTMS_ADMIN_KEY and/or TOKENMETER_URL. dev: - PUBLIC_URL=http://localhost:8080 INSECURE_NO_AUTH=true go run ./cmd/llm-proxy-admin + PUBLIC_URL=http://localhost:8080 INSECURE_NO_AUTH=true go run ./cmd/ktms-admin diff --git a/README.md b/README.md index e9eb562..ec633e7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# KTMS admin (llm-proxy-admin) +# Keystone. The Model Switchboard. (KTMS): admin console [![Test](https://github.com/ai-process/ktms-admin/actions/workflows/test.yml/badge.svg)](https://github.com/ai-process/ktms-admin/actions/workflows/test.yml) [![Docker image](https://img.shields.io/docker/v/coyl/ktms-admin?label=docker%20hub&sort=semver)](https://hub.docker.com/r/coyl/ktms-admin) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](LICENSE) -A web console for [**KTMS**](https://github.com/ai-process/ktms) (the LLM gateway, formerly llm-proxy) and +A web console for [**Keystone**](https://github.com/ai-process/ktms), the LLM gateway (KTMS, formerly llm-proxy), and [**light-tokenmeter**](https://github.com/ai-process/light-tokenmeter) (the usage meter). Use it with both, or with either one alone. @@ -207,13 +207,19 @@ through the gateway's stable `ktms.v1` admin API. Within 1.x the console's setti below) keep their names and meaning; renamed ones keep working with a startup warning until 2.0. The console's own `/api` serves its web UI only and is not a public API. -| ktms-admin | KTMS gateway | -| --- | --- | -| 1.x | 1.x | -| 0.4.x | 0.14.x | -| 0.3.x and earlier | 0.13.x | +| ktms-admin | KTMS gateway | light-tokenmeter | +| --- | --- | --- | +| 1.x | 1.x | 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 | + +Before 1.0 there is no compatibility promise: use the pairs above. The Usage page reads +light-tokenmeter's HTTP summary API, which stays compatible within its 1.x. -Before 1.0 there is no compatibility promise: use the pairs above. +`scripts/e2e.sh` checks this on every pull request, against the KTMS and light-tokenmeter refs in +`scripts/e2e.refs`, and daily against their `main`: it turns light-tokenmeter's authentication on and +drives the console's API through models, rules, vendor keys, client keys and their policies, spend +limits and spend, tenants, a playground call through a mock vendor and its usage report. ## Running with one backend or both @@ -222,7 +228,7 @@ The console shows only the pages whose backend you configured. | You set | You get | | --- | --- | | `TOKENMETER_URL` (+ `TOKENMETER_TOKEN` when light-tokenmeter has auth on) | **Usage** page. Nothing else. | -| `KTMS_ADDR` + `KTMS_ADMIN_KEY` | **Models, Rules, Keys, Nodes, Playground, Judge**. | +| `KTMS_ADDR` + `KTMS_ADMIN_KEY` | **Models, Rules, Limits, Keys, Tenants, Team, Nodes, Playground, Judge**. | | both | Everything, plus **estimated cost** on the usage page (tokens × the prices on your models; estimates use standard in/out prices and do not account for peak or batch rates). | The sidebar shows a green or red dot per backend, so a wrong address or a revoked key is visible at once: @@ -247,7 +253,7 @@ All settings are environment variables; see [`env.example`](env.example). | `AUTH_HEADER_EMAIL` | `X-Forwarded-Email` | The header the proxy names the user's e-mail in. | | `AUTH_HEADER_NAME` | empty | Optional header with the user's display name. | | `CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD` | empty | Turn on [Cloudflare Access](#sign-in-behind-an-access-proxy) sign-in. | -| `ALLOWED_EMAILS` | – | Admin patterns. At least one of the two lists must be non-empty. | +| `ALLOWED_EMAILS` | – | Admin patterns. At least one of `ALLOWED_EMAILS`, `VIEWER_EMAILS` or `TEAM_MEMBERSHIPS` must be non-empty. | | `VIEWER_EMAILS` | empty | Read-only patterns. | | `TEAM_MEMBERSHIPS` | empty | Team workspaces: `email:team_id[:teamlead\|member[:org_id]]`, see [Who may sign in](#who-may-sign-in). | | `DEV_USER_ROLE`, `DEV_USER_TEAM_ID`, `DEV_USER_ORG_ID` | admin | With `INSECURE_NO_AUTH`: the role and team of the local dev user. | @@ -310,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, +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. @@ -355,16 +363,19 @@ make proto # regenerate internal/gen from proto/ (needs buf) `js/lib.js` (shared components), `js/pages/*` (one module per page), and `vendor/` (Preact 10 and htm 3, unmodified except that hooks import `./preact.mjs`). Restart the server after editing them: they are embedded at build time. `proto/` is a copy of KTMS's -protocol, so the console talks to it over gRPC without importing its code. +protocol, so the console talks to it over gRPC without importing its code. CI fails when the copy differs +from KTMS at the ref in `scripts/e2e.refs`, or `internal/gen` from the copy: copy the files over and run +`make proto`. -Layout: `cmd/llm-proxy-admin` (entrypoint), `internal/auth` (sessions, allowlists, sign-in page; one package per sign-in method below it), +Layout: `cmd/ktms-admin` (entrypoint), `internal/auth` (sessions, allowlists, sign-in page; one package per sign-in method below it), `internal/api` (JSON API and guards), `internal/usage` (light-tokenmeter client and the greedy/worthy analysis), `internal/config`, `internal/web` (embedded UI). ### Releasing -Pushing a `vX.Y.Z` tag publishes a multi-arch image to Docker Hub as `coyl/ktms-admin:`, also -under the old name `coyl/llm-proxy-admin`. `latest` moves only with the newest stable release. The +Pushing a `vX.Y.Z` tag publishes a multi-arch image to Docker Hub as `coyl/ktms-admin:`. +`latest` moves only with the newest stable release. The old name `coyl/llm-proxy-admin` gets no new +releases after 0.4.0. The image is signed with keyless cosign and carries an SBOM and provenance (verify like the gateway's images, see [KTMS SECURITY.md](https://github.com/ai-process/ktms/blob/main/SECURITY.md#verifying-a-release), with `docker-publish.yml` of `ai-process/ktms-admin`). The repository needs the secrets diff --git a/SECURITY.md b/SECURITY.md index b139c7d..5752e1a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,19 +1,44 @@ # Security policy +## Supported versions + +| Version | Supported | +| ------- | --------- | +| `1.x`, latest minor release | :white_check_mark: | +| `0.x` | :x: upgrade to 1.0 | + ## Reporting a vulnerability Please do **not** open a public issue. Use GitHub's [private vulnerability reporting](https://github.com/ai-process/ktms-admin/security/advisories/new) for this repository. You will get a first answer within a few days. +## Verifying a release + +From 1.0.0 on, images are signed with keyless [Sigstore cosign](https://docs.sigstore.dev/) and carry an +SBOM and SLSA provenance. Use cosign 3 and pin the identity to the tag you verify: + +```bash +V=v1.0.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 +docker buildx imagetools inspect coyl/ktms-admin:${V#v} --format '{{ json .SBOM }}' +``` + ## What this console protects The console holds an **admin key for the KTMS gateway**, which can read and change routing and mint API keys. Treat access to it accordingly: -- Only people matching `ALLOWED_EMAILS` (full access) or `VIEWER_EMAILS` (read-only) can sign in, and - only with a Google-verified address. The lists are re-checked on every request. +- Only verified e-mails that match `ALLOWED_EMAILS` (admins), `VIEWER_EMAILS` (read-only) or + `TEAM_MEMBERSHIPS` (one team's keys and spend) can sign in, and the lists are re-checked on every + request. Sign-in is a password (argon2id or bcrypt hashes, with back-off per account and client + address), OpenID Connect, GitHub, or an access proxy. +- An access proxy's e-mail header is trusted only from `TRUSTED_PROXY_CIDRS`; Cloudflare Access tokens + are verified against the team's keys. Never expose the console directly when relying on a header. - The admin key never reaches the browser; vendor API keys are write-only through the console. -- Mutating requests need the admin role, a custom `X-Requested-With` header and a same-origin `Origin`. +- Mutating requests need a role allowed to make them, a custom `X-Requested-With` header and a + same-origin `Origin`. - Run it behind HTTPS (`PUBLIC_URL=https://…`); cookies are then `Secure`, `HttpOnly` and `SameSite=Lax`. - `INSECURE_NO_AUTH` disables sign-in and is refused unless `PUBLIC_URL` is localhost. diff --git a/buf.gen.yaml b/buf.gen.yaml index f32bd92..efb9d97 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -1,10 +1,11 @@ version: v2 plugins: - - remote: buf.build/protocolbuffers/go + # Pinned: a new plugin release must not turn the generated-code check red. + - remote: buf.build/protocolbuffers/go:v1.36.12 out: internal/gen opt: - paths=source_relative - - remote: buf.build/grpc/go + - remote: buf.build/grpc/go:v1.6.2 out: internal/gen opt: - paths=source_relative @@ -12,4 +13,4 @@ managed: enabled: true override: - file_option: go_package_prefix - value: github.com/ai-process/llm-proxy-admin/internal/gen + value: github.com/ai-process/ktms-admin/internal/gen diff --git a/cmd/llm-proxy-admin/hashpw.go b/cmd/ktms-admin/hashpw.go similarity index 96% rename from cmd/llm-proxy-admin/hashpw.go rename to cmd/ktms-admin/hashpw.go index 83c5cef..838f276 100644 --- a/cmd/llm-proxy-admin/hashpw.go +++ b/cmd/ktms-admin/hashpw.go @@ -8,7 +8,7 @@ import ( "golang.org/x/term" - "github.com/ai-process/llm-proxy-admin/internal/auth/password" + "github.com/ai-process/ktms-admin/internal/auth/password" ) const hashUsage = `usage: ktms-admin hash-password [email] diff --git a/cmd/llm-proxy-admin/main.go b/cmd/ktms-admin/main.go similarity index 90% rename from cmd/llm-proxy-admin/main.go rename to cmd/ktms-admin/main.go index d4baec3..0db85a8 100644 --- a/cmd/llm-proxy-admin/main.go +++ b/cmd/ktms-admin/main.go @@ -14,14 +14,14 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/ai-process/llm-proxy-admin/internal/api" - "github.com/ai-process/llm-proxy-admin/internal/auth" - "github.com/ai-process/llm-proxy-admin/internal/auth/cfaccess" - "github.com/ai-process/llm-proxy-admin/internal/auth/github" - "github.com/ai-process/llm-proxy-admin/internal/auth/header" - "github.com/ai-process/llm-proxy-admin/internal/auth/oidc" - "github.com/ai-process/llm-proxy-admin/internal/auth/password" - "github.com/ai-process/llm-proxy-admin/internal/config" + "github.com/ai-process/ktms-admin/internal/api" + "github.com/ai-process/ktms-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth/cfaccess" + "github.com/ai-process/ktms-admin/internal/auth/github" + "github.com/ai-process/ktms-admin/internal/auth/header" + "github.com/ai-process/ktms-admin/internal/auth/oidc" + "github.com/ai-process/ktms-admin/internal/auth/password" + "github.com/ai-process/ktms-admin/internal/config" ) func main() { diff --git a/examples/compose.yaml b/examples/compose.yaml index f63fd97..dd9146c 100644 --- a/examples/compose.yaml +++ b/examples/compose.yaml @@ -27,6 +27,7 @@ services: image: coyl/ktms:latest depends_on: { db: { condition: service_healthy } } environment: + # The database keeps its llm-proxy-era name, so existing volumes keep their data. DATABASE_URL: postgres://postgres:${DB_PASSWORD:-postgres}@db:5432/llmproxy?sslmode=disable KTMS_ENCRYPTION_KEYS: "${KTMS_ENCRYPTION_KEYS:?set KTMS_ENCRYPTION_KEYS in .env, see env.example}" BOOTSTRAP_ADMIN_KEY: ${BOOTSTRAP_ADMIN_KEY:-} diff --git a/go.mod b/go.mod index b5302de..ca8d1e4 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/ai-process/llm-proxy-admin +module github.com/ai-process/ktms-admin go 1.26.0 diff --git a/internal/api/api.go b/internal/api/api.go index 8afbda8..4276cfc 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -22,15 +22,15 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - "github.com/ai-process/llm-proxy-admin/internal/auth" - "github.com/ai-process/llm-proxy-admin/internal/config" - pb "github.com/ai-process/llm-proxy-admin/internal/gen/ktms/v1" - "github.com/ai-process/llm-proxy-admin/internal/usage" - "github.com/ai-process/llm-proxy-admin/internal/web" + "github.com/ai-process/ktms-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/config" + pb "github.com/ai-process/ktms-admin/internal/gen/ktms/v1" + "github.com/ai-process/ktms-admin/internal/usage" + "github.com/ai-process/ktms-admin/internal/web" ) const csrfHeader = "X-Requested-With" -const csrfValue = "llm-proxy-admin" +const csrfValue = "ktms-admin" type Server struct { cfg *config.Config @@ -110,6 +110,9 @@ 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/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/api_test.go b/internal/api/api_test.go index 9669b72..02eec0d 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -13,11 +13,11 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/ai-process/llm-proxy-admin/internal/auth" - "github.com/ai-process/llm-proxy-admin/internal/auth/oidc" - "github.com/ai-process/llm-proxy-admin/internal/config" - pb "github.com/ai-process/llm-proxy-admin/internal/gen/ktms/v1" - "github.com/ai-process/llm-proxy-admin/internal/usage" + "github.com/ai-process/ktms-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth/oidc" + "github.com/ai-process/ktms-admin/internal/config" + pb "github.com/ai-process/ktms-admin/internal/gen/ktms/v1" + "github.com/ai-process/ktms-admin/internal/usage" ) type fakeAdmin struct { @@ -33,6 +33,8 @@ type fakeAdmin struct { lastMint *pb.MintAPIKeyRequest lastRevoke string spendLimits []*pb.SpendLimitSpec + lastPolicy *pb.SetAPIKeyPolicyRequest + lastSpend *pb.GetSpendRequest } type fakeData struct { @@ -270,7 +272,7 @@ func call(h http.Handler, method, path, body string, c *http.Cookie, hdr map[str return w } -var csrf = map[string]string{"X-Requested-With": "llm-proxy-admin", "Content-Type": "application/json"} +var csrf = map[string]string{"X-Requested-With": "ktms-admin", "Content-Type": "application/json"} func TestAPIRequiresSession(t *testing.T) { s, _, _ := newServer(t, "") @@ -298,7 +300,7 @@ func TestMutationsNeedRoleAndCSRF(t *testing.T) { if w := call(h, "PUT", "/api/proxy/models/m2", body, boss, nil); w.Code != 403 { t.Errorf("no csrf header = %d", w.Code) } - cross := map[string]string{"X-Requested-With": "llm-proxy-admin", "Origin": "https://evil.example"} + cross := map[string]string{"X-Requested-With": "ktms-admin", "Origin": "https://evil.example"} if w := call(h, "PUT", "/api/proxy/models/m2", body, boss, cross); w.Code != 403 { t.Errorf("cross origin = %d", w.Code) } diff --git a/internal/api/keypolicy_test.go b/internal/api/keypolicy_test.go new file mode 100644 index 0000000..22d34ed --- /dev/null +++ b/internal/api/keypolicy_test.go @@ -0,0 +1,96 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "strings" + "sync" + "testing" + + "google.golang.org/grpc" + + 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 +} + +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) + } +} + +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 3776288..699f1e6 100644 --- a/internal/api/proxy.go +++ b/internal/api/proxy.go @@ -16,9 +16,9 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/encoding/protojson" - "github.com/ai-process/llm-proxy-admin/internal/auth" - pb "github.com/ai-process/llm-proxy-admin/internal/gen/ktms/v1" - "github.com/ai-process/llm-proxy-admin/internal/usage" + "github.com/ai-process/ktms-admin/internal/auth" + pb "github.com/ai-process/ktms-admin/internal/gen/ktms/v1" + "github.com/ai-process/ktms-admin/internal/usage" ) const rpcTimeout = 20 * time.Second @@ -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,81 @@ 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, 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 { + backendErr(w, badBody(err)) + return + } + name := r.PathValue("name") + ctx, cancel := context.WithTimeout(r.Context(), rpcTimeout) + defer cancel() + if !u.IsSuperAdmin() { + 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 { @@ -916,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 @@ -1015,7 +1134,7 @@ func (s *Server) fetchPrices(ctx context.Context) (map[string]usage.Price, error return out, nil } -// modelPrices returns cached prices, or nil when llm-proxy is not configured +// modelPrices returns cached prices, or nil when the gateway is not configured // or unreachable; the usage view then simply omits cost. func (s *Server) modelPrices(ctx context.Context) map[string]usage.Price { if s.priceFetch == nil { diff --git a/internal/api/tenancy_access_test.go b/internal/api/tenancy_access_test.go index c17cb03..70893c5 100644 --- a/internal/api/tenancy_access_test.go +++ b/internal/api/tenancy_access_test.go @@ -12,10 +12,10 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/ai-process/llm-proxy-admin/internal/auth" - "github.com/ai-process/llm-proxy-admin/internal/auth/oidc" - "github.com/ai-process/llm-proxy-admin/internal/config" - pb "github.com/ai-process/llm-proxy-admin/internal/gen/ktms/v1" + "github.com/ai-process/ktms-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth/oidc" + "github.com/ai-process/ktms-admin/internal/config" + pb "github.com/ai-process/ktms-admin/internal/gen/ktms/v1" ) func (f *fakeAdmin) GetTeamSpend(_ context.Context, r *pb.GetTeamSpendRequest, _ ...grpc.CallOption) (*pb.GetTeamSpendResponse, error) { diff --git a/internal/api/usage.go b/internal/api/usage.go index 716ab64..5e7bb0f 100644 --- a/internal/api/usage.go +++ b/internal/api/usage.go @@ -4,8 +4,8 @@ import ( "net/http" "time" - "github.com/ai-process/llm-proxy-admin/internal/auth" - "github.com/ai-process/llm-proxy-admin/internal/usage" + "github.com/ai-process/ktms-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/usage" ) const maxRangeDays = 400 diff --git a/internal/auth/cfaccess/cfaccess.go b/internal/auth/cfaccess/cfaccess.go index 2ef7bf2..c8fd5a9 100644 --- a/internal/auth/cfaccess/cfaccess.go +++ b/internal/auth/cfaccess/cfaccess.go @@ -11,7 +11,7 @@ import ( gooidc "github.com/coreos/go-oidc/v3/oidc" "github.com/rs/zerolog/log" - "github.com/ai-process/llm-proxy-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth" ) const ( diff --git a/internal/auth/github/github.go b/internal/auth/github/github.go index 228b745..2091901 100644 --- a/internal/auth/github/github.go +++ b/internal/auth/github/github.go @@ -16,7 +16,7 @@ import ( "github.com/rs/zerolog/log" "golang.org/x/oauth2" - "github.com/ai-process/llm-proxy-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth" ) const ( diff --git a/internal/auth/github/github_test.go b/internal/auth/github/github_test.go index 4b1ec06..e38ff80 100644 --- a/internal/auth/github/github_test.go +++ b/internal/auth/github/github_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/ai-process/llm-proxy-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth" ) // fakeGitHub is a GitHub Enterprise-shaped server: OAuth under /login and diff --git a/internal/auth/header/header.go b/internal/auth/header/header.go index 5889be7..c549106 100644 --- a/internal/auth/header/header.go +++ b/internal/auth/header/header.go @@ -12,7 +12,7 @@ import ( "github.com/rs/zerolog/log" - "github.com/ai-process/llm-proxy-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth" ) const methodID = "header" diff --git a/internal/auth/oidc/oidc.go b/internal/auth/oidc/oidc.go index 7ebb23f..153ed8b 100644 --- a/internal/auth/oidc/oidc.go +++ b/internal/auth/oidc/oidc.go @@ -14,7 +14,7 @@ import ( "github.com/rs/zerolog/log" "golang.org/x/oauth2" - "github.com/ai-process/llm-proxy-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth" ) const ( diff --git a/internal/auth/oidc/oidc_test.go b/internal/auth/oidc/oidc_test.go index 1f80d59..9b0749d 100644 --- a/internal/auth/oidc/oidc_test.go +++ b/internal/auth/oidc/oidc_test.go @@ -14,7 +14,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" - "github.com/ai-process/llm-proxy-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth" ) // fakeIdP is a minimal OIDC provider: discovery, keys and a token endpoint diff --git a/internal/auth/password/password.go b/internal/auth/password/password.go index 129fc90..17eebff 100644 --- a/internal/auth/password/password.go +++ b/internal/auth/password/password.go @@ -19,7 +19,7 @@ import ( "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" - "github.com/ai-process/llm-proxy-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth" ) const ( diff --git a/internal/auth/password/password_test.go b/internal/auth/password/password_test.go index 35c4055..05253c3 100644 --- a/internal/auth/password/password_test.go +++ b/internal/auth/password/password_test.go @@ -13,7 +13,7 @@ import ( "golang.org/x/crypto/bcrypt" - "github.com/ai-process/llm-proxy-admin/internal/auth" + "github.com/ai-process/ktms-admin/internal/auth" ) func TestHashAndVerify(t *testing.T) { diff --git a/internal/gen/ktms/v1/admin.pb.go b/internal/gen/ktms/v1/admin.pb.go index 33a3d6c..3339214 100644 --- a/internal/gen/ktms/v1/admin.pb.go +++ b/internal/gen/ktms/v1/admin.pb.go @@ -46,8 +46,13 @@ 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 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 + sizeCache protoimpl.SizeCache } func (x *ModelSpec) Reset() { @@ -206,18 +211,33 @@ 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 - Priority int32 `protobuf:"varint,2,opt,name=priority,proto3" json:"priority,omitempty"` // unique; higher wins, first match takes the request - Effort Effort `protobuf:"varint,3,opt,name=effort,proto3,enum=ktms.v1.Effort" json:"effort,omitempty"` // EFFORT_UNSPECIFIED = matches any effort - 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"` // equality predicates over request attributes - Use []string `protobuf:"bytes,5,rep,name=use,proto3" json:"use,omitempty"` // ordered fallback chain of model ids, cheaper-first - Enabled bool `protobuf:"varint,6,opt,name=enabled,proto3" json:"enabled,omitempty"` - Weights []int32 `protobuf:"varint,7,rep,packed,name=weights,proto3" json:"weights,omitempty"` // traffic weights per model in use; empty means deterministic (100% on use[0]). Canary distribution is deterministic and sticky when request attribute canary_key or user_id is provided, or weighted random if omitted or canary_mode="random". - ShadowModel string `protobuf:"bytes,8,opt,name=shadow_model,json=shadowModel,proto3" json:"shadow_model,omitempty"` // optional model id for async traffic mirroring - ShadowPercentage float64 `protobuf:"fixed64,9,opt,name=shadow_percentage,json=shadowPercentage,proto3" json:"shadow_percentage,omitempty"` // percentage of requests to mirror (0.0 - 100.0) - ShadowEval bool `protobuf:"varint,10,opt,name=shadow_eval,json=shadowEval,proto3" json:"shadow_eval,omitempty"` // optional automated quality evaluation using Judge + 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 + Priority int32 `protobuf:"varint,2,opt,name=priority,proto3" json:"priority,omitempty"` // unique; higher wins, first match takes the request + Effort Effort `protobuf:"varint,3,opt,name=effort,proto3,enum=ktms.v1.Effort" json:"effort,omitempty"` // EFFORT_UNSPECIFIED = matches any effort + 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"` // equality predicates over request attributes + Use []string `protobuf:"bytes,5,rep,name=use,proto3" json:"use,omitempty"` // ordered fallback chain of model ids, cheaper-first + Enabled bool `protobuf:"varint,6,opt,name=enabled,proto3" json:"enabled,omitempty"` + Weights []int32 `protobuf:"varint,7,rep,packed,name=weights,proto3" json:"weights,omitempty"` // traffic weights per model in use; empty means deterministic (100% on use[0]). Canary distribution is deterministic and sticky when request attribute canary_key or user_id is provided, or weighted random if omitted or canary_mode="random". + // Shadow traffic and its evaluation may change in a 1.x minor release. + ShadowModel string `protobuf:"bytes,8,opt,name=shadow_model,json=shadowModel,proto3" json:"shadow_model,omitempty"` // Experimental: optional model id for async traffic mirroring + ShadowPercentage float64 `protobuf:"fixed64,9,opt,name=shadow_percentage,json=shadowPercentage,proto3" json:"shadow_percentage,omitempty"` // Experimental: percentage of requests to mirror (0.0 - 100.0) + ShadowEval bool `protobuf:"varint,10,opt,name=shadow_eval,json=shadowEval,proto3" json:"shadow_eval,omitempty"` // Experimental: optional automated quality evaluation using Judge unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -942,6 +962,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 } @@ -1004,6 +1025,167 @@ func (x *MintAPIKeyRequest) GetTeamId() string { return "" } +func (x *MintAPIKeyRequest) GetPolicy() *APIKeyPolicy { + if x != nil { + return x.Policy + } + return nil +} + +// 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 + 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 @@ -1013,7 +1195,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) } @@ -1025,7 +1207,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 { @@ -1038,7 +1220,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 { @@ -1057,7 +1239,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) } @@ -1069,7 +1251,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 { @@ -1082,7 +1264,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 { @@ -1100,7 +1282,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) } @@ -1112,7 +1294,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 { @@ -1125,7 +1307,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 { @@ -1136,7 +1318,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) } @@ -1148,7 +1330,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 { @@ -1161,7 +1343,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 { @@ -1172,13 +1354,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) } @@ -1190,7 +1373,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 { @@ -1203,7 +1386,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 { @@ -1248,6 +1431,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"` @@ -1257,7 +1447,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) } @@ -1269,7 +1459,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 { @@ -1282,7 +1472,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 { @@ -1300,7 +1490,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) } @@ -1312,7 +1502,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 { @@ -1325,19 +1515,24 @@ 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 +// with the active keyring version; calling it again changes nothing. A vendor +// key encrypted with a version the keyring lacks fails it with +// FAILED_PRECONDITION and nothing changes. UNAVAILABLE after the vendor keys +// committed means only the node tokens are left: call it again. type RotateEncryptionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Reencrypted int32 `protobuf:"varint,1,opt,name=reencrypted,proto3" json:"reencrypted,omitempty"` // vendor keys re-encrypted with the active keyring version + Reencrypted int32 `protobuf:"varint,1,opt,name=reencrypted,proto3" json:"reencrypted,omitempty"` // vendor keys and node auth tokens re-encrypted unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } 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) } @@ -1349,7 +1544,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 { @@ -1362,7 +1557,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 { @@ -1386,7 +1581,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) } @@ -1398,7 +1593,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 { @@ -1411,7 +1606,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 { @@ -1466,7 +1661,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) } @@ -1478,7 +1673,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 { @@ -1491,7 +1686,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 { @@ -1517,7 +1712,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) } @@ -1529,7 +1724,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 { @@ -1542,7 +1737,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 { @@ -1560,7 +1755,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) } @@ -1572,7 +1767,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 { @@ -1585,7 +1780,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 { @@ -1596,7 +1791,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) } @@ -1608,7 +1803,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 { @@ -1621,7 +1816,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 { @@ -1641,7 +1836,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) } @@ -1653,7 +1848,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 { @@ -1666,7 +1861,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 { @@ -1741,7 +1936,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) } @@ -1753,7 +1948,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 { @@ -1766,7 +1961,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 { @@ -1784,7 +1979,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) } @@ -1796,7 +1991,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 { @@ -1809,7 +2004,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 { @@ -1838,7 +2033,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) } @@ -1850,7 +2045,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 { @@ -1863,7 +2058,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 { @@ -2001,7 +2196,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) } @@ -2013,7 +2208,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 { @@ -2026,7 +2221,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 { @@ -2045,7 +2240,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) } @@ -2057,7 +2252,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 { @@ -2070,7 +2265,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 { @@ -2088,7 +2283,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) } @@ -2100,7 +2295,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 { @@ -2113,7 +2308,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 { @@ -2130,7 +2325,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) } @@ -2142,7 +2337,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 { @@ -2155,7 +2350,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 { @@ -2209,7 +2404,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) } @@ -2221,7 +2416,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 { @@ -2234,7 +2429,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 { @@ -2252,7 +2447,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) } @@ -2264,7 +2459,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 { @@ -2277,7 +2472,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 { @@ -2290,7 +2485,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) } @@ -2302,7 +2497,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 { @@ -2315,7 +2510,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 { @@ -2340,7 +2535,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) } @@ -2352,7 +2547,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 { @@ -2365,7 +2560,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 { @@ -2377,7 +2572,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) } @@ -2389,7 +2584,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 { @@ -2402,7 +2597,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 { @@ -2421,7 +2616,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) } @@ -2433,7 +2628,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 { @@ -2446,7 +2641,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 { @@ -2474,7 +2669,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) } @@ -2486,7 +2681,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 { @@ -2499,7 +2694,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 { @@ -2539,7 +2734,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) } @@ -2551,7 +2746,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 { @@ -2564,7 +2759,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 { @@ -2582,7 +2777,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) } @@ -2594,7 +2789,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 { @@ -2607,7 +2802,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 { @@ -2621,7 +2816,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) } @@ -2633,7 +2828,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 { @@ -2646,7 +2841,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 { @@ -2678,7 +2873,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) } @@ -2690,7 +2885,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 { @@ -2703,7 +2898,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 { @@ -2716,7 +2911,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) } @@ -2728,7 +2923,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 { @@ -2741,7 +2936,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 { @@ -2767,7 +2962,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) } @@ -2779,7 +2974,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 { @@ -2792,7 +2987,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 { @@ -2813,7 +3008,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) } @@ -2825,7 +3020,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 { @@ -2838,7 +3033,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 { @@ -2879,7 +3074,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) } @@ -2891,7 +3086,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 { @@ -2904,7 +3099,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 { @@ -2958,7 +3153,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) } @@ -2970,7 +3165,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 { @@ -2983,7 +3178,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 { @@ -3002,7 +3197,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) } @@ -3014,7 +3209,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 { @@ -3027,7 +3222,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 { @@ -3045,7 +3240,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) } @@ -3057,7 +3252,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 { @@ -3070,7 +3265,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 { @@ -3082,7 +3277,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) } @@ -3094,7 +3289,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 { @@ -3107,7 +3302,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 { @@ -3126,7 +3321,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) } @@ -3138,7 +3333,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 { @@ -3151,7 +3346,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 { @@ -3169,7 +3364,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) } @@ -3181,7 +3376,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 { @@ -3194,7 +3389,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 { @@ -3209,7 +3404,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) } @@ -3221,7 +3416,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 { @@ -3234,7 +3429,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 { @@ -3274,7 +3469,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) } @@ -3286,7 +3481,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 { @@ -3299,7 +3494,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 { @@ -3318,7 +3513,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) } @@ -3330,7 +3525,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 { @@ -3343,7 +3538,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 { @@ -3362,7 +3557,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) } @@ -3374,7 +3569,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 { @@ -3387,7 +3582,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 { @@ -3406,7 +3601,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) } @@ -3418,7 +3613,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 { @@ -3431,7 +3626,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 { @@ -3450,7 +3645,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) } @@ -3462,7 +3657,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 { @@ -3475,7 +3670,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 { @@ -3499,7 +3694,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) } @@ -3511,7 +3706,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 { @@ -3524,7 +3719,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 { @@ -3564,7 +3759,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) } @@ -3576,7 +3771,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 { @@ -3589,7 +3784,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 { @@ -3608,7 +3803,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) } @@ -3620,7 +3815,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 { @@ -3633,7 +3828,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 { @@ -3651,7 +3846,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) } @@ -3663,7 +3858,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 { @@ -3676,7 +3871,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 { @@ -3688,7 +3883,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) } @@ -3700,7 +3895,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 { @@ -3713,7 +3908,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 { @@ -3738,7 +3933,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) } @@ -3750,7 +3945,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 { @@ -3763,7 +3958,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 { @@ -3794,11 +3989,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" + @@ -3818,7 +4195,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" + @@ -3873,18 +4252,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" + @@ -3893,7 +4281,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" + @@ -4052,7 +4441,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" + @@ -4063,7 +4465,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" + @@ -4087,9 +4490,10 @@ 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\x99\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\x01ZAgithub.com/ai-process/llm-proxy-admin/internal/gen/ktms/v1;ktmsv1\xa2\x02\x03KXX\xaa\x02\aKtms.V1\xca\x02\aKtms\\V1\xe2\x02\x13Ktms\\V1\\GPBMetadata\xea\x02\bKtms::V1b\x06proto3" + "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() } @@ -4289,7 +4708,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 3b6dffb..ed541ef 100644 --- a/internal/gen/ktms/v1/gateway.pb.go +++ b/internal/gen/ktms/v1/gateway.pb.go @@ -1297,12 +1297,15 @@ type GenerateTextRequest struct { ModelOverride string `protobuf:"bytes,10,opt,name=model_override,json=modelOverride,proto3" json:"model_override,omitempty"` // exact registry model id; bypasses rules, not throttles Tools []*Tool `protobuf:"bytes,11,rep,name=tools,proto3" json:"tools,omitempty"` ToolChoice *ToolChoice `protobuf:"bytes,12,opt,name=tool_choice,json=toolChoice,proto3" json:"tool_choice,omitempty"` - EnableCache bool `protobuf:"varint,13,opt,name=enable_cache,json=enableCache,proto3" json:"enable_cache,omitempty"` // opt-in response cache: exact match, plus latent-similarity match when the proxy enables SEMANTIC_CACHE_ENABLED - Placement string `protobuf:"bytes,14,opt,name=placement,proto3" json:"placement,omitempty"` // "nodes_first" | "nodes_only" | "cloud_first" | "cloud_only" - AutoRoute bool `protobuf:"varint,15,opt,name=auto_route,json=autoRoute,proto3" json:"auto_route,omitempty"` // true when smart model routing is enabled - EffortSpecified bool `protobuf:"varint,16,opt,name=effort_specified,json=effortSpecified,proto3" json:"effort_specified,omitempty"` // true when effort was explicitly pinned by caller - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + EnableCache bool `protobuf:"varint,13,opt,name=enable_cache,json=enableCache,proto3" json:"enable_cache,omitempty"` // opt-in response cache: exact match, plus latent-similarity match when the proxy enables SEMANTIC_CACHE_ENABLED (that match is experimental) + Placement string `protobuf:"bytes,14,opt,name=placement,proto3" json:"placement,omitempty"` // "nodes_first" | "nodes_only" | "cloud_first" | "cloud_only" + // auto_route picks the chain by effort and placement instead of a model + // name. With effort_specified false a judge picks the effort, and that + // choice is experimental; with it true the routing is stable. + AutoRoute bool `protobuf:"varint,15,opt,name=auto_route,json=autoRoute,proto3" json:"auto_route,omitempty"` + EffortSpecified bool `protobuf:"varint,16,opt,name=effort_specified,json=effortSpecified,proto3" json:"effort_specified,omitempty"` // true when effort was explicitly pinned by caller + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GenerateTextRequest) Reset() { @@ -1448,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 } @@ -1499,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"` @@ -1576,10 +1596,10 @@ type GenerateTextResponse struct { MatchedRule string `protobuf:"bytes,5,opt,name=matched_rule,json=matchedRule,proto3" json:"matched_rule,omitempty"` // rule name, "" on model_override Details []*Choice `protobuf:"bytes,6,rep,name=details,proto3" json:"details,omitempty"` Cached bool `protobuf:"varint,7,opt,name=cached,proto3" json:"cached,omitempty"` // true when served from cache - RouteMode string `protobuf:"bytes,8,opt,name=route_mode,json=routeMode,proto3" json:"route_mode,omitempty"` // "direct" | "override" | "auto-judge" | "auto-default" | "auto-effort" + RouteMode string `protobuf:"bytes,8,opt,name=route_mode,json=routeMode,proto3" json:"route_mode,omitempty"` // "direct" | "override" | "auto-judge" | "auto-default" | "auto-effort"; "auto-judge" and "auto-default" are experimental Placement string `protobuf:"bytes,9,opt,name=placement,proto3" json:"placement,omitempty"` // "nodes_first" | "nodes_only" | "cloud_first" | "cloud_only" - JudgeScore float64 `protobuf:"fixed64,10,opt,name=judge_score,json=judgeScore,proto3" json:"judge_score,omitempty"` // score or confidence from judge evaluation - JudgeModel string `protobuf:"bytes,11,opt,name=judge_model,json=judgeModel,proto3" json:"judge_model,omitempty"` // judge model id if judge was invoked + JudgeScore float64 `protobuf:"fixed64,10,opt,name=judge_score,json=judgeScore,proto3" json:"judge_score,omitempty"` // Experimental: score or confidence from judge evaluation + JudgeModel string `protobuf:"bytes,11,opt,name=judge_model,json=judgeModel,proto3" json:"judge_model,omitempty"` // Experimental: judge model id if judge was invoked unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1701,10 +1721,10 @@ type GenerateTextChunk struct { MatchedRule string `protobuf:"bytes,6,opt,name=matched_rule,json=matchedRule,proto3" json:"matched_rule,omitempty"` ToolCallChunks []*ToolCallChunk `protobuf:"bytes,7,rep,name=tool_call_chunks,json=toolCallChunks,proto3" json:"tool_call_chunks,omitempty"` Cached bool `protobuf:"varint,8,opt,name=cached,proto3" json:"cached,omitempty"` // true when served from cache - RouteMode string `protobuf:"bytes,9,opt,name=route_mode,json=routeMode,proto3" json:"route_mode,omitempty"` // "direct" | "override" | "auto-judge" | "auto-default" | "auto-effort" + RouteMode string `protobuf:"bytes,9,opt,name=route_mode,json=routeMode,proto3" json:"route_mode,omitempty"` // "direct" | "override" | "auto-judge" | "auto-default" | "auto-effort"; "auto-judge" and "auto-default" are experimental Placement string `protobuf:"bytes,10,opt,name=placement,proto3" json:"placement,omitempty"` // "nodes_first" | "nodes_only" | "cloud_first" | "cloud_only" - JudgeScore float64 `protobuf:"fixed64,11,opt,name=judge_score,json=judgeScore,proto3" json:"judge_score,omitempty"` // score or confidence from judge evaluation - JudgeModel string `protobuf:"bytes,12,opt,name=judge_model,json=judgeModel,proto3" json:"judge_model,omitempty"` // judge model id if judge was invoked + JudgeScore float64 `protobuf:"fixed64,11,opt,name=judge_score,json=judgeScore,proto3" json:"judge_score,omitempty"` // Experimental: score or confidence from judge evaluation + JudgeModel string `protobuf:"bytes,12,opt,name=judge_model,json=judgeModel,proto3" json:"judge_model,omitempty"` // Experimental: judge model id if judge was invoked unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1870,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() { @@ -1967,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), @@ -2555,6 +2592,7 @@ func (x *JudgeAnswer) GetLegend() map[string]string { // EmbeddingPart is a latent state vector (JEPA, CLIP, ...) judged as-is, so a // caller with an encoder never has to decode it back to text first. +// Experimental: may change in a 1.x minor release (README, API stability). type EmbeddingPart struct { state protoimpl.MessageState `protogen:"open.v1"` Values []float32 `protobuf:"fixed32,1,rep,packed,name=values,proto3" json:"values,omitempty"` @@ -4238,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" + @@ -4281,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" + @@ -4291,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" + @@ -4549,8 +4594,8 @@ const file_ktms_v1_gateway_proto_rawDesc = "" + "\vSubmitBatch\x12\x1b.ktms.v1.SubmitBatchRequest\x1a\x1c.ktms.v1.SubmitBatchResponse\x124\n" + "\bGetBatch\x12\x18.ktms.v1.GetBatchRequest\x1a\x0e.ktms.v1.Batch\x12W\n" + "\x10ListBatchResults\x12 .ktms.v1.ListBatchResultsRequest\x1a!.ktms.v1.ListBatchResultsResponse\x12H\n" + - "\vCancelBatch\x12\x1b.ktms.v1.CancelBatchRequest\x1a\x1c.ktms.v1.CancelBatchResponseB\x9b\x01\n" + - "\vcom.ktms.v1B\fGatewayProtoP\x01ZAgithub.com/ai-process/llm-proxy-admin/internal/gen/ktms/v1;ktmsv1\xa2\x02\x03KXX\xaa\x02\aKtms.V1\xca\x02\aKtms\\V1\xe2\x02\x13Ktms\\V1\\GPBMetadata\xea\x02\bKtms::V1b\x06proto3" + "\vCancelBatch\x12\x1b.ktms.v1.CancelBatchRequest\x1a\x1c.ktms.v1.CancelBatchResponseB\x96\x01\n" + + "\vcom.ktms.v1B\fGatewayProtoP\x01Z_", "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). @@ -77,9 +79,11 @@ type GatewayServiceClient interface { GetEmbeddings(ctx context.Context, in *GetEmbeddingsRequest, opts ...grpc.CallOption) (*GetEmbeddingsResponse, error) // JudgeStream provides bidirectional streaming decisions for real-time // agent evaluation and robotics control loops. + // Experimental: may change in a 1.x minor release (README, API stability). JudgeStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[JudgeRequest, JudgeResponse], error) // EvaluateRollouts scores candidate trajectories against state and criteria // for speculative decoding and Best-of-N selection. + // Experimental: may change in a 1.x minor release (README, API stability). EvaluateRollouts(ctx context.Context, in *EvaluateRolloutsRequest, opts ...grpc.CallOption) (*EvaluateRolloutsResponse, error) ListModels(ctx context.Context, in *ListModelsRequest, opts ...grpc.CallOption) (*ListModelsResponse, error) // Batch generation RPCs. @@ -243,19 +247,21 @@ func (c *gatewayServiceClient) CancelBatch(ctx context.Context, in *CancelBatchR // All implementations must embed UnimplementedGatewayServiceServer // for forward compatibility. // -// Error contract: retry only on UNAVAILABLE ("vendors_unavailable", or +// Error contract: retry UNAVAILABLE with backoff ("vendors_unavailable"; +// "auth_unavailable" or "storage_unavailable" while Postgres is down; // "throttled:spend_unavailable" when spend limits fail closed). When vendors // 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). @@ -282,9 +288,11 @@ type GatewayServiceServer interface { GetEmbeddings(context.Context, *GetEmbeddingsRequest) (*GetEmbeddingsResponse, error) // JudgeStream provides bidirectional streaming decisions for real-time // agent evaluation and robotics control loops. + // Experimental: may change in a 1.x minor release (README, API stability). JudgeStream(grpc.BidiStreamingServer[JudgeRequest, JudgeResponse]) error // EvaluateRollouts scores candidate trajectories against state and criteria // for speculative decoding and Best-of-N selection. + // Experimental: may change in a 1.x minor release (README, API stability). EvaluateRollouts(context.Context, *EvaluateRolloutsRequest) (*EvaluateRolloutsResponse, error) ListModels(context.Context, *ListModelsRequest) (*ListModelsResponse, error) // Batch generation RPCs. diff --git a/internal/gen/ktms/v1/node.pb.go b/internal/gen/ktms/v1/node.pb.go index a8a2749..c7fe184 100644 --- a/internal/gen/ktms/v1/node.pb.go +++ b/internal/gen/ktms/v1/node.pb.go @@ -879,8 +879,8 @@ const file_ktms_v1_node_proto_rawDesc = "" + "\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" + "\tHeartbeat\x12\x19.ktms.v1.HeartbeatRequest\x1a\x1a.ktms.v1.HeartbeatResponse\x12Q\n" + - "\x0eUnregisterNode\x12\x1e.ktms.v1.UnregisterNodeRequest\x1a\x1f.ktms.v1.UnregisterNodeResponseB\x98\x01\n" + - "\vcom.ktms.v1B\tNodeProtoP\x01ZAgithub.com/ai-process/llm-proxy-admin/internal/gen/ktms/v1;ktmsv1\xa2\x02\x03KXX\xaa\x02\aKtms.V1\xca\x02\aKtms\\V1\xe2\x02\x13Ktms\\V1\\GPBMetadata\xea\x02\bKtms::V1b\x06proto3" + "\x0eUnregisterNode\x12\x1e.ktms.v1.UnregisterNodeRequest\x1a\x1f.ktms.v1.UnregisterNodeResponseB\x93\x01\n" + + "\vcom.ktms.v1B\tNodeProtoP\x01Z { await fetch('/auth/logout', { method: 'POST', headers: { 'X-Requested-With': 'llm-proxy-admin' } }); location.href = '/'; }; + const signOut = async () => { await fetch('/auth/logout', { method: 'POST', headers: { 'X-Requested-With': 'ktms-admin' } }); location.href = '/'; }; return html`
- ${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} />`} `; } @@ -64,7 +62,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 +72,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 +80,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 +121,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,12 +137,66 @@ function Wizard({ teams, team, onDone }) { `; } +// 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`${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`—`; +} + +// 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 = team?.allowed_models?.length ? team.allowed_models : (data?.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.'); + 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`<${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."> + <${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()); const rotate = () => confirmAction({ title: 'Re-encrypt vendor keys?', confirm: 'Re-encrypt', danger: false, - text: 'Re-encrypts every stored vendor credential with the newest key in LLMPROXY_ENCRYPTION_KEYS. Do this after adding a key version.', + text: 'Re-encrypts every stored vendor credential with the newest key in KTMS_ENCRYPTION_KEYS. Do this after adding a key version.', action: async () => { const r = await api('POST', '/api/proxy/rotate'); toast(`Re-encrypted ${r?.reencrypted ?? 0} credentials`); }, }); const remove = (v) => confirmAction({ 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 59a102f..c9ee402 100644 --- a/proto/ktms/v1/admin.proto +++ b/proto/ktms/v1/admin.proto @@ -30,6 +30,11 @@ 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 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; } message RuleSpec { @@ -40,9 +45,10 @@ message RuleSpec { repeated string use = 5; // ordered fallback chain of model ids, cheaper-first bool enabled = 6; repeated int32 weights = 7; // traffic weights per model in use; empty means deterministic (100% on use[0]). Canary distribution is deterministic and sticky when request attribute canary_key or user_id is provided, or weighted random if omitted or canary_mode="random". - string shadow_model = 8; // optional model id for async traffic mirroring - double shadow_percentage = 9; // percentage of requests to mirror (0.0 - 100.0) - bool shadow_eval = 10; // optional automated quality evaluation using Judge + // Shadow traffic and its evaluation may change in a 1.x minor release. + string shadow_model = 8; // Experimental: optional model id for async traffic mirroring + double shadow_percentage = 9; // Experimental: percentage of requests to mirror (0.0 - 100.0) + bool shadow_eval = 10; // Experimental: optional automated quality evaluation using Judge } message UpsertModelRequest { @@ -103,8 +109,28 @@ message MintAPIKeyRequest { repeated string scopes = 2; // "generate" | "admin" string org_id = 3; string team_id = 4; + APIKeyPolicy policy = 5; } +// 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 + // 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 } @@ -124,6 +150,7 @@ message APIKeyInfo { bool disabled = 4; string org_id = 5; string team_id = 6; + APIKeyPolicy policy = 7; } message ListAPIKeysResponse { @@ -132,8 +159,13 @@ message ListAPIKeysResponse { message RotateEncryptionRequest {} +// RotateEncryption re-encrypts every vendor key and registered node auth token +// with the active keyring version; calling it again changes nothing. A vendor +// key encrypted with a version the keyring lacks fails it with +// FAILED_PRECONDITION and nothing changes. UNAVAILABLE after the vendor keys +// committed means only the node tokens are left: call it again. message RotateEncryptionResponse { - int32 reencrypted = 1; // vendor keys re-encrypted with the active keyring version + int32 reencrypted = 1; // vendor keys and node auth tokens re-encrypted } message MintNodeKeyRequest { @@ -371,6 +403,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 { @@ -383,6 +435,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); @@ -405,4 +458,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 37c13d9..23d8b6a 100644 --- a/proto/ktms/v1/gateway.proto +++ b/proto/ktms/v1/gateway.proto @@ -157,15 +157,21 @@ message GenerateTextRequest { string model_override = 10; // exact registry model id; bypasses rules, not throttles repeated Tool tools = 11; ToolChoice tool_choice = 12; - bool enable_cache = 13; // opt-in response cache: exact match, plus latent-similarity match when the proxy enables SEMANTIC_CACHE_ENABLED + bool enable_cache = 13; // opt-in response cache: exact match, plus latent-similarity match when the proxy enables SEMANTIC_CACHE_ENABLED (that match is experimental) string placement = 14; // "nodes_first" | "nodes_only" | "cloud_first" | "cloud_only" - bool auto_route = 15; // true when smart model routing is enabled + // auto_route picks the chain by effort and placement instead of a model + // name. With effort_specified false a judge picks the effort, and that + // choice is experimental; with it true the routing is stable. + bool auto_route = 15; bool effort_specified = 16; // true when effort was explicitly pinned by caller } 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 { @@ -183,10 +189,10 @@ message GenerateTextResponse { string matched_rule = 5; // rule name, "" on model_override repeated Choice details = 6; bool cached = 7; // true when served from cache - string route_mode = 8; // "direct" | "override" | "auto-judge" | "auto-default" | "auto-effort" + string route_mode = 8; // "direct" | "override" | "auto-judge" | "auto-default" | "auto-effort"; "auto-judge" and "auto-default" are experimental string placement = 9; // "nodes_first" | "nodes_only" | "cloud_first" | "cloud_only" - double judge_score = 10; // score or confidence from judge evaluation - string judge_model = 11; // judge model id if judge was invoked + double judge_score = 10; // Experimental: score or confidence from judge evaluation + string judge_model = 11; // Experimental: judge model id if judge was invoked } message GenerateTextChunk { @@ -198,10 +204,10 @@ message GenerateTextChunk { string matched_rule = 6; repeated ToolCallChunk tool_call_chunks = 7; bool cached = 8; // true when served from cache - string route_mode = 9; // "direct" | "override" | "auto-judge" | "auto-default" | "auto-effort" + string route_mode = 9; // "direct" | "override" | "auto-judge" | "auto-default" | "auto-effort"; "auto-judge" and "auto-default" are experimental string placement = 10; // "nodes_first" | "nodes_only" | "cloud_first" | "cloud_only" - double judge_score = 11; // score or confidence from judge evaluation - string judge_model = 12; // judge model id if judge was invoked + double judge_score = 11; // Experimental: score or confidence from judge evaluation + string judge_model = 12; // Experimental: judge model id if judge was invoked } message ListModelsRequest {} @@ -216,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 { @@ -228,19 +237,21 @@ message GenerateTextStreamRequest { GenerateTextRequest request = 1; } -// Error contract: retry only on UNAVAILABLE ("vendors_unavailable", or +// Error contract: retry UNAVAILABLE with backoff ("vendors_unavailable"; +// "auth_unavailable" or "storage_unavailable" while Postgres is down; // "throttled:spend_unavailable" when spend limits fail closed). When vendors // 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). @@ -267,9 +278,11 @@ service GatewayService { rpc GetEmbeddings(GetEmbeddingsRequest) returns (GetEmbeddingsResponse); // JudgeStream provides bidirectional streaming decisions for real-time // agent evaluation and robotics control loops. + // Experimental: may change in a 1.x minor release (README, API stability). rpc JudgeStream(stream JudgeRequest) returns (stream JudgeResponse); // EvaluateRollouts scores candidate trajectories against state and criteria // for speculative decoding and Best-of-N selection. + // Experimental: may change in a 1.x minor release (README, API stability). rpc EvaluateRollouts(EvaluateRolloutsRequest) returns (EvaluateRolloutsResponse); rpc ListModels(ListModelsRequest) returns (ListModelsResponse); @@ -344,6 +357,7 @@ message JudgeAnswer { // EmbeddingPart is a latent state vector (JEPA, CLIP, ...) judged as-is, so a // caller with an encoder never has to decode it back to text first. +// Experimental: may change in a 1.x minor release (README, API stability). message EmbeddingPart { repeated float values = 1; string model = 2; // encoder that produced the vector, e.g. "v-jepa-2" diff --git a/scripts/e2e.refs b/scripts/e2e.refs new file mode 100644 index 0000000..4b60777 --- /dev/null +++ b/scripts/e2e.refs @@ -0,0 +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 diff --git a/scripts/e2e.sh b/scripts/e2e.sh new file mode 100755 index 0000000..63372a6 --- /dev/null +++ b/scripts/e2e.sh @@ -0,0 +1,179 @@ +#!/bin/sh +# End-to-end check of the console against KTMS and light-tokenmeter built +# from the refs in e2e.refs (or KTMS_REF / TOKENMETER_REF), with +# light-tokenmeter's authentication on and a mock vendor. +set -eu + +cd "$(dirname "$0")/.." +# 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} +P=ktms-admin-e2e-$$ +NET=$P-net +WORK=$(mktemp -d) +ADMIN_PORT=${ADMIN_PORT:-18090} +CONSOLE="http://127.0.0.1:$ADMIN_PORT" + +cleanup() { + docker rm -f -v "$P-pg" "$P-redis" "$P-mock" "$P-tm" "$P-ktms" "$P-admin" >/dev/null 2>&1 || true + docker network rm "$NET" >/dev/null 2>&1 || true + docker rmi -f "$P-admin-img" "$P-ktms-img" "$P-tm-img" "$P-mock-img" >/dev/null 2>&1 || true + rm -rf "$WORK" +} +trap cleanup EXIT +trap 'exit 130' INT TERM + +fail() { + echo "e2e: $*" >&2 + for c in ktms tm admin; do + echo "--- $c logs" >&2 + docker logs "$P-$c" 2>&1 | tail -n 20 >&2 || true + done + exit 1 +} + +# api METHOD PATH [JSON]: calls the console the way its UI does. Requests take +# protojson field names; responses use the proto names (plain_key). A refused +# call prints the console's error body to stderr. +api() { + out=$(curl -sS --connect-timeout 5 --max-time 60 -w '\n%{http_code}' -X "$1" "$CONSOLE$2" \ + -H 'X-Requested-With: ktms-admin' -H 'Content-Type: application/json' ${3:+-d "$3"}) || return 1 + code=$(printf '%s\n' "$out" | tail -n 1) + body=$(printf '%s\n' "$out" | sed '$d') + case $code in + 2??) printf '%s\n' "$body" ;; + *) echo "$1 $2 -> HTTP $code: $body" >&2; return 1 ;; + esac +} + +# build NAME CONTEXT [ARGS]: keeps the log quiet unless the build fails. +build() { + name=$1; shift + docker build -t "$P-$name-img" "$@" >"$WORK/$name.build.log" 2>&1 || { tail -n 40 "$WORK/$name.build.log" >&2; fail "building $name"; } +} + +wait_for() { + i=0 + until curl -fsS --max-time 5 "$1" >/dev/null 2>&1; do + i=$((i + 1)) + [ "$i" -gt 90 ] && fail "$1 did not answer" + sleep 1 + done +} + +echo "e2e: building ktms@$KTMS_REF, light-tokenmeter@$TOKENMETER_REF and this console" +git clone -q --depth 1 --branch "$KTMS_REF" https://github.com/ai-process/ktms.git "$WORK/ktms" || fail "cloning ktms@$KTMS_REF" +git clone -q --depth 1 --branch "$TOKENMETER_REF" https://github.com/ai-process/light-tokenmeter.git "$WORK/tm" || fail "cloning light-tokenmeter@$TOKENMETER_REF" +build admin . +build ktms --target slim "$WORK/ktms" +build mock "$WORK/ktms/bench/mock-upstream" +build tm "$WORK/tm" + +W=$(openssl rand -hex 16) +R=$(openssl rand -hex 16) +h() { printf %s "$1" | openssl dgst -sha256 -r | cut -d' ' -f1; } +BOOTSTRAP=$(openssl rand -hex 16) +KEYS="1:$(openssl rand -base64 32)" + +docker network create "$NET" >/dev/null +docker run -d --name "$P-pg" --network "$NET" -e POSTGRES_PASSWORD=pg postgres:17-alpine >/dev/null +docker run -d --name "$P-redis" --network "$NET" redis:7-alpine >/dev/null +docker run -d --name "$P-mock" --network "$NET" -e MOCK_REQUIRE_KEY=vk-e2e "$P-mock-img" >/dev/null +i=0 +until docker exec "$P-pg" pg_isready -h 127.0.0.1 -U postgres >/dev/null 2>&1; do + i=$((i + 1)) + [ "$i" -gt 60 ] && fail "Postgres did not become ready" + sleep 1 +done +docker exec "$P-pg" psql -qU postgres -c "CREATE DATABASE ktms" -c "CREATE DATABASE tokenmeter" >/dev/null || fail "creating databases" + +docker run -d --name "$P-tm" --network "$NET" \ + -e DATABASE_URL="postgres://postgres:pg@$P-pg:5432/tokenmeter?sslmode=disable" \ + -e TOKENMETER_TOKENS="ktms:write:$(h "$W"),console:read:$(h "$R")" "$P-tm-img" >/dev/null +docker run -d --name "$P-ktms" --network "$NET" \ + -e DATABASE_URL="postgres://postgres:pg@$P-pg:5432/ktms?sslmode=disable" -e KTMS_ENCRYPTION_KEYS="$KEYS" \ + -e BOOTSTRAP_ADMIN_KEY="$BOOTSTRAP" -e REDIS_ADDR="$P-redis:6379" -e ROUTING_RELOAD_SECONDS=1 \ + -e USAGE_COLLECTOR_HOST="$P-tm:9090" -e USAGE_COLLECTOR_TOKEN="$W" "$P-ktms-img" >/dev/null + +# run_console KEY TOKEN starts the console on localhost only, without sign-in. +run_console() { + docker rm -f "$P-admin" >/dev/null 2>&1 || true + docker run -d --name "$P-admin" --network "$NET" -p "127.0.0.1:$ADMIN_PORT:8080" \ + -e PUBLIC_URL="$CONSOLE" -e INSECURE_NO_AUTH=true \ + -e KTMS_ADDR="$P-ktms:9090" -e KTMS_ADMIN_KEY="$1" \ + -e TOKENMETER_URL="http://$P-tm:8080" -e TOKENMETER_TOKEN="$2" "$P-admin-img" >/dev/null + wait_for "$CONSOLE/healthz" +} + +# The bootstrap key mints the console's own key, as an operator would. +run_console "$BOOTSTRAP" "$R" +i=0 +until api GET /api/health 2>/dev/null | jq -e '.proxy.ok and .usage.ok' >/dev/null; do + i=$((i + 1)) + [ "$i" -gt 60 ] && fail "backends not healthy: $(api GET /api/health)" + sleep 1 +done +CONSOLE_KEY=$(api POST /api/proxy/keys '{"name":"console","scopes":["admin"]}' | jq -r .plain_key) +[ -n "$CONSOLE_KEY" ] && [ "$CONSOLE_KEY" != null ] || fail "minting the console key failed" +run_console "$CONSOLE_KEY" "$R" +api GET /api/health | jq -e '.proxy.ok and .usage.ok' >/dev/null || fail "health with the console key: $(api GET /api/health)" +echo "e2e: both backends reachable, light-tokenmeter with a read token" + +api PUT /api/proxy/models/mock-gpt '{"id":"mock-gpt","vendor":"openai","endpoint":"http://'"$P-mock"':8000/v1","efforts":["EFFORT_LOW","EFFORT_MEDIUM","EFFORT_HIGH"],"priceInPerMtok":1,"priceOutPerMtok":2,"enabled":true}' >/dev/null || + fail "upsert model" +api PUT /api/proxy/vendor-keys '{"apiKeyName":"console","vendor":"openai","plainKey":"vk-e2e"}' >/dev/null || fail "set vendor key" +api PUT /api/proxy/rules '{"rules":[{"name":"catch-all","priority":10,"use":["mock-gpt"],"enabled":true}]}' >/dev/null || fail "replace rules" +api GET /api/proxy/config | jq -e '(.models | map(.id) | index("mock-gpt")) and (.rules | map(.name) | index("catch-all"))' >/dev/null || + fail "config does not show the model and rule: $(api GET /api/proxy/config)" +echo "e2e: model, vendor key and rule configured through the console" + +api POST /api/proxy/keys '{"name":"e2e-client","scopes":["generate"]}' | jq -e '.plain_key | startswith("ktms_")' >/dev/null || fail "mint client key" +api GET /api/proxy/keys | jq -e '.keys | map(.name) | index("e2e-client")' >/dev/null || fail "client key not listed" +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/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/key-spend)" + 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" +api GET /api/proxy/teams | jq -e '.teams | map(.name) | index("e2e-team")' >/dev/null || fail "team not listed" +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, 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) +i=0 +until OUT=$(api POST /api/proxy/playground '{"prompt":"hello","effort":"low"}' 2>/dev/null) && + echo "$OUT" | jq -e '.resolved_model == "mock-gpt" and (.choices | length > 0)' >/dev/null; do + i=$((i + 1)) + [ "$i" -gt 30 ] && fail "playground did not answer through KTMS: ${OUT:-}" + sleep 1 +done +echo "e2e: playground answered through KTMS and the mock vendor" + +DAY=$(date -u +%F) +i=0 +until api GET "/api/usage?from=$SINCE&to=$DAY&group=model" | jq -e '(.report.groups // []) | map(select(.key == "mock-gpt" and .requests >= 1)) | length > 0' >/dev/null; do + i=$((i + 1)) + [ "$i" -gt 30 ] && fail "usage did not reach the console: $(api GET "/api/usage?from=$SINCE&to=$DAY&group=model")" + sleep 1 +done +echo "e2e: the call's usage went KTMS -> light-tokenmeter (write token) -> console (read token)" + +# A token without the read scope must show as refused, not as down. +run_console "$CONSOLE_KEY" "$W" +api GET /api/health | jq -e '.usage.ok == false and .usage.auth == true' >/dev/null || fail "a write-only token was not reported as refused: $(api GET /api/health)" +echo "e2e: a token without the read scope is reported as refused" +echo "e2e passed" diff --git a/scripts/proto-check.sh b/scripts/proto-check.sh new file mode 100755 index 0000000..26459c4 --- /dev/null +++ b/scripts/proto-check.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# KTMS owns ktms.v1: proto/ktms/v1 must be its copy at KTMS_REF (default: the +# pair in e2e.refs), whole directory, and internal/gen generated from the copy. +set -eu + +cd "$(dirname "$0")/.." +# shellcheck source=/dev/null +. ./scripts/e2e.refs +REF=${KTMS_REF:-$E2E_KTMS_REF} +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +git clone -q --depth 1 --branch "$REF" https://github.com/ai-process/ktms.git "$WORK/ktms" || + { echo "proto-check: cannot fetch ktms@$REF" >&2; exit 1; } +diff -ru "$WORK/ktms/proto/ktms/v1" proto/ktms/v1 || + { echo "proto-check: proto/ktms/v1 differs from ktms@$REF: copy the directory over and run make proto" >&2; exit 1; } + +# From scratch, so the output of a removed proto file shows up as a deletion. +rm -rf internal/gen +buf generate +if [ -n "$(git status --porcelain -- internal/gen)" ]; then + git status --short -- internal/gen >&2 + echo "proto-check: internal/gen is stale: run make proto and commit" >&2 + exit 1 +fi +echo "proto-check: proto/ktms/v1 matches ktms@$REF and internal/gen is current"