{"record":{"id":"375c888f1cb1b547","repo":"BigPizzaV3/CodexPlusPlus","slug":"model-metadata-slug","errorCode":null,"errorMessage":"model_metadata 的模型 {slug} 值必须是对象","messagePattern":"model_metadata 的模型 (.+?) 值必须是对象","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/relay_config.rs","lineNumber":2147,"sourceCode":"        if crate::model_suffix::parse_compact_percent(value).is_none() {\n            anyhow::bail!(\"model_auto_compact 的模型 {slug} 百分比无效：{value}\");\n        }\n    }\n    Ok(())\n}\n\nfn parse_model_metadata_map(metadata_json: &str) -> anyhow::Result<serde_json::Map<String, Value>> {\n    if metadata_json.trim().is_empty() {\n        return Ok(serde_json::Map::new());\n    }\n    let value: Value = serde_json::from_str(metadata_json)\n        .map_err(|error| anyhow::anyhow!(\"model_metadata JSON 解析失败：{error}\"))?;\n    let map = value\n        .as_object()\n        .ok_or_else(|| anyhow::anyhow!(\"model_metadata 必须是 JSON 对象\"))?;\n    for (slug, metadata) in map {\n        if !metadata.is_object() {\n            anyhow::bail!(\"model_metadata 的模型 {slug} 值必须是对象\");\n        }\n    }\n    Ok(map.clone())\n}\n\nfn model_metadata_has_entries(\n    metadata: &serde_json::Map<String, Value>,\n    entry_slugs: &HashSet<String>,\n) -> bool {\n    metadata.keys().any(|slug| entry_slugs.contains(slug))\n}\n\nfn apply_model_metadata_overrides(\n    catalog_json: &str,\n    override_map: &serde_json::Map<String, Value>,\n) -> anyhow::Result<String> {\n    if override_map.is_empty() {\n        return Ok(catalog_json.to_string());","sourceCodeStart":2129,"sourceCodeEnd":2165,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/relay_config.rs#L2129-L2165","documentation":"parse_model_metadata_map parses the model_metadata JSON string and requires both the top level and every per-model value to be a JSON object. If a slug maps to a non-object (string, number, array, bool, null), the apply fails so malformed model metadata is never written into the catalog/config.","triggerScenarios":"Calling a config-apply path with profile.model_metadata set to JSON like {\"model-a\": \"enabled\"} or {\"model-a\": [1,2]} — any slug whose value is not a JSON object.","commonSituations":"Hand-editing model_metadata and using a string instead of an object for a model; an older config format stored per-model scalars; a copy/paste dropped the object braces around the metadata fields.","solutions":["Make every value in the model_metadata JSON a proper object, e.g. {\"model-a\": {\"priority\": 2}}","Validate the JSON with jq or a linter before applying","Remove the malformed slug entry if its metadata is unnecessary","If migrating from an older format, wrap scalar values in an object with the appropriate field name"],"exampleFix":"// before\n{\"model-a\": \"enabled\"}\n// after\n{\"model-a\": {\"supports_search_tool\": true}}","handlingStrategy":"validation","validationCode":"const map = JSON.parse(profile.model_metadata);\nif (typeof map !== 'object' || map === null || Array.isArray(map)) throw new Error('model_metadata must be a JSON object');\nfor (const [slug, v] of Object.entries(map)) {\n  if (typeof v !== 'object' || v === null || Array.isArray(v)) throw new Error(`metadata for ${slug} must be an object`);\n}","typeGuard":"const isMetadataObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);","tryCatchPattern":"try { applyProfile(profile); } catch (e) { if (String(e).includes('model_metadata')) { profile.model_metadata = sanitizeMetadata(profile.model_metadata); applyProfile(profile); } else throw e; }","preventionTips":["Always store per-model metadata as objects, even for a single field","Lint model_metadata JSON before saving in the UI","Migrate legacy scalar formats explicitly"],"tags":["config","json","validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}