Skip to content

fix: keep discovered model capabilities out of config - #1761

Merged
Aaronontheweb merged 2 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/1756-dynamic-model-capabilities
Aug 5, 2026
Merged

fix: keep discovered model capabilities out of config#1761
Aaronontheweb merged 2 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/1756-dynamic-model-capabilities

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Summary

  • Stop model discovery from writing context-window and modality overrides.
  • Keep explicit operator overrides and clear operations unchanged.
  • Remove discovered capability fields from the init wizard model section.
  • Document the safe repair path for older configurations.

Producer and consumer

The CLI, model TUI, and init wizard produce named model definitions.

The daemon consumes absent capability fields through runtime provider detection.

Validation

  • dotnet test Netclaw.slnx --no-restore: 6,222 passed and 14 expected skips.
  • dotnet slopwatch analyze: zero issues.
  • pwsh ./scripts/Add-FileHeaders.ps1 -Verify: passed.
  • git diff --check: passed.
  • The native init smoke built binaries, but Ollama installation required an unavailable sudo password.
  • The eval suite could not run because the required NETCLAW_EVAL_* credentials were absent.

Fixes #1756.

@Aaronontheweb Aaronontheweb added bug Something isn't working config Configuration issues, netclaw doctor, schema validation. providers Provider integrations and capability detection across OpenAI-compatible backends. labels Aug 5, 2026

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author rationale review.

These comments explain why I made the key changes. This review does not request changes.

Provider discovery supplies temporary runtime facts. Config capability fields represent durable operator intent. The fix removes the path between those authority levels.

The proof covers the shared writer, CLI, model TUI, init wizard, runtime consumer, and native config artifact.

Capability discovery does not write `ContextWindow`, `InputModalities`, or `OutputModalities`.
The daemon resolves those dynamic values at startup when the operator leaves them absent.

Older releases can contain discovery snapshots in these override fields. The stored shape

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale: I did not add an automatic migration. Older config does not record whether a field came from discovery or an operator. Automatic deletion could remove a real override. The existing clear flags provide a safe, explicit repair.

ValueOverride<ModelModality> inputModalities,
ValueOverride<ModelModality> outputModalities,
DiscoveredModel? discovered)
ValueOverride<ModelModality> outputModalities)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale: The DiscoveredModel parameter no longer exists. The type system now prevents this writer from persisting provider capabilities by accident.

var resolvedOutput = ResolveModality(outputModalities, sameModelEntry, existing?.OutputModalities, discovered?.OutputModalities);
// Explicit operator input wins. Otherwise, the existing operator override remains.
// A new definition stays empty so runtime detection owns dynamic capabilities (#1756).
var resolvedWindow = contextWindow.Supplied ? contextWindow.Value : existing?.ContextWindow;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale: Unset now means "preserve the stored override, or keep the field absent." It no longer means "copy the latest probe." An absent field lets daemon detection run at every startup.

inputOverride,
outputOverride,
discoveredModel);
outputOverride);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale: The CLI still uses the live model list to validate the model ID. It passes only explicit flags into persistence. This separates validation data from durable operator intent.

? ValueOverride<ModelModality>.Set(output)
: ValueOverride<ModelModality>.Unset,
discovered: null);
ValueOverride<int>.Unset,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale: The wizard has no capability override controls. It must pass Unset for every capability field. Otherwise, provider results gain authority that the operator never gave them.

var resolved = ModelCapabilityResolution.ResolveModelCapabilities(
new ModelSelection { Main = main }, detectedAtStartup);

Assert.Equal(100000, resolved.ContextWindowTokens);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale: This test covers the complete producer-to-consumer contract. Selection sees 327680, startup sees 100000, and the runtime selects 100000. The modality change proves the same rule for all dynamic capability fields.

Assert.Equal(512000, main.GetProperty("ContextWindow").GetInt32());
Assert.Equal("Text, Image", main.GetProperty("InputModalities").GetString());
Assert.Equal("Text", main.GetProperty("OutputModalities").GetString());
Assert.False(main.TryGetProperty("ContextWindow", out _));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale: This test uses a successful OAuth probe with complete metadata. The saved definition still omits all capability keys. This proves that known provider data remains dynamic.

assert_field '.Models.Roles.Main' 'ollama-qwen2-0-5b' "$config_json" || :
assert_field '.Models.Definitions[.Models.Roles.Main].Provider' 'ollama' "$config_json" || :
assert_field '.Models.Definitions[.Models.Roles.Main].ModelId' 'qwen2:0.5b' "$config_json" || :
assert_field '(.Models.Definitions[.Models.Roles.Main] | has("ContextWindow"))' 'false' "$config_json" || :

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale: Unit tests cover the writer, but the init wizard had its own data path. These assertions inspect the real config file. They catch any future wizard refactor that restores the bug.

@Aaronontheweb
Aaronontheweb merged commit 2bba18d into netclaw-dev:dev Aug 5, 2026
15 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/1756-dynamic-model-capabilities branch August 5, 2026 14:47
@Aaronontheweb Aaronontheweb mentioned this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working config Configuration issues, netclaw doctor, schema validation. providers Provider integrations and capability detection across OpenAI-compatible backends.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discovered context window becomes a permanent configuration override

1 participant