Skip to content

refactor(config)!: Flatten the model parameter collector - #1132

Open
JeanMertz wants to merge 3 commits into
config-map-strategiesfrom
config-parameter-flatten
Open

JeanMertz wants to merge 3 commits into
config-map-strategiesfrom
config-parameter-flatten

Conversation

@JeanMertz

Copy link
Copy Markdown
Collaborator

A model parameter JP does not model is collected into other, which
was a real key on the wire: presence_penalty = 0.5 was stored as
other = { presence_penalty = 0.5 }. It is flattened, so the block
holds the parameter directly and other is gone from the generated
config.toml and from the schema as a key to write.

A config file or stored conversation written with the nested table is
hoisted on read, so nothing needs migrating by hand.

Deleting the hand-rolled collector removes a hand-maintained list of
known keys, so a newly added typed field can no longer be silently
forwarded to the provider as a raw parameter. It also inverts a
data-loss risk: stripping skips a struct holding a flattened field, so
provider parameters survive a load that previously kept them only
because other happened to be named in the schema.

BREAKING CHANGE: other is no longer a reserved parameter name

--cfg ...parameters.other names a parameter called other, not the
collector. Write an unrecognized parameter directly:
--cfg ...parameters.presence_penalty=0.5.

`--cfg assistant.model.parameters.other.presence_penalty=0.5` reaches
the parameter, where before it created a table nested inside itself:
`other = { other = { presence_penalty = 0.5 } }`. Clearing the table
with `--cfg assistant.model.parameters.other=null` empties it, where
before it removed an entry that happened to be named `other` and left
the rest in place.

Unrecognised keys in the parameter block are provider parameters JP
does not model, and the last arm of the block's key-value dispatch
collected them. `other` had no arm of its own, so it fell to that arm
too and addressed an entry rather than the table.

A config file already reserves the name: `KNOWN_KEYS` lists `other`, so
`[assistant.model.parameters] other = 5` is read as the explicit table
and not as a parameter called `other`. `--cfg` now agrees with it.

BREAKING CHANGE: `--cfg` cannot set a provider parameter named `other`

Write it inside the table instead, as a config file already must:
`--cfg assistant.model.parameters.other.other=5`.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
`assistant.model.parameters.other` is where the parameters JP does not
model are collected, not a key to write. A provider parameter goes in
the block itself:

```toml
[assistant.model.parameters]
presence_penalty = 0.5
```

The doc comment advertised `[assistant.model.parameters.other]` as an
equally good spelling, which put a field that exists to be invisible in
front of the user, in both the generated `config.toml` and the exported
JSON schema.

Hiding it outright is not available. `#[setting(exclude)]` takes a field
out of the schema, and the compat layer strips from every stored config
whatever the schema does not name, so excluding this one would discard
the provider parameters each existing conversation was created with. A
test now writes a parameter, reads it back, and fails if it goes
missing, so the next attempt to hide the field is caught here rather
than in someone's conversation history.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
A provider parameter JP does not model is written in the parameter
block, and reaches the wire the same way:

```toml
[assistant.model.parameters]
presence_penalty = 0.5
```

Before, it was collected into an `other` field that serialized under
its own name, so every stored conversation config carried
`other = { presence_penalty = 0.5 }` and the generated `config.toml`
and JSON schema both offered `other` as a key to write. Flattening the
field makes it what it always was: the bucket, not a key. The generated
TOML skips a flattened field, so it no longer appears there at all.

Existing configs keep working. A nested `other` table, in a config file
or a stored conversation, is hoisted into the block on read, with a
nested entry still winning a collision against a sibling of the same
name exactly as it did when the nested form was documented.

The mechanism the collecting rests on is now serde's rather than ours.
`deserialize_collecting_other` split the block against a hand-kept
`KNOWN_KEYS` list, which a new typed field had to be added to or be
silently forwarded to the provider as a raw parameter; both are gone,
along with the test that guarded the list. `allow_unknown_fields` on
the block is what lets serde flatten a map there, matching
`conversation.tools`.

It is also what keeps the parameters. The compat layer strips from
every stored config whatever the schema does not name, and skips a
struct holding a flattened field for that reason, so a provider
parameter survives a load that would otherwise discard it. A test in
`jp_conversation` reads one back and fails if it goes missing.

BREAKING CHANGE: `assistant.model.parameters.other` is no longer a key

A provider parameter is written directly in the parameter block. The
nested form is still read, so no existing config or conversation needs
changing, but `--cfg assistant.model.parameters.other.presence_penalty`
now sets a parameter named `other` holding an object rather than
reaching `presence_penalty`. Write
`--cfg assistant.model.parameters.presence_penalty=0.5` instead.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
@JeanMertz
JeanMertz force-pushed the config-parameter-flatten branch from 340e8c5 to 8d212f4 Compare September 8, 2026 08:09
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.

1 participant