From 11603e31b7a7566d5530c55fe916f479986916f9 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Wed, 23 Sep 2026 14:25:57 -0700 Subject: [PATCH] fix(console): honor gpu model filters in bid screening Encode GPU models in resource attribute keys so Console can filter providers. Keep empty screening results as arrays in JSON and YAML. Add wire-request and output regression tests. Console automatic funding from issue #103 is already covered by #94. Closes #103 Signed-off-by: Joseph Chalabi --- .changelog/console-screen-gpu-model.fixed.md | 4 + DESIGN.md | 5 + SPEC.md | 10 +- internal/cli/console/gateway.go | 4 +- internal/cli/console/gateway_test.go | 100 +++++++++++++++++++ 5 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 .changelog/console-screen-gpu-model.fixed.md diff --git a/.changelog/console-screen-gpu-model.fixed.md b/.changelog/console-screen-gpu-model.fixed.md new file mode 100644 index 0000000..d46d003 --- /dev/null +++ b/.changelog/console-screen-gpu-model.fixed.md @@ -0,0 +1,4 @@ +- Fix Console bid screening GPU model filters by sending the model in the + resource attribute key with value `true`, including when overriding an SDL. + Return an empty array in JSON/YAML when no providers match, while preserving + the pretty output message. Add request and empty-output regression tests. diff --git a/DESIGN.md b/DESIGN.md index 82bd175..6a0384b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -529,6 +529,11 @@ API key, and explicit command groups keep their own transport boundaries. This prevents each surface from acquiring its own partial version of the same rules. +Public Console bid screening translates `--gpu-model ` into the resource +GPU attribute `vendor/nvidia/model/=true`, matching the API's model +selector format. A model with no matching providers yields an empty list in +JSON/YAML and a human-readable message in pretty output. + A context may use both credentials. `auth-method` remains the on-disk name for compatibility and records only which credential `akt deploy`, `akt update`, and `akt close` prefer. `akt context create/edit --deploy-via chain|console` diff --git a/SPEC.md b/SPEC.md index 3ad13a2..9c481a4 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2933,6 +2933,14 @@ one-shot read. | `akt console shell [service] [-- command...]` | `--stdin` | Interactive shell in a lease container, default `/bin/sh`; when exactly one SDL service exists it is selected automatically. Multiple services require an explicit name and list the choices. Exec is the same operation with an explicit command (JWT scopes `shell,status`). | | `akt console screen [sdl-file]` | `--cpu`, `--memory`, `--storage`, `--gpu`, `--gpu-model`, `--count`, `--attribute`, `--signed-by`, `--reclamation-window` | Client-side bid screening from an SDL, resource flags, or both. Explicit flags override SDL-derived fields (public endpoint, no key needed). | +`console screen --gpu-model ` MUST encode the model in each screened +resource's `gpu.attributes` as +`{"key":"vendor/nvidia/model/","value":"true"}`. This applies both to +resource-only requests and to GPU overrides of an SDL. The model name MUST NOT +be sent as the value of a `vendor/nvidia/model` attribute, which does not constrain +the API's provider selection. When no providers match, JSON and YAML output +MUST be an empty array (`[]`); pretty output retains the no-match message. + Per the positional-primary convention (§3.8), every console command takes its primary value(s) positionally; the equivalent flags remain as overrides and a positional value wins when both are given. (2026-07: the flag twins marked *disabled pending feedback* above are commented out in code for the positional-only UX trial — the positional form is the only way while the trial runs; the original flag definitions are preserved in `FEEDBACK(2026-07)` comments for restoration.) Default structured reads are indented JSON, while human acknowledgements and streams use the command-specific pretty forms described below. USD values at or above one cent render with two decimals. A nonzero sub-cent value renders with up to six decimals and trailing zeros stripped; a magnitude below one millionth of a dollar renders as `$<0.000001` (or `-$<0.000001`) rather than the false `$0.00`. Zero remains `$0.00`. State-changing calls are recorded in the context's action log as `type=console` entries (§5.6). No command ever prints a Console API key, except the one-time secret from `apikey create`. Console escrow `uact` values are micro-USD: amounts render as dollars and @@ -4947,7 +4955,7 @@ Status of `akt` coverage for every Akash Console capability. "Covered" means the | Live lease status | `akt console status ` (`--watch`) | Reads the provider gateway directly using a Console-minted scoped JWT. | | Container logs / cluster events | `akt console logs [service]`, `akt console events ` (`--follow`) | Same streaming paths as `akt provider lease-logs/lease-events`, authenticated by the Console JWT — no websocket relay needed. | | Exec / interactive shell | `akt console shell [-- command]` | Exec is the same command with an explicit command argument. | -| Bid screening | `akt console screen ` | Public endpoint; resources are derived from the SDL. | +| Bid screening | `akt console screen [sdl-file]` | Public endpoint; resources come from the SDL, resource flags, or both. GPU model filtering and empty output follow §2.9. | | Wallet balances & managed wallets | `akt console wallet balance/list` | Balances are µACT rendered as USD (1 ACT = 1 USD); wallet credits are dollar-scale. | | Auto Recharge | `akt console wallet settings [true\|false]` | Account-level card charging (`autoReloadEnabled`), the Console UI's "Auto Recharge". Distinct from per-deployment funding, which is always on and not configurable. | | Cost estimate & usage history | `akt console wallet cost`, `akt console usage [from] [to]` | Usage totals the requested range; the lifetime figure is reported separately. | diff --git a/internal/cli/console/gateway.go b/internal/cli/console/gateway.go index d423edc..1557d9c 100644 --- a/internal/cli/console/gateway.go +++ b/internal/cli/console/gateway.go @@ -615,7 +615,7 @@ func screenCmd(mgrFn func() *aktctx.Manager) *cobra.Command { } if len(providers) == 0 { - return printConsoleText(cmd, "No providers matched the SDL's resource requirements.\n") + return printConsoleResult(cmd, "No providers matched the SDL's resource requirements.", []console.ScreenedProvider{}) } return printJSON(cmd, providers) @@ -703,7 +703,7 @@ func screeningRequestFromResources(cmd *cobra.Command, args []string, raw json.R } gpuValue := map[string]any{"units": map[string]any{"val": strconv.FormatUint(uint64(gpu), 10)}} if model != "" { - gpuValue["attributes"] = []any{map[string]any{"key": "vendor/nvidia/model", "value": model}} + gpuValue["attributes"] = []any{map[string]any{"key": "vendor/nvidia/model/" + model, "value": "true"}} } resourceValue["gpu"] = gpuValue } diff --git a/internal/cli/console/gateway_test.go b/internal/cli/console/gateway_test.go index 85a8d42..0d8956d 100644 --- a/internal/cli/console/gateway_test.go +++ b/internal/cli/console/gateway_test.go @@ -805,6 +805,106 @@ func TestDefaultShellServiceCoversManifestBoundaries(t *testing.T) { } } +func TestScreenGPUModelRequest(t *testing.T) { + tests := []struct { + name string + model string + useSDL bool + }{ + {name: "rtx5090", model: "rtx5090"}, + {name: "a100", model: "a100"}, + {name: "unknown model", model: "totally-fake-gpu"}, + {name: "SDL override", model: "a100", useSDL: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var req struct { + Resources []struct { + Resource struct { + GPU struct { + Units struct { + Val string `json:"val"` + } `json:"units"` + Attributes []aktconsole.Attribute `json:"attributes"` + } `json:"gpu"` + } `json:"resource"` + } `json:"resources"` + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/v1/bid-screening" { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Errorf("decode screening request: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + writeJSON(t, w, `{"providers":[{"owner":"akash1prov"}]}`) + })) + defer srv.Close() + + args := []string{"screen"} + if test.useSDL { + sdlPath := filepath.Join(t.TempDir(), "deploy.yaml") + if err := os.WriteFile(sdlPath, []byte(screenSDL), 0o600); err != nil { + t.Fatal(err) + } + args = append(args, sdlPath) + } + args = append(args, "--gpu", "3", "--gpu-model", test.model) + if _, err := execConsole(t, newTestManager(t), srv.URL, args...); err != nil { + t.Fatalf("screen: %v", err) + } + if len(req.Resources) != 1 { + t.Fatalf("resources length = %d, want 1", len(req.Resources)) + } + gpu := req.Resources[0].Resource.GPU + if gpu.Units.Val != "3" { + t.Errorf("GPU units = %q, want 3", gpu.Units.Val) + } + want := []aktconsole.Attribute{{Key: "vendor/nvidia/model/" + test.model, Value: "true"}} + if !reflect.DeepEqual(gpu.Attributes, want) { + t.Errorf("GPU attributes = %#v, want %#v", gpu.Attributes, want) + } + }) + } +} + +func TestScreenEmptyOutput(t *testing.T) { + for _, format := range []string{"pretty", "json", "yaml"} { + t.Run(format, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeJSON(t, w, `{"providers":[]}`) + })) + defer srv.Close() + + out, err := execConsole(t, newTestManager(t), srv.URL, + "screen", "--gpu-model", "totally-fake-gpu", "-o", format) + if err != nil { + t.Fatalf("screen: %v", err) + } + if format == "pretty" { + if want := "No providers matched the SDL's resource requirements.\n"; out != want { + t.Errorf("empty pretty output = %q, want %q", out, want) + } + return + } + var decoded any + if format == "json" { + err = json.Unmarshal([]byte(out), &decoded) + } else { + err = yaml.Unmarshal([]byte(out), &decoded) + } + if err != nil { + t.Fatalf("decode %s output: %v", format, err) + } + if providers, ok := decoded.([]any); !ok || len(providers) != 0 { + t.Errorf("empty %s output = %#v, want an empty array", format, decoded) + } + }) + } +} + func TestScreeningRequestSupportsResourceFlagsWithoutSDL(t *testing.T) { cmd := screenCmd(func() *aktctx.Manager { return nil }) for flag, value := range map[string]string{