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:
1 parent
4eb49d0e4c
commit
d1bf26528b
4 files changed
+51
-1
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user