BigPizzaV3/CodexPlusPlus · error

model_providers. 必须是 TOML table

Error message

model_providers.{provider_id} 必须是 TOML table

What it means

The provider-table helper inserts model_providers.<provider_id> if absent, then requires the entry to be a mutable TOML table. If the entry exists but holds a non-table value (string, integer, array), as_table_mut returns None and it throws "model_providers.{provider_id} 必须是 TOML table". Unlike error 461 (read-only patch), this one tries to create the provider first but cannot overwrite an existing wrong-typed value.

Solutions

  1. Replace the scalar entry for the named provider with a proper table: [model_providers.<provider_id>] with key/value fields.
  2. If using [[model_providers]] array-of-tables syntax, rewrite as [model_providers.<id>] named tables.
  3. Delete the bad entry and re-apply the profile so the helper inserts a fresh empty table.
  4. Validate config.toml structure (each model_providers child must be a table) before running apply.

Example fix

// before (config.toml)
model_providers.openai = "https://api.openai.com/v1"
// after (config.toml)
[model_providers.openai]
base_url = "https://api.openai.com/v1"
Defensive patterns

Strategy: validation

Validate before calling

fn provider_entry_is_table(doc: &toml_edit::DocumentMut, id: &str) -> bool {
    doc.get("model_providers")
        .and_then(|p| p.get(id))
        .map(|i| i.is_table())
        .unwrap_or(true) // absent is fine: helper will insert it
}

Type guard

fn as_provider_table(item: Option<&toml_edit::Item>) -> Option<&toml_edit::Table> {
    item.and_then(|i| i.as_table())
}

Try / catch

match apply_relay_profile_to_home(...) {
    Err(e) if e.to_string().contains("必须是 TOML table") => {
        // remove the scalar model_providers.<id> entry, re-apply to recreate it as a table
    }
    other => other?,
}

Prevention

When it happens

Trigger: Any apply/write flow that ensures a provider exists when config.toml contains e.g. `model_providers.openai = "https://..."` or `[model_providers]` declared as an array of tables `[[model_providers]]` with that id.

Common situations: Users converting a provider into a bare base_url string to 'simplify' the config, following old docs that used array-of-tables syntax, or corrupted sections after manual merges.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/95d5b034e60e32eb. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/relay_config.rs:3328

}

fn ensure_provider_table<'a>(
    doc: &'a mut DocumentMut,
    provider_id: &str,
) -> anyhow::Result<&'a mut Table> {
    let providers = table_mut_or_insert(doc, "model_providers")?;
    if !providers.contains_key(provider_id)
        || providers
            .get(provider_id)
            .and_then(Item::as_table)
            .is_none()
    {
        providers.insert(provider_id, toml_edit::table());
    }
    providers
        .get_mut(provider_id)
        .and_then(Item::as_table_mut)
        .ok_or_else(|| anyhow::anyhow!("model_providers.{provider_id} 必须是 TOML table"))
}

fn remove_provider_table(doc: &mut DocumentMut, provider_id: &str) {
    if let Some(providers) = doc.get_mut("model_providers").and_then(Item::as_table_mut) {
        providers.remove(provider_id);
        if providers.is_empty() {
            doc.as_table_mut().remove("model_providers");
        }
    }
}

fn retain_only_provider_table(doc: &mut DocumentMut, provider_id: &str) {
    if let Some(providers) = doc.get_mut("model_providers").and_then(Item::as_table_mut) {
        let provider = providers
            .remove(provider_id)
            .unwrap_or_else(toml_edit::table);
        providers.clear();
        providers.insert(provider_id, provider);

View on GitHub (pinned to b1ed92e5e4)