{"record":{"id":"633d78c4139f5aeb","repo":"BigPizzaV3/CodexPlusPlus","slug":"model-metadata-json","errorCode":null,"errorMessage":"model_metadata 必须是 JSON 对象","messagePattern":"model_metadata 必须是 JSON 对象","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/relay_config.rs","lineNumber":2144,"sourceCode":"\nfn validate_model_auto_compact(model_auto_compact: &HashMap<String, String>) -> anyhow::Result<()> {\n    for (slug, value) in model_auto_compact {\n        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>,","sourceCodeStart":2126,"sourceCodeEnd":2162,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/relay_config.rs#L2126-L2162","documentation":"After parsing model_metadata, the function requires the top-level JSON value to be an object (as_object). Arrays, strings, numbers or null produce \"model_metadata 必须是 JSON 对象\". Each object value must itself be an object (checked on the next lines) since it holds per-model metadata.","triggerScenarios":"model_metadata set to a JSON array like [{...}], a bare string, or a number — valid JSON but not an object at top level.","commonSituations":"Users pasting an array of metadata records, or a single model's metadata object without the outer slug→object wrapper.","solutions":["Wrap entries in a top-level object keyed by model slug: {\"slug\": {...}}.","If you have an array, convert it to an object keyed by each record's slug field.","Validate the parsed value is an object before assigning to the profile.","Clear the field if metadata overrides are not needed."],"exampleFix":"// before\nmodel_metadata = \"[{\\\"slug\\\": \\\"deepseek-v4-pro\\\", \\\"context_window\\\": 1000000}]\"\n// after\nmodel_metadata = \"{\\\"deepseek-v4-pro\\\": {\\\"context_window\\\": 1000000}}\"","handlingStrategy":"type-guard","validationCode":"fn metadata_is_object(json: &str) -> bool {\n    serde_json::from_str::<serde_json::Value>(json)\n        .map(|v| v.is_object())\n        .unwrap_or(false)\n}","typeGuard":"fn is_object(v: &serde_json::Value) -> bool { v.is_object() }","tryCatchPattern":"match apply_model_catalog_to_config(...) {\n    Err(e) if e.to_string().contains(\"model_metadata 必须是 JSON 对象\") => {\n        // wrap array entries into a slug-keyed object and retry\n    }\n    other => other?,\n}","preventionTips":["Always key metadata by model slug at the top level","Reject arrays/scalars in form validation before saving","Convert array-of-records inputs into slug-keyed objects in the UI layer"],"tags":["json","schema","metadata"],"backgroundTag":"unexpected-response-shape","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"}