feat: agent-friendly cli output (CON-683, CON-684, CON-685)#306
feat: agent-friendly cli output (CON-683, CON-684, CON-685)#306justinwlin wants to merge 4 commits into
Conversation
CON-683: clean cli error output for agent consumption
- set SilenceUsage (SilenceErrors already set) so runtime errors no longer
dump the flag/usage block; usage is re-printed only for genuine usage
errors (bad flags, wrong arg count, unknown command) via a usageError type
- Execute is now the single error sink: one flat JSON error object on stderr
with a stable "code" field (and http "status" when available), instead of
each command printing then Execute printing again
- unwrap nested API error JSON in the rest client: APIError is now a real
error type carrying the unwrapped message + code + status, so errors read
{"error":"pod not found","code":"not_found","status":404} instead of
{"error":"api error: {\"error\":\"pod not found\"} (status 404)"}
- remove the now-redundant per-command output.Error(err) calls
CON-684: include invoke urls in serverless output
- add urls (run, runsync, health) to serverless create, get, and list output,
computed client-side from the endpoint id
CON-685: add pricing and per-datacenter availability to gpu list
- add secure/community on-demand $/hr fields (securePrice, communityPrice)
- add per-datacenter availability breakdown alongside the best overall stock
- asUsageError: short-circuit typed *api.APIError / *api.GraphQLError so a
runtime failure whose server message starts with a usage-ish word (e.g.
"invalid argument: ...") keeps its real code and doesn't trigger a usage
dump; tighten the string-prefix fallback (drop the broad "requires ").
- add GraphQLError type with a stable "graphql_error" code + http status, and
route all graphql error constructions through it; unwrap the graphql http
transport body ({"errors":[{message}]}) instead of embedding the raw json.
- normalize explicit api error codes to lowercase.
- document the stable error-code vocabulary; drop dead FormatError.
- tests: parseGraphQLHTTPError unwrap/code, GraphQLError shape, and
asUsageError typed-error short-circuit.
an empty stockStatus is noise (the gpu exists in the dc but has no reported availability); only list data centers with a real status.
VerificationVerified with unit tests + live e2e against the real API. All checks green. Automated: Live e2e (real CON-685 —
|
do not silently drop data centers with no reported stock; list them with an explicit "none" status so an agent can distinguish "offered here, no stock" from a missing entry.
|
Promptless prepared a documentation update related to this change. Triggered by runpodctl PR #306 Documented the three agent-friendly output changes in the runpodctl CLI reference: the new Review: Document runpodctl agent-friendly output (pricing, invoke URLs, error format) |
LarveyOfficial
left a comment
There was a problem hiding this comment.
Overview
Three linked agent-legibility improvements to runpodctl v2: (683) a single flat JSON error sink with stable code/status fields, SilenceUsage, unwrapped API/GraphQL errors, and removal of ~88 redundant per-command output.Error calls; (684) client-computed invoke urls on serverless create/get/list; (685) on-demand pricing + per-datacenter availability in gpu list.
Strengths
- The error-handling redesign is the right architecture. Making
APIError/GraphQLErrorreal error types withErrorCode()/HTTPStatus(), then havingoutput.Errorpull those out viaerrors.Asthrough the wrap chain, means a wrappedfmt.Errorf("failed to X: %w", apiErr)still surfaces the correct code/status. Collapsing 88 call sites into a singleExecutesink is a genuine consistency win and removes the double-print/usage-dump bug. SilenceUsage+ typedusageErrorsplit is correct. Usage text now shows only for real usage mistakes, and theSetFlagErrorFuncroute +nilno-op guard inoutput.Errorare good touches.- Good, targeted tests —
parseAPIError(unwrap/code/status), the "must not be double-encoded" assertions, usage-vs-runtime classification including the typed-error bail, and the per-DC "none" vs missing distinction.
Concerns / suggestions
-
asUsageErrorstring-prefix fallback is the main fragility. After the (good) typedAPIError/GraphQLErrorbail-out, any plainfmt.Errorfruntime error whose message happens to start with"invalid argument","accepts ","requires at least", etc. would be misclassified as a usage error and dump usage text. Today's plain runtime errors don't collide (e.g.pod/list.go's"invalid --created-after format…"starts with"invalid ", not"invalid argument"), but this couples correctness to Cobra's internal message strings and to every command author avoiding those prefixes.TestAsUsageErrorguards the Cobra strings, which helps — consider a short comment warning command authors off those leading words, and note it may need revisiting on a Cobra upgrade. -
Hardcoded invoke base
https://api.runpod.ai/v2ininvokeURLs. Correct as long as the serverless invoke domain is always prod-global and independent of the control-plane API URL. If someone points runpodctl at a dev/staging API, the emittedurlswould still target prod — worth a one-line confirmation that invoke is always this host regardless ofapiUrl. -
GraphQL errors collapse to a single
graphql_errorcode with no status (200-with-errorsbody). Acceptable and documented, but it means an agent can't branch a GraphQL "not found" the way it can a REST 404. Fine as a known tradeoff — flagging only so it's a conscious one.
Nits
- The
APIErrorfield renameError→Message(method vs field) is contained tointernal/with no external importers andgo build/go vetpass, so no compatibility risk — just the kind of change worth a line in the description. - PR is still marked draft; scope (51 files, 3 tickets) is large but cohesive. No objection to the bundling given they're one theme.
Solid, well-tested work with the right architecture. Only #1 (string-prefix fragility) is worth a follow-up thought; nothing blocking.
Summary
Agent-friendly output improvements to the runpodctl v2 surface, covering three linked tickets from the CON_2026 Q3 Agents Emergency Triage rock. All three are "make the CLI legible to an agent driving it."
gpu listCON-683: clean CLI error output
Before, a runtime failure printed the error JSON, then the full flag/usage dump, then the error again — and API errors were double-encoded (
{"error":"api error: {\"error\":\"pod not found\"} (status 404)"}), so agents misread runtime failures as flag mistakes.SilenceUsage(SilenceErrorswas already set). Usage is now re-printed only for genuine usage errors (bad flags, wrong arg count, unknown command) via ausageErrortype +SetFlagErrorFunc.Executeis now the single error sink: one flat JSON error object on stderr with a stablecodefield (and HTTPstatuswhen available). Removed the redundant per-commandoutput.Error(err)calls (88 sites).APIErroris now a realerrortype carrying the unwrapped message + code + status.CON-684: invoke URLs in serverless output
serverless create/get/listnow include aurlsobject (run,runsync,health) computed client-side from the endpoint id, so an agent that just deployed an endpoint can call it without a UI round-trip.CON-685: pricing + per-datacenter availability in
gpu listAdded secure/community on-demand
$/hr(securePricePerHr,communityPricePerHr) and a per-datacenter availability breakdown alongside the existing best-overallstockStatus, so agents can cost-optimize and pick a DC that will actually schedule.Testing
go build ./...,go vet ./...,go test ./...— 237 pass.parseAPIError(unwrap/code/status),output.Errorwith code/status + nil no-op, usage-error classification +SilenceUsage/SilenceErrors,invokeURLs+ get/list URL population,ListGpuTypespricing + per-DC aggregation.RUNPOD_API_KEY):gpu list(pricing + per-DC), runtime/usage/unknown-command/unknown-flag error shapes, and fullserverless create→get→list→deletecycle (created a temporary serverless template + endpoint withworkers-min 0, verifiedurls, then deleted both — no lingering resources).Follow-up
gpu listfields,urls, and error shape. Not in this PR.Kept as a draft for your review.