Conversation
cliValue.SetInnerField dispatched the slice case on the static element kind, so an untyped Flag[any] holding a []any from a JSON or YAML array literal returned before the merge path and the inner-field value was dropped while the flag still counted as supplied. Dispatch elements by their dynamic type and check the trailing-element assertion, so a Flag[[]map[string]any] outer keeps its existing behavior and a []any outer follows the same merge or append-new-element rules.
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.
Summary
For a body parameter that is a nullable array of objects, codegen emits an untyped
requestflag.Flag[any]plusInnerFlagchildren markedOuterIsArrayOfObjects. Combiningan array literal on the outer flag with any inner flag silently discards the inner value:
request body built from
ExtractRequestContents:{"model":"gpt-4o-mini","training_file":"file-abc123","integrations":[{"type":"wandb"}]}wandbis gone — no error, no warning, and--integration.wandbstill counts as supplied,so nothing downstream notices. Expected
{"integrations":[{"type":"wandb","wandb":{…}}]}.Problem
cliValue.SetInnerFielddispatches the slice case on the static element kind(
internal/requestflag/requestflag.go):A
Flag[any]outer holds a[]anyas soon as it is set from a JSON or YAML array literal,and
[]any's element kind isreflect.Interface, notMap. Every inner-field assignmenttherefore returns at that guard, while
Flag.SetInnerFieldgoes on to sethasBeenSet = true. The typed outerFlag[[]map[string]any](element kindMap) takes themerge path — which is the behavior the existing
TestInnerFlagAfterNullArrayElementcasespin, including "merge into the trailing element" and "repeated field starts another element".
Two details in the surrounding code say the
[]anyshape was meant to work: the appendswitch already has a
case []any:arm that the guard made unreachable, andinnerFieldIsSetinstdinprovenance.goalready reads the trailing element of a[]anywith a checked assertion.
Affected parameters, all five
Flag[any]with array-of-objects inner flags (verified in--helpof the built binary):fine-tuning:jobs create --integration,responses create --context-management,beta:responses create --context-management,beta:threads:runs create --additional-message,beta:threads:messages create --attachment.Live request capture
Same command against a local recorder on
127.0.0.1:4011(synthetic data, no live endpoint),binary built from
0169bffvs from this branch:POST /fine_tuning/jobsbody,integrations0169bff[{"type": "wandb"}][{"type": "wandb", "wandb": {"entity": "me", "project": "my-project"}}]Both runs exit 0 and get a
200— the only signal that the field was lost is the body itself.Fix
Dispatch elements by their dynamic type, and merge only into a trailing object:
map[string]any→ existing rule (merge unless it already carries the field;fill a nil map in place).
nullelement, or a trailingelement that already has the field → append a new object element, i.e. the same
"repeated field starts another element" rule the typed path already uses. Nothing the user
typed is discarded either way.
Interface().(map[string]any)becomes checked, so widening the guard cannotturn the assertion below it into a panic path.
No new limits, no error paths, no signature changes:
SetInnerField's contract and theFlag/InnerFlagwiring are untouched. This is the remaining half of the gap #162 closed:that change made a
nullelement inside a typed[]map[string]anywork, while the guardabove still drops every inner flag whose outer is an untyped
Flag[any].Tests
New
internal/requestflag/innerflag_untyped_array_test.go, 8 cases. Seven run through thereal
cli.Command.Run→ExtractRequestContentspath (the same map the request body isbuilt from), one drives
Flag/InnerFlagdirectly to mirror the existingnull-elementtest:
--entry '[{"name":"earlier"}]' --entry.description details[{"name":"earlier","description":"details"}]--entry '[{"name":"earlier"}]' --entry.name demo[{"name":"earlier"},{"name":"demo"}]--entry '[{}]' --entry.name demo --entry.description details[{"name":"demo","description":"details"}]--entry '- {name: earlier}' --entry.description details(YAML flow)[{"name":"earlier","description":"details"}]--entry '["plain"]' --entry.name demo["plain",{"name":"demo"}]--entry '[null]' --entry.name demo[null,{"name":"demo"}]--entry '[]' --entry.name demo[{"name":"demo"}]On base
0169bff(same test file,requestflag.gorestored fromgit show HEAD:…) all 8fail, each dropping the inner field:
With the patch all 8 pass, and the pre-existing
internal/requestflagsuite passesunchanged, so the typed
[]map[string]any/map[string]anyvectors are unaffected.For context on the syntax: inner flags are not listed in
--help, but they are codegen'dfor exactly these parameters and are the style the generated
inner flagssubtests drive(
TestFineTuningJobsCreateinpkg/cmd/finetuningjob_test.gopasses--integration.typeand
--integration.wandb). Those tests use only inner flags, which is why the regressionescaped: the lost field needs an array literal and an inner flag in the same command.
Validation
cwd
.runtime/2026-09-20/oa/src, base0169bff(=origin/main), go1.25.0 darwin/arm64,network proxies unset,
-count=1where shown:go test ./internal/requestflag/ -run 'TestInnerFlagAfterUntypedArrayLiteral' -v(unpatched)go test ./internal/requestflag/ -run 'TestInnerFlagAfterUntypedArrayLiteral' -v(patched)go test ./internal/...go test ./internal/requestflag/ -race -count=1go test ./... -run '^$'go test ./pkg/cmd -count=1with./scripts/mockon127.0.0.1:4010TestFilesCreateCLICancelClosesStalledFIFOgo test ./pkg/cmd -run TestFilesCreateCLICancelClosesStalledFIFOon base, patch revertedmultipartresource_unix_test.go)go vet ./..../scripts/lintgofmt -l internal/requestflag/go mod verifygo build -o /tmp/oai ./cmd/openai(patched andgit show HEAD:variants), each run against a local127.0.0.1:4011recorderGOOS=windows GOARCH=amd64 go build ./...,GOOS=linux GOARCH=arm64 go test -c ./internal/requestflag/The mock-server dependency was reviewed before running it:
scripts/steady/manifest.jsonpins the Steady commit and Deno runtime,
./scripts/steady/installverified the source andruntime digests, and the server bound to
127.0.0.1:4010with synthetic data only../scripts/testitself was not run (it additionally cross-compiles the whole test tree); thepkg/cmdsuite above is the mock-backed part that covers this change.internal/requestflag/requestflag.gois handwritten behavior perAGENTS.md, and the newtest file is wholly handwritten, so no generated-owned file changes and nothing in the
Castiron generation baseline moves. The change is in request parsing, so CODEOWNER review is
expected.
AI-assisted: an AI coding agent produced this change under the account owner's standing
instruction for this repo, ran every command above and read its output. No human reviewed
the diff before it was opened.