Studio: tighten YAML import/save for vision_image_size

Two YAML-path asymmetries that could leak a stale image size into
training:

- parseYamlConfig now treats a missing training.vision_image_size as
  null. Without this, importing a YAML saved before this feature (or
  any config that omits the key) preserved whatever value the user had
  previously set on a different model. The model-defaults reload path
  still uses Object.hasOwn so same-model defaults reloads do not wipe
  a manual selection; only file import normalises the missing key.

- handleSaveConfig now passes a DeepSeek-OCR-specific guard to
  serializeConfigToYaml so saved YAML matches what the API mapper
  actually sends. Previously a state with visionImageSize set could
  emit the key even though Studio ignored it at training time for
  DeepSeek OCR, and a later import for a non-DeepSeek vision model
  would activate the stale value.

serializeConfigToYaml gains an optional third parameter
includeVisionImageSize defaulting to includeVisionFields, preserving
the existing 2-arg call signature for backwards compatibility.
This commit is contained in:
Daniel Han 2026-05-25 08:08:51 +00:00
commit 89ae87c11f
2 changed files with 35 additions and 3 deletions

View file

@ -87,7 +87,18 @@ export function TrainingSection() {
// vision_image_size choice in those windows.
const includeVisionFields =
store.isVisionModel && store.isDatasetImage !== false;
const yamlStr = serializeConfigToYaml(store, includeVisionFields);
// DeepSeek OCR ignores vision_image_size at training time (mappers.ts
// sends null), so do not emit it to YAML either; otherwise a stale
// value could later apply to a non-DeepSeek vision model.
const selectedModelLower = (store.selectedModel ?? "").toLowerCase();
const isDeepseekOcr =
selectedModelLower.includes("deepseek") &&
selectedModelLower.includes("ocr");
const yamlStr = serializeConfigToYaml(
store,
includeVisionFields,
includeVisionFields && !isDeepseekOcr,
);
const blob = new Blob([yamlStr], { type: "text/yaml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");

View file

@ -27,8 +27,28 @@ export function parseYamlConfig(text: string): BackendModelConfig {
console.warn("Ignored unknown YAML keys:", unknownKeys.join(", "));
}
// YAML import means "use this config as authoritative". An absent
// vision_image_size should reset the in-memory value to Default, not
// preserve a stale one. Same-model defaults reloads (which also flow
// through the model-config mapper) skip the reset via Object.hasOwn
// in model-defaults.ts; here we forge the key so import always wins.
const trainingObj =
raw.training != null &&
typeof raw.training === "object" &&
!Array.isArray(raw.training)
? { ...(raw.training as Record<string, unknown>) }
: raw.training;
if (
trainingObj != null &&
typeof trainingObj === "object" &&
!Array.isArray(trainingObj) &&
!Object.hasOwn(trainingObj, "vision_image_size")
) {
(trainingObj as Record<string, unknown>).vision_image_size = null;
}
return {
training: (raw.training ?? undefined) as BackendModelConfig["training"],
training: trainingObj as BackendModelConfig["training"],
lora: (raw.lora ?? undefined) as BackendModelConfig["lora"],
logging: (raw.logging ?? undefined) as BackendModelConfig["logging"],
};
@ -41,6 +61,7 @@ export function parseYamlConfig(text: string): BackendModelConfig {
export function serializeConfigToYaml(
state: TrainingConfigState,
includeVisionFields: boolean,
includeVisionImageSize: boolean = includeVisionFields,
): string {
const lora: Record<string, unknown> = {
lora_r: state.loraRank,
@ -77,7 +98,7 @@ export function serializeConfigToYaml(
lr_scheduler_type: state.lrSchedulerType,
};
if (includeVisionFields) {
if (includeVisionImageSize) {
training.vision_image_size = state.visionImageSize;
}