keys limits changes only the limits it is given
CI / test (push) Successful in 5m25s

Setting one limit at a scope wrote the whole row, so a tool grant given
alone cleared the scope's rate, budget and models. --replace writes it
whole, as the console's form, which shows every field, now asks for.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-20 17:32:43 +02:00
1 parent 4eb49d0e4c
commit d1bf26528b
4 files changed
+51 -1

No files matched your search

+4
View File
@@ -61,6 +61,10 @@ APIs change between minors without a deprecation period.
### Fixed
- **`llmgw keys limits` no longer clears the limits it was not given.** Setting one limit at a scope wrote the whole
row, so `--tools 'jira.*'` alone took away that scope's rate, budget and model list. Only what is given changes now.
`--replace` writes every limit, as the console's limits form does.
- **The plan refused tensor parallelism past a model's KV heads.** Qwen3-235B, with four KV heads, had no way to run
on eight cards, though vLLM gives each card a copy of a head. A width that is a multiple of the KV heads is now
offered, as is one that divides them.
+15
View File
@@ -303,6 +303,7 @@ func newLimitsCmd(open opener) *cobra.Command {
var rpm, tpm int
var budget float64
var models, tools []string
var replace bool
cmd := &cobra.Command{
Use: "limits",
@@ -358,7 +359,19 @@ func newLimitsCmd(open opener) *cobra.Command {
return errors.New("--per-member is for --scope org, team or project")
}
}
// Only what is given changes: setting a tool grant must not
// clear the scope's rate or budget. --replace writes the row
// whole, a flag not given then no limit, as the console's
// form does with every field it shows.
l := store.Limits{Scope: level, ScopeID: scopeID}
if !replace {
switch cur, err := s.LimitsFor(cmd.Context(), level, scopeID); {
case err == nil:
l = cur
case !errors.Is(err, store.ErrNotFound):
return err
}
}
if cmd.Flags().Changed("rpm") {
l.RPM = &rpm
}
@@ -404,6 +417,8 @@ func newLimitsCmd(open opener) *cobra.Command {
cmd.Flags().Float64Var(&budget, "budget", 0, "budget per period, in whole units as the quotas page shows them")
cmd.Flags().StringVar(&period, "period", "month", "day | month")
cmd.Flags().StringSliceVar(&models, "models", nil, "allowed models; empty allows none")
cmd.Flags().BoolVar(&replace, "replace", false,
"write every limit at the scope: one not given is then no limit there (default: only what is given changes)")
cmd.Flags().StringSliceVar(&tools, "tools", nil,
"the company MCP servers' tools granted here, as patterns: jira.*,db.query; none granted anywhere is none")
return cmd
+30
View File
@@ -131,3 +131,33 @@ func TestKeyBudgetsAreWholeUnits(t *testing.T) {
}
}
}
// A limit given alone changes that limit alone: granting a tool keeps the
// scope's rate. --replace writes the row whole, as the console's form does.
func TestALimitGivenAloneLeavesTheOthersAtTheScope(t *testing.T) {
db := filepath.Join(t.TempDir(), "s.db")
mustRun(t, db, "org", "add", "org", "Acme")
mustRun(t, db, "org", "add", "team", "platform")
mustRun(t, db, "org", "add", "project", "gateway")
mustRun(t, db, "keys", "limits", "--scope", "project", "--id", "gateway", "--rpm", "30")
mustRun(t, db, "keys", "limits", "--scope", "project", "--id", "gateway", "--tools", "jira.*")
read := func() store.Limits {
s, err := store.Open(context.Background(), db)
if err != nil {
t.Fatal(err)
}
defer s.Close()
all, _ := s.AllLimits(context.Background())
for _, l := range all {
return l
}
return store.Limits{}
}
if l := read(); l.RPM == nil || *l.RPM != 30 || len(l.AllowedTools) != 1 {
t.Errorf("after a tool grant: rpm %v, tools %v", l.RPM, l.AllowedTools)
}
mustRun(t, db, "keys", "limits", "--replace", "--scope", "project", "--id", "gateway", "--tpm", "1000")
if l := read(); l.RPM != nil || l.AllowedTools != nil || l.TPM == nil {
t.Errorf("after --replace: %+v", l)
}
}
+2 -1
View File
@@ -407,7 +407,8 @@ func (m *consoleModel) limitsForm(scope store.Scope, id, who string) {
textF("company tools", tools, "MCP tools granted here, as jira.*,db.query; empty grants none here"),
},
submit: func(m *consoleModel, v []string) (tea.Cmd, error) {
args := []string{"keys", "limits", "--scope=" + string(scope), "--id=" + id}
// Every field the form shows, so an emptied one is cleared.
args := []string{"keys", "limits", "--replace", "--scope=" + string(scope), "--id=" + id}
for i, flag := range []string{"rpm", "tpm"} {
if v[i] != "" {
if _, err := strconv.Atoi(v[i]); err != nil {