fix(provider): wizard honors default=None — never force-writes choice/boolean values (in_memory defect) - #284
Conversation
…/boolean values
Root cause: `_prompt_for_field`'s choice branch had no representation for
"unset". It computed `effective_value = existing_value or default` and then
hard-coded `default_choice = "1"`, so a provider `ConfigField` declaring
`default=None` (deliberately, to mean "leave unset — use provider/model
default") with `choices=[...]` silently collapsed to `choices[0]` and ALWAYS
wrote a value on Enter.
Causal chain of the real-world defect: provider-openai declares
`prompt_cache_retention` with `default=None` + `choices=["in_memory","24h"]`
-> the wizard force-wrote `in_memory` into settings.yaml -> gpt-5.6 models
reject that value at runtime, producing a drop-warning every session. The
same bug force-wrote `text_verbosity=low` for openai and would force-write
similarly-shaped anthropic choice fields the moment they declared
`default=None`. The boolean branch had a related latent bug:
`default and default.lower()` with `default=None` evaluates to `None`
(short-circuit), which `Confirm.ask(default=None)` still accepts as a
default — so pressing Enter wrote the literal string `"none"`.
Fixes (amplifier_app_cli/provider_config_utils.py):
1. Choice branch (_prompt_for_field): when `default is None` and the field
is not required, prepend a "(leave unset — use provider/model default)"
option and make it the default selection; choosing it returns None so the
caller omits the key — matching the existing text-field "may be absent"
contract. A real existing_value still takes priority as the pre-selected
option. required=True fields keep the original choices[0]-default
behavior unchanged.
2. Boolean branch (_prompt_for_field): guard `default is None` explicitly.
Confirm.ask is now called with an explicit default=None (not a computed
None from `and`-chaining) and pressing Enter returns None -> key omitted.
Never writes "none".
3. Removed the dead `cli_overrides` machinery in `configure_provider`: the
`model=`, `endpoint=`, `deployment=`, `use_azure_cli=` parameters and
their handling had zero callers anywhere in the app or tests
(survey-verified by grep across amplifier_app_cli/ and tests/;
`use_default_credential`/`use_managed_identity` target fields no provider
even declares). `non_interactive` and all other parameters are untouched.
4. Extracted the two byte-identical 42-line non-interactive-mode blocks
(Phase 1 pre-model fields, Phase 3 post-model fields) into a single
`_apply_non_interactive_field` helper — mechanical, behavior-preserving.
Tests (tests/test_provider_config_unset_fields.py, 22 new):
- Regression: choice field default=None + not required omits the key on
Enter (the in_memory defect) instead of force-writing choices[0].
- Choice field with a real default: unchanged behavior (no unset option).
- Choice field with existing_value: preserved as the default selection,
positioned after the prepended unset option.
- Boolean default=None: key omitted, never writes "none".
- Full `_should_show_field` predicate matrix: literal/contains/
not_contains/startswith/not_startswith, missing key, multiple conditions.
- requires_model Phase-1/Phase-3 split: a requires_model field's show_when
is proven to see `default_model` only after model selection (Phase 2).
- End-to-end exact-key-set assertion for a full configure_provider() run
against a mocked provider schema.
- Dead-parameter removal: model=/endpoint=/deployment=/use_azure_cli= are
no longer accepted parameters.
Full suite: 1491 passed, 1 skipped, 13 deselected, 1 xfailed (baseline 1469
passed + 22 new tests; zero regressions).
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Maintainer admin-merge: self-authored, self-approval impossibleBasis: Root cause of the reported Testing: 22 new tests added covering previously-zero-coverage code paths (choice branch, DTU validation: Control run reproduced the defect exactly (force-wrote CI: All 9 matrix jobs + CLA green (see checks tab). This PR is self-authored. Self-approval is not possible under branch protection, so this merge is being performed via |
Inline /amplifier-config skill (root provider selection vs spawned-session routing vs bundle composition vs settings-scope precedence): inspects effective config with provenance, proposes smallest safe change with blast-radius/rollback, applies only authorized edits. Verified against this week's changes (#284 wizard None-default rendering, provider-openai#69 model-gated ConfigFields, #281 multi-instance credential flow) -- none hardcoded in SKILL.md; it correctly defers version-sensitive facts to live inspection + app-cli:cli-expert. Wiring confirmed via auto-discovery in _ensure_default_skills_dirs() (same mechanism as goalify/goal-batch/ten-lane-highway) and new tests (test_packaged_amplifier_config_discovery_and_invocation, test_amplifier_config_skill_contract). Full suite: 1493 passed (main's 1491 + 2 new tests), zero regressions. Admin-merge at team direction: fork PR (no push access to author's branch to rebase), ruleset requires 1 approving review with none yet recorded, CI 9/9 green, content review complete -- see PR comment for full due-diligence writeup. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Root cause
_prompt_for_field's choice branch (inamplifier_app_cli/provider_config_utils.py) had no representation for "unset": it computedeffective_value = existing_value or defaultand then hard-codeddefault_choice = "1". That meant a providerConfigFielddeclaringdefault=None(deliberately, to signal "leave unset — use provider/model default") withchoices=[...]silently collapsed tochoices[0]and always wrote a value on Enter.Causal chain of the real-world defect
provider-openaideclaresprompt_cache_retentionwithdefault=None+choices=["in_memory", "24h"]— the field's own source comment saysNonemeans "leave unset."choices[0]regardless.settings.yamlgotprompt_cache_retention: in_memorywritten on every fresh configure, even though the user never chose it.in_memoryat runtime, producing a drop-warning every session.The same bug force-writes
text_verbosity=lowforprovider-openai, and would force-write any other provider'sdefault=Nonechoice field the moment it's declared (e.g. an anthropicreasoning_effortfield shaped the same way).A related latent bug lived in the boolean branch:
default and default.lower()withdefault=Noneevaluates toNone(short-circuitand), andConfirm.ask(default=None)still accepts that as a default — so pressing Enter wrote the literal string"none"into config.Fixes (all in app-cli; no provider or core changes)
Choice branch (
provider_config_utils.py:610-649): whendefault is Noneand the field is not required, prepend a"(leave unset - use provider/model default)"option and make it the default selection. Selecting it returnsNone, so the caller omits the key — matching the existing text-field "may be absent" contract. A realexisting_valuestill takes priority as the pre-selected option (offset to account for the prepended entry).required=Truefields keep the originalchoices[0]-default behavior unchanged.Boolean branch (
provider_config_utils.py:583-608): guarddefault is Noneexplicitly instead of relying onand-chaining producing an accidentalNone.Confirm.askis called with an explicitdefault=None; pressing Enter now returnsNone→ key omitted. Never writes"none".Removed dead
cli_overridesmachinery inconfigure_provider(): themodel=,endpoint=,deployment=,use_azure_cli=parameters and their handling (previously ~:716-719, :767-771, :846-851, :884, :891) had zero callers anywhere in the app or tests (grep-verified acrossamplifier_app_cli/andtests/).use_default_credential/use_managed_identitytargeted a field no provider even declares.non_interactiveand every other parameter are untouched.Extracted the two byte-identical 42-line non-interactive-mode blocks (Phase 1 pre-model fields, Phase 3 post-model fields) into one
_apply_non_interactive_field()helper — mechanical, behavior-preserving refactor.Tests (
tests/test_provider_config_unset_fields.py, 22 new)default=None+required=Falseomits the key on Enter (thein_memorydefect) instead of force-writingchoices[0].existing_value: preserved as the default selection, positioned after the prepended unset option.default=None: key omitted, never writes"none"._should_show_fieldpredicate matrix: literal /contains:/not_contains:/startswith:/not_startswith:, missing key, multiple conditions.requires_modelPhase-1/Phase-3 split: arequires_modelfield'sshow_whenis proven to seedefault_modelonly after model selection (Phase 2) — spied via_should_show_field.configure_provider()run against a mocked provider schema (asserts the omitted fields are truly absent, not just falsy).model=/endpoint=/deployment=/use_azure_cli=are no longer accepted parameters.Test results
Full suite: 1491 passed, 1 skipped, 13 deselected, 1 xfailed (baseline 1469 passed + 22 new tests; zero regressions).
🤖 Generated with Amplifier