Skip to content

fix: resolve omitted Optional inputs to None at the HTTP edge#3051

Merged
markphelps merged 1 commit into
mainfrom
fix/optional-input-runtime-default
Jun 8, 2026
Merged

fix: resolve omitted Optional inputs to None at the HTTP edge#3051
markphelps merged 1 commit into
mainfrom
fix/optional-input-runtime-default

Conversation

@markphelps

@markphelps markphelps commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

A predictor input declared Optional[T] / T | None is emitted as an optional field in the OpenAPI schema (nullable: true, absent from required), but whether it can actually be omitted at request time depended on a Python-level detail the schema doesn't capture: whether the parameter had a usable __defaults__ entry.

Signature Omitted at runtime
value: Optional[str] (bare, no Input) TypeError: missing 1 required positional argument
value: Optional[str] = Input(description="…") ✅ resolves to None
value: Optional[str] = Input(default=None) ✅ resolves to None

All three generate a correct "optional" schema. The bare form was a silent footgun: it built and served fine, advertised the field as optional, then failed at prediction time whenever a caller omitted it.

Fix

Validation against the OpenAPI schema already runs at coglet's HTTP edge (InputValidator), not in the worker. After validation passes, inject an explicit null for every property that is nullable: true, not in required, and has no default key, when it is absent from the request.

This rule mirrors the schema-generation discriminator in pkg/schema/openapi.go (buildInputSchema), keeps the OpenAPI schema as the source of truth for what is optional, and leaves the worker unchanged (it needs no schema).

The rule safely skips:

  • Optional[T] = Input(default=None) — emits default: null, already handled by unwrap_field_info_defaults
  • real defaults like Input(default=42) — emits default: 42
  • required fields — the validator rejects omission first

Injection runs only after successful validation, so missing required fields still return 422. Training inputs (TrainingInput) are covered via the same path. Union optionals (A | B | None) are forced into required by the generator and are correctly skipped (they 422 on omission rather than resolving to None — pre-existing behavior, not a regression).

Tests

  • Unit (input_validation.rs): inject for bare optional / optional list / optional enum; skip explicit-default and required fields; never override a present value.
  • Unit (service.rs): predict injects after Ok, does not inject when a required field is missing, train path injects.
  • Integration (optional_input_no_default.txtar): a bare value: Optional[str] omitted via both cog predict and POST /predictions {"input":{}} resolves to None.

A predictor input declared `Optional[T]` / `T | None` is emitted as an
optional field in the OpenAPI schema (nullable, absent from `required`),
but whether it can actually be omitted at request time depended on a
Python-level detail: whether the parameter had a usable `__defaults__`
entry. A bare `value: Optional[str]` (no `= Input(...)`) built and served
fine, advertised the field as optional, then raised
`TypeError: missing 1 required positional argument` whenever a caller
omitted it.

Fix this in coglet's HTTP edge, where input validation already runs with
the OpenAPI schema in hand. After validation passes, inject an explicit
`null` for every property that is `nullable: true`, absent from
`required`, and has no `default` key. This mirrors the schema-generation
discriminator in pkg/schema/openapi.go and keeps the schema the source of
truth for what is optional. The worker is unchanged and needs no schema.

The rule safely skips `Optional[T] = Input(default=None)` (emits
`default: null`), real defaults, and required fields. Injection runs only
after successful validation, so missing required fields still 422.
Training inputs (TrainingInput) are covered via the same path. Union
optionals remain in `required` by design and are correctly skipped.

Tests:
- unit (input_validation.rs): inject for bare optional, optional list,
  optional enum; skip explicit-default and required fields; never
  override a present value.
- unit (service.rs): predict injects after Ok, does not inject when a
  required field is missing, train path injects.
- integration: optional_input_no_default.txtar exercises a bare
  `value: Optional[str]` via cog predict and POST /predictions.
@markphelps markphelps requested a review from a team as a code owner June 8, 2026 17:53
@ask-bonk

ask-bonk Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

LGTM

github run

@markphelps markphelps requested a review from pfrohlich June 8, 2026 18:08
@markphelps markphelps merged commit e306412 into main Jun 8, 2026
75 of 78 checks passed
@markphelps markphelps deleted the fix/optional-input-runtime-default branch June 8, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants