fix(generate): --print-input without --image needs no credential (#257) - #262
Open
ZacxDev wants to merge 1 commit into
Open
fix(generate): --print-input without --image needs no credential (#257)#262ZacxDev wants to merge 1 commit into
ZacxDev wants to merge 1 commit into
Conversation
`generate`'s RunE refused whenever no token was configured, before
runGenerate was ever entered. That contradicted the invariant stated in
the command's own comment at the --print-input short-circuit — "With no
--checkpoint/--lora there is no request of any kind" — and the reporter
measured the same thing from the other side: with a garbage token the
invocation completes fully offline, so the credential was never used.
The gate stays in ONE place and is narrowed rather than pushed down into
the call sites: it is skipped only when --print-input is set AND no
--image is present.
The condition is --image and not the issue's wider --image/--checkpoint/
--lora proposal. --print-input really does upload local --image files
(AGENTS.md item 19(f)) and hop 1 of that upload is authed (item 19(e)),
so --image genuinely needs a token even here. --checkpoint/--lora resolve
through the PUBLIC GET /api/v1/model-versions/{id} (item 13, "free,
unauthenticated-capable"), so gating on those would keep refusing a case
that demonstrably works with no credential.
Tests drive the real command through NewRootCmd with an isolated
XDG_CONFIG_HOME and CIVITAI_BASE_URL pointed at a recording httptest
server, so "no network" is a measured zero with its own positive control.
Classification is asserted with errors.Is, never message text (item 7).
Measured: "delete the gate entirely" SURVIVED a table built on exit code
plus offline-ness alone, because internal/auth's token source refuses a
credential-less request too, with its own ErrUnauthorized and without
dialing. The discriminator is the gate's place in the order — it runs
ahead of validateGenerateOpts, so an invocation that is both
credential-less and usage-invalid answers ErrUnauthorized today and flips
to ErrUsage the moment the gate is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #257.
The defect
internal/cmd/generate.go'sRunEcheckedcfg.Token() == ""and returned acivitai.ErrUnauthorized-tagged error unconditionally, beforerunGeneratewas ever entered. The--print-inputshort-circuit sits insiderunGenerateand returnsprintAssembledGraphbefore the estimator, the submit and the balance read.The strongest evidence is not the README — it is the code's own comment at that short-circuit, which already asserts:
So the command contradicted its own documented invariant. The reporter measured the same thing from the other side: with a garbage token (
CIVITAI_TOKEN=not-a-real-token-000)--print-inputsucceeds fully offline, which is what proves the credential is never used on that path.The fix
One line, at the existing gate — narrowed, not pushed down into the call sites (moving it would invite a "gate deleted for every path" mutation the tests would have to re-cover from scratch):
o.imagesis bound byStringArrayVar, so it is already populated at this point inRunE— noFlags().Changeddance needed.Why
--imageand not the issue's--image/--checkpoint/--loraThe issue proposes gating on all three. That is too wide:
--imagedoes need a credential even here.--print-inputreally uploads local files (AGENTS.md item 19(f)), and hop 1 of that upload —getConsumerBlobUploadUrl— is authed (item 19(e)).--checkpoint/--loraresolve through the publicGET /api/v1/model-versions/{id}, whichgenerate.go's own comment calls "free, unauthenticated-capable" (item 13). Gating on them would keep refusing a case that demonstrably works with no credential.An https
--imagevalue only reaches the credential-free fetch, so it does not strictly need a token. The gate still covers the whole flag: "some--imagevalues need a credential" is a worse contract than "--imagedoes", and the narrower rule buys nothing. Stated as a residual in the code comment rather than hidden.Tests —
internal/cmd/generate_print_input_auth_test.goEvery row drives the real command through
NewRootCmd()+SetArgs, with an isolatedXDG_CONFIG_HOME(t.TempDir()),CIVITAI_TOKENexplicitly cleared, andCIVITAI_BASE_URLpointed at a livehttptestserver that records every request. "No network" is therefore a measured zero, andTestGeneratePrintInput_NetworkRecorderPositiveControldrives that same recorder above zero through the same wiring so the zeros are not a recorder wired to nothing.Classification is asserted with
errors.Is, never message text (item 7). The JSON in the permitted case is decoded tomap[string]anyand checked key-by-key — astrings.Containscannot tellcfgfromcfgScale(item 14) — including thatquantityis present and2and that unsetstepsis absent.🔴 "Still ErrUnauthorized" was not enough to pin the gate, and that was measured
The first version of this table asserted exit code plus offline-ness on the three refusal rows. The whole "delete the gate" mutation survived it:
internal/auth's token source refuses a credential-less request too, with its ownErrUnauthorizedtag and without dialing. A control satisfied by a bystander — the exact near-miss shape AGENTS item 23 records.The discriminator is the gate's place in the order, asserted structurally rather than by message: it runs ahead of
validateGenerateOpts, so an invocation that is both credential-less and usage-invalid answersErrUnauthorizedtoday and flips toErrUsagethe moment the gate is gone. Those rows carryassertRefusedByTheGate.The
--checkpointrow exists for the same reason in the other direction: the issue's wider gate survived everything else, because nothing exercised--print-input --checkpointoffline. That row asserts the run reaches/api/v1/model-versions/carrying noAuthorizationheader — "a request went out" alone cannot distinguish a public read from an authed one, and the whole argument for excluding--checkpointrests on which it is.Mutation matrix
Each mutant is checksum-gated (md5 must move, gate line must occur exactly once) so an edit that silently failed to apply cannot read as a survivor.
M0is the unmutated null control.needsCredential := true(revert the fix — base8ed4d69)--print-input with no credential must succeed, got no token configured … (unauthorized=true);the credential gate still fired on a --print-input run that needs no credential;--print-input --checkpoint needs no credential …, but it was refusedneedsCredential := false(delete the gate)refused as a USAGE error, so the credential gate no longer runs ahead of validateGenerateOpts: --image requires --ecosystem …/… --quantity must be at least 1, got 0(×2)needsCredential := !o.printInput(drop the--imageterm)refused as a USAGE error, so the credential gate no longer runs ahead of validateGenerateOpts: --image requires --ecosystem …… || o.checkpointSet || len(o.loras) > 0(the issue's wider gate)--print-input --checkpoint needs no credential (the version read is public), but it was refused: …Note recorded honestly: under M2 and M3 the plain
--imagerefusal subtest stays green — the downstream token source carries it. That is why the discriminating subtest sits beside it rather than replacing it.Gate
make ci— rc=0,--- FAILcount = 0,build failedcount = 0, 18 packagesok(positive control on the count: 18 ≥ 1).gofmt -s -l .prints nothing over 297 Go files found (positive control: the tool found files).Not done, deliberately
No new AGENTS.md item (six agents are working in parallel and would collide on the number). Nothing in the existing text became inaccurate: item 19(f) still describes the
--imageupload correctly, and item 13's "free, unauthenticated-capable" is now load-bearing for the gate rather than merely descriptive. If a maintainer wants an item, the substance is the "still ErrUnauthorized was not enough" measurement above.README untouched (outside this PR's file ownership). It is not made inaccurate by this change — it says
--print-input"reaches no money seam", which stays true — but it also never says the command works without a credential. Suggested addition to theRaw graphssection, for whoever owns README next:No standalone-binary reproduction. The symptom is reproduced in-process through the real
NewRootCmd()(which is exactly whatmainexecutes;mainonly maps the returned error to an exit code, and that mapping is pinned elsewhere). The sandbox refusesHOME/XDG_CONFIG_HOMEoverrides in a shell, so running./bin/civitaiagainst an isolated config was not possible here — flagged rather than claimed.🤖 Generated with Claude Code