normalized method
The same request with every value the engines normalise away already applied, so two requests that build a bit-identical engine compare equal.
Comparing the RAW arguments caused spurious rebuilds — and a rebuild here unloads and reloads multi-gigabyte weights, so it is expensive and very visible. Three knobs are normalised downstream:
maxNumImagesismaxNumImages ?? 1when vision is on and discarded entirely when it is off, in both engines. So null and 1 are the same request, and withsupportImage: falsethe value is dead.preferredVisionBackend/preferredAudioBackenddefault to CPU (encoderBackendWireName(null) == 'cpu'), which the facade's own doc tells callers — so writing the value explicitly followed the documentation into a reload.
maxTokens is NOT normalised here: the .litertlm engine clamps it up to
1024 but MediaPipe does not, so the effective value depends on which
engine canHandle picks and core cannot know it. A caller who asks for
512 and then for the default 1024 still rebuilds on the litertlm path
even though both engines end up at 1024. Fixing that means moving the
clamp out of the engine and into core; it is left alone rather than
guessed at.
Implementation
ActiveModelParams normalized() => ActiveModelParams(
maxTokens: maxTokens,
preferredBackend: preferredBackend,
preferredVisionBackend: preferredVisionBackend ?? PreferredBackend.cpu,
preferredAudioBackend: preferredAudioBackend ?? PreferredBackend.cpu,
supportImage: supportImage,
supportAudio: supportAudio,
maxNumImages: supportImage ? (maxNumImages ?? 1) : null,
enableSpeculativeDecoding: enableSpeculativeDecoding,
maxConcurrentSessions: maxConcurrentSessions,
loraRanks: loraRanks,
);