"Model metadata for `gpt-oss:20b` not found" with ollama
What version of Codex CLI is running?
0.114.0
What subscription do you have?
None - I am using an Ollama provider
Which model were you using?
gpt-oss:20b
What platform is your computer?
Linux 6.18.17 x86_64 unknown (NixOS)
What terminal emulator and version are you using (if applicable)?
wezterm
What issue are you seeing?
When I try to interact with the Codex CLI, I see the warning message in the attached screenshot and quoted below. This was not a problem before using the 0.114.0 version of codex cli.
⚠ Model metadata for gpt-oss:20b not found. Defaulting to fallback metadata; this
can degrade performance and cause issues.
As the message notes, performance is degraded severely and it appears that when I try to interact with the agent after the message, that features like "plan mode" are not usable and the agent completely loses track of its goal.
Relevant config:
model = "gpt-oss:20b"
model_provider = "ollama"
[model_providers.ollama]
baseURL = "http://localhost:11434/v1"
name = "Ollama"
I have not found a workaround. I have found seemingly related issues (see list below), but none of the workarounds discussed there are working for me.
- https://github.com/openai/codex/issues/12100 (I have tried feeding a relevant
model_catalog_jsonpath to the config) - https://github.com/ollama/ollama/issues/14752 (duplicate issue on the ollama repo)
- https://github.com/openai/codex/issues/14276 (I have tried using a model with an un-hyphenated name)
<img width="1275" height="468" alt="Image" src="https://github.com/user-attachments/assets/5ce350a6-25c3-4d55-83c7-27d0c028a8b8" />
What steps can reproduce the bug?
Open codex cli v0.114.0 configured to use gpt-oss:20b with ollama 0.17.7. Chat with the agent and see the message.
What is the expected behavior?
No warning message, agent stays on task.
Additional information
_No response_
8 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I have mentioned the above potential duplicate in my issue. I do not think that this is a duplicate issue.
May I ask if it has been solved?
@RoterLong - it has not been solved.
In addition to the description that I left of the "missing metadata" I've also realized that the performance degradation I'm noticing is this bug: https://github.com/openai/codex/issues/11940 where the gpt-oss model no longer has access to 'apply_patch'
model-catalog.local.json
You need a "model-catalog.local.json" in your
"~/.codex"directory.Include it inside your config.toml with:
model_catalog_json = "~/.codex/model-catalog.local.json"To be honest, it feels like using local models with codex is going to end at some point. I'm amazed they included it at all, since it doesn't benefit OpenAI financially.
codex/codex-rs/core/models.jsonin the source tree has examples.@ninevoltz thank you for the response. That is working for me now. Originally it didn't work, but that was because the "slug" and "display_name" keys were "gpt-oss-20b" which did not match my specified model of "gpt-oss:20b".
Closed this issue due to the discovered workaround.
Yeah, this approach barely scales for gpt-oss, but it makes perfect financial sense for current state. Bootstrap your harness, and earn good will, sit back and enjoy the secondary benefits of avoiding angry tweets, and placating youtube influencers with an open source + rust. @ninevoltz totally agree, this will be gone, but I'd argue it already is lol. I mean you "technically" can run local models, kind of.
To save everyone time and tokens, schema details: TLDR for /v1/responses drop any incompatible params via empty array or remove keys for optionals
Codex
model_catalog_jsonAuthoring ReferencePinned to commit
3a23e87e203a0cebb57ee1aebf654b460c1e4e1dofgithub.com/openai/codex. Every claim below is grounded in a source file incodex-rs/; paths are relative to that root.---
1. Full schema
ModelInfois defined inprotocol/src/openai_models.rs(lines 257–315). The JSON file you pointmodel_catalog_jsonat must deserialize asModelsResponse { models: Vec<ModelInfo> }(line 437).| JSON field | Rust type | Required? | Default if omitted | Notes |
|---|---|---|---|---|
|
slug|String| required | — | Identifier the model resolves against; also sent on the wire asmodel. ||
display_name|String| required | — | UI string. Not on the wire. ||
description|Option<String>| required key (nullok) | — | Field must be present (no#[serde(default)]); set tonullto omit content. UI only. ||
default_reasoning_level|Option<ReasoningEffort>| optional |None|#[serde(default, skip_serializing_if = "Option::is_none")]. Wire effect: see §4. ||
supported_reasoning_levels|Vec<ReasoningEffortPreset>| required | — | Each item is{ "effort": <ReasoningEffort>, "description": "..." }. UI picker only; not on wire. May be[]. ||
shell_type|ConfigShellToolType| required | — | See §6 for behavior; §7 for accepted strings. ||
visibility|ModelVisibility| required | — | Picker visibility."none"hides everywhere except direct slug requests. ||
supported_in_api|bool| required | — | UI/picker filter (ModelPreset::filter_by_auth). Not on wire. ||
priority|i32| required | — | Lower wins in the picker sort. ||
additional_speed_tiers|Vec<String>| optional |[]| UI only; deprecated in favor ofservice_tiers. ||
service_tiers|Vec<ModelServiceTier>| optional |[]| Each entry:{ "id": "...", "name": "...", "description": "..." }. UI only by default;idvalues become eligibleservice_tierwire strings ifsupports_service_tieraccepts them. ||
availability_nux|Option<ModelAvailabilityNux>| required key (nullok) | — |{ "message": "..." }ornull. UI only. ||
upgrade|Option<ModelInfoUpgrade>| required key (nullok) | — |{ "model": "...", "migration_markdown": "..." }ornull. UI only. ||
base_instructions|String| required | — | The system prompt the wire sends asinstructionswhenmodel_messagesis null. Use the bundled prompt atmodels-manager/prompt.mdas a starting point. ||
model_messages|Option<ModelMessages>| optional |None|#[serde(default, skip_serializing_if = "Option::is_none")]. Enables personality templating; see §4. ||
supports_reasoning_summaries|bool| required | — | Gates whetherreasoningis sent at all. Critical. See §4. ||
default_reasoning_summary|ReasoningSummary| optional |"auto"|#[serde(default)]. Only consulted whensupports_reasoning_summaries=true. ||
support_verbosity|bool| required | — | Gatestext.verbosityon the wire (§4). ||
default_verbosity|Option<Verbosity>| required key (nullok) | — | Only used ifsupport_verbosity=true. ||
apply_patch_tool_type|Option<ApplyPatchToolType>| required key (nullok) | — | Only enum variant is"freeform". See §6. ||
web_search_tool_type|WebSearchToolType| optional |"text"|#[serde(default)]. Shapes wireweb_searchtool, only if web search is enabled. ||
truncation_policy|TruncationPolicyConfig| required | — |{ "mode": "bytes"\|"tokens", "limit": <i64> }. Used by output truncation; can be overridden at runtime bytool_output_token_limit. ||
supports_parallel_tool_calls|bool| required | — | Becomes the wireparallel_tool_calls. ||
supports_image_detail_original|bool| optional |false|#[serde(default)]. Gates whetherview_image/inputs may requestdetail: "original". ||
context_window|Option<i64>| optional |None|#[serde(default, skip_serializing_if = "Option::is_none")]. Used for auto-compact threshold and UI. ||
max_context_window|Option<i64>| optional |None| Same. Clamps usermodel_context_windowoverrides. ||
auto_compact_token_limit|Option<i64>| optional |None| Same. Falls back to0.9 * resolved_context_window(). ||
effective_context_window_percent|i64| optional |95|#[serde(default = "default_effective_context_window_percent")]. ||
experimental_supported_tools|Vec<String>| required | — | String allowlist enabling experimental tools. Only"test_sync_tool"is read (see §6). May be[]. ||
input_modalities|Vec<InputModality>| optional |["text","image"]|#[serde(default = "default_input_modalities")]. Gatesimage_generationtool registration. ||
supports_search_tool|bool| optional |false|#[serde(default)]. Gates thetool_searchtool. |used_fallback_model_metadatais marked#[serde(skip_serializing, skip_deserializing)]and is internal-only.Sub-structures
ReasoningEffortPreset(line 96):{ "effort": <ReasoningEffort>, "description": String }— both fields required.ModelServiceTier(line 119):{ "id": String, "name": String, "description": String }— all required.ModelAvailabilityNux(line 114):{ "message": String }— required.ModelInfoUpgrade(line 421):{ "model": String, "migration_markdown": String }— both required.ModelMessages(line 365):{ "instructions_template": Option<String>, "instructions_variables": Option<ModelInstructionsVariables> }— both keys required, may benull.ModelInstructionsVariables(line 394): all three keys required (personality_default,personality_friendly,personality_pragmatic), eachOption<String>.TruncationPolicyConfig(line 226):{ "mode": "bytes"\|"tokens", "limit": <i64> }— both required.Required vs optional rule of thumb
Any field annotated with
#[serde(default)]or#[serde(default = "...")]is genuinely optional. Everything else must appear as a key, butOption<T>fields acceptnull. A test atopenai_models.rs:782(model_info_defaults_availability_nux_to_none_when_omitted) shows the full set of keys that work when the optional ones are stripped — but note that test still passes every "required key" with an explicitnull.---
2. Loader behavior
Path resolution
model_catalog_jsonis set inconfig.toml(or per-profile) as a path. Definition:config/src/config_toml.rs:350andconfig/src/profile_toml.rs:40. Profile value wins over top-level (core/src/config/mod.rs:3145-3150):Deserialization
load_catalog_json(core/src/config/mod.rs:1532-1553):deny_unknown_fieldsonModelInfo,ModelsResponse, or any nested struct. The bundledmodels-manager/models.jsonincludes forward-compat fields (prefer_websockets,reasoning_summary_format,minimal_client_version,available_in_plans) that the current Rust struct ignores. Feel free to add comments-as-keys like_note.serde_json(e.g. "missing fieldslug"); the whole load fails and Codex refuses to start.foo, expected one of …"). Same failure mode.{"models": []}): explicitly rejected:model_catalog_json path '...' must contain at least one model(line 1547). Tested atcore/src/config/config_tests.rs:7395.failed to parse model_catalog_json path '...' as JSON.What happens after a successful load
The catalog is stuffed into
Config.model_catalog: Option<ModelsResponse>(core/src/config/mod.rs:861). It is then handed to the model provider'smodels_manager(...)call atcore/src/thread_manager.rs:226.The provider's behavior when this value is
Some(model-provider/src/provider.rs:238-260):Implication: setting
model_catalog_jsonswitches Codex toStaticModelsManager. The disk-cachedmodels_cache.jsonand the upstreamGET /modelsendpoint are never consulted. The catalog file becomes the authoritative, complete list. (manager.rs:382-407is the entireStaticModelsManagerimpl —refresh_if_new_etagis a no-op.)Partial-override fields in
config.tomlThese supplement a catalog entry at runtime via
with_config_overrides(models-manager/src/model_info.rs:23-63):|
config.tomlkey | Reads field | Effect ||---|---|---|
|
model_context_window(i64) |model.context_window| Clamped tomodel.max_context_windowif present. ||
model_auto_compact_token_limit(i64) |model.auto_compact_token_limit| Direct override; runtimeauto_compact_token_limit()further clamps to 90% of resolved context. ||
tool_output_token_limit(usize) |model.truncation_policy| Recomputes the policy in the same mode as the catalog entry (Bytes→approx_bytes_for_tokens(...),Tokens→ as-is). ||
model_supports_reasoning_summaries(bool) |model.supports_reasoning_summaries| Force-true only (if let Some(x) && x { model.supports_reasoning_summaries = true; }). Cannot force false. ||
experimental_compact_prompt_file/experimental_compact_prompt|model.base_instructions| Replaces base instructions; also clearsmodel_messages(disables personality templating). |If
base_instructionsoverride is absent andpersonality_enabled == false(inModelsManagerConfig),model.model_messagesis set toNoneeven if you provided it.---
3. Slug resolution
Entry point:
construct_model_info_from_candidates(models-manager/src/manager.rs:459-478):Match order
find_model_by_longest_prefix, lines 422-438). The requested string muststarts_with(candidate.slug). Among matches, the longest catalog slug wins.Examples (assuming catalog has
["gpt-5", "gpt-5-codex"]):"gpt-5-codex-20250812"→ matches"gpt-5-codex"."gpt-5"→ matches"gpt-5"."gpt"→ no match (prefix relationship is requeststarts_withslug, not vice versa).find_model_by_namespaced_suffix, lines 440-457). Tries once if step 1 missed:/only; rejects strings with more than one/.[A-Za-z0-9_\-]+."openrouter/anthropic-claude-3-sonnet"with catalog slug"anthropic-claude-3"→ matches.model_info::model_info_from_slug,model_info.rs:66-102). LogsUnknown model <slug> is used. This will use fallback model metadata.Returns:``
rust
``ModelInfo {
slug: <requested>,
display_name: <requested>,
description: None,
default_reasoning_level: None,
supported_reasoning_levels: vec![],
shell_type: ConfigShellToolType::Default,
visibility: ModelVisibility::None,
supported_in_api: true,
priority: 99,
additional_speed_tiers: vec![],
service_tiers: vec![],
availability_nux: None,
upgrade: None,
base_instructions: BASE_INSTRUCTIONS, // bundled prompt.md
model_messages: <special-case for "gpt-5.2-codex" and "exp-codex-personality" only>,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
default_verbosity: None,
apply_patch_tool_type: None,
web_search_tool_type: WebSearchToolType::Text,
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: Some(272_000),
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: vec![],
input_modalities: vec![Text, Image],
used_fallback_model_metadata: true,
supports_search_tool: false,
}
The slug-rewrite quirk
When step 1 or 2 matches, the resolved entry's
slugis replaced with the user's full request string, not the catalog entry's slug (line 470). The wire request always sendsmodel = <user's exact request>. The catalog slug is only the lookup key.This means if you want the proxy to receive
"meta-llama/Llama-3.3-70B-Instruct"but want to share metadata with a"llama-3.3"catalog entry, you can put"slug": "llama-3.3"in your catalog and the longest-prefix mechanic will pick it up — but you should generally just write out the exact wire string as the catalog slug to avoid surprises.---
4. Field → wire effect mapping
The HTTP request body is built in
core/src/client.rs:709-766(build_responses_request). The body type isResponsesApiRequest(codex-api/src/common.rs:169-190):Catalog → wire mapping
| Catalog field | Wire location | Translation |
|---|---|---|
|
slug|model| Verbatim (user's request string, after slug rewrite per §3). ||
base_instructions|instructions| After personality template substitution ifmodel_messages.instructions_templateis set; otherwise raw. Empty string isskip_serializing_if-omitted. ||
model_messages.instructions_template|instructions| Replacesbase_instructionswhen present; supports{{ personality }}placeholder. ||
model_messages.instructions_variables|instructions| Source of{{ personality }}value, keyed byPersonalityenum at runtime. ||
supports_parallel_tool_calls|parallel_tool_calls| Directly.turn.rs:993readsturn_context.model_info.supports_parallel_tool_calls. ||
supports_reasoning_summaries|reasoning| Gates whole field.false→reasoning: null(i.e. no reasoning sent at all).true→{effort, summary}. (client.rs:690-707) ||
default_reasoning_level|reasoning.effort| Used when the request doesn't override;Noneis allowed (Responses API acceptseffort: null). ||
default_reasoning_summary|reasoning.summary| Used when not overridden.ReasoningSummary::None→ wiresummary: null; other values serialize to their lowercase string. ||
support_verbosity|text.verbosity| Gates field.false→ never sent (warns at runtime ifmodel_verbositywas configured). ||
default_verbosity|text.verbosity| Used whensupport_verbosity=trueand no runtime override. ||
service_tiers[].id|service_tier| Only sent if a runtime service tier is requested andmodel_info.supports_service_tier(tier)returns true (client.rs:744-745). ||
web_search_tool_type|tools[].search_content_types(only onweb_searchtool entry) |"text"→ field omitted;"text_and_image"→["text", "image"]. Only applies if web_search is enabled by feature flags. (hosted_spec.rs:27-35) ||
apply_patch_tool_type|tools[](only as a"custom"type entry) |Some(Freeform)adds theapply_patchfreeform tool with lark grammar;Noneomits it. See §6. ||
shell_type|tools[](shellorexec_command/write_stdinfunctions) | Determines which exec tool is registered. See §6 — not a direct wire field, but adds tool entries. ||
supports_search_tool|tools[](tool_search) | Gates registration of the deferred-tool search tool. ||
experimental_supported_tools|tools[]|"test_sync_tool"registersTestSyncHandler. No other strings are read. ||
input_modalities|tools[](image_generation) + image input acceptance |contains(Image)enables image_generation tool registration. ||
supports_image_detail_original|input.[].content[].detail| Sanitizesdetail: "original"toautoon outgoing image references if false. ||
truncation_policy| not on wire | Used locally to truncate tool outputs before they go intoinput. ||
context_window/max_context_window/auto_compact_token_limit/effective_context_window_percent| not on wire | Local; control auto-compact and prompt budgeting. |UI-only / picker-only fields
These never reach the wire and never affect tool registration:
display_name,descriptionsupported_reasoning_levels(UI picker; the wire usesdefault_reasoning_levelor runtime override directly)visibility,supported_in_api,priorityadditional_speed_tiers,service_tiers[].name,service_tiers[].descriptionavailability_nux,upgrade---
5. Hardcoded request fields
Things Codex sends that you cannot toggle from the catalog (or
config.toml) without forking. Fromclient.rs:746-764:| Field | Value | Source |
|---|---|---|
|
tool_choice|"auto"| Hardcoded string literal (line 751). ||
stream|true| Hardcodedboolliteral (line 755). ||
store|provider.is_azure_responses_endpoint()| Effectivelyfalsefor non-Azure providers; not catalog-controllable (line 754). ||
include|["reasoning.encrypted_content"]ifreasoningis set, else[]| Conditional onsupports_reasoning_summaries, but the inclusion list itself is hardcoded (lines 722-726). ||
prompt_cache_key| thread/session id |Some(self.state.thread_id.to_string())(line 743). ||
client_metadata|{"x-codex-installation-id": <uuid>}| Always sent; key is the constantX_CODEX_INSTALLATION_ID_HEADER(line 760-763). ||
parallel_tool_calls| direct frommodel.supports_parallel_tool_calls| Catalog-controlled, but always sent (no skip_serializing). || HTTP headers | session/auth headers from
build_session_headers| Not catalog-controllable; seecodex-api. |Reasoning field serialization quirk
On
ResponsesApiRequestitself (codex-api/src/common.rs:178),reasoning: Option<Reasoning>has noskip_serializing_ifannotation. Whensupports_reasoning_summaries=false, the value isNoneand the wire body still contains"reasoning": null. Many non-OpenAI proxies tolerate this, but some (notably stricter Bedrock-style passthroughs) will reject an explicit null where the field isn't supported.Inside the
Reasoningstruct (line 113-119), botheffortandsummaryuseskip_serializing_if = "Option::is_none", so aSome(Reasoning { effort: None, summary: None })would serialize as"reasoning": {}— but this shape can't actually be produced bybuild_reasoning(it returnsNonewheneversupports_reasoning_summaries=false).---
6. Tool registration gates
Tools registered for any turn are computed in
tools/src/tool_config.rs(ToolsConfig::new, lines 167-273) andcore/src/tools/spec_plan.rs::collect_tool_executors. Catalog-controlled gates:shell_typetool_config.rs:204-225collapses the catalog value into a runtime shell type, thenspec_plan.rs:264-291registers handlers:| Catalog
shell_type| After feature-flag adjustment | Handler registered ||---|---|---|
|
"default"| becomesShellCommand|ShellCommandHandler||
"local"| becomesShellCommand|ShellCommandHandler||
"shell_command"|ShellCommand|ShellCommandHandler||
"unified_exec"(ifUnifiedExecfeature on andconpty_supported()) |UnifiedExec|ExecCommandHandler+WriteStdinHandler+ secondaryShellCommandHandler(lines 296-307) ||
"unified_exec"(otherwise) |ShellCommand|ShellCommandHandleronly ||
"disabled"|Disabled| nothing |If feature
ShellToolis off entirely, the type is forced toDisabledregardless of catalog. Soshell_type: "disabled"is the only way to disable from the catalog; turning it on requires both catalog config and the feature flag.apply_patch_tool_typeOnly one enum variant exists:
"freeform". The handler registers a freeform (custom) tool with lark grammar (apply_patch_spec.rs:9-27). Wire shape:If
apply_patch_tool_type: null, no apply_patch tool is registered (spec_plan.rs:339-343short-circuits).web_search_tool_typeDoesn't gate registration — that's controlled by feature flags / runtime
web_search_mode. It only shapes the wireweb_searchtool entry. Wire shape (tool_spec.rs:37-48):supports_search_toolGates
tool_searchregistration alongside theFeature::ToolSearchflag (tool_config.rs:184-185):Wire shape:
experimental_supported_toolsOnly one string is read (
spec_plan.rs:345-351):Any other string is silently ignored. This is exhaustive — there are no other readers of this field outside tests as of the pinned commit.
input_modalitiescontains(Image)enables image_generation tool registration ifimage_generation_tool_auth_allowed && Feature::ImageGeneration. Function:supports_image_generation(tool_config.rs:411-413). Wire shape:supports_image_detail_originalDoesn't add or remove tools; gates whether
view_imageand image inputs acceptdetail: "original". False (default) → values are normalized toauto(image_detail.rs:23-38).Tools always registered regardless of catalog
From
spec_plan.rs:PlanHandler(line 315) —plantoolRequestUserInputHandler(line 322) — gated byrequest_user_input_available_modes, not catalogViewImageHandler(line 356) — gated byenvironment_mode.has_environment(), not catalogListMcpResourcesHandler, etc., lines 309-313) — gated byparams.mcp_tools.is_some()Goal tools, multi-agent tools, code-mode tools, agent-jobs tools, permissions tools, image generation, and tool_search are all gated by feature flags that the catalog does not control.
Tool wire-shape summary (
tools/src/tool_spec.rs:15-51)|
ToolSpecvariant | Wiretype| Distinguishing fields ||---|---|---|
|
Function|"function"|name,description,parameters,strict||
Namespace|"namespace"|name,description,tools(nested) ||
ToolSearch|"tool_search"|execution,description,parameters||
ImageGeneration|"image_generation"|output_format||
WebSearch|"web_search"|external_web_access,filters,user_location,search_context_size,search_content_types(all skip-if-none) ||
Freeform|"custom"|name,description,format: {type, syntax, definition}|---
7. Enum reference
All enums apply
#[serde(rename_all = ...)]. Accepted strings on the wire and in catalog JSON:ReasoningEffort—lowercase(openai_models.rs:44)Default:
"medium".ReasoningSummary—lowercase(config_types.rs:32)Default:
"auto"."none"causessummaryto be omitted on the wire.Verbosity—lowercase(config_types.rs:59)Default:
"medium".Personality—lowercase(config_types.rs:278)Runtime only (not a catalog field; used to select
instructions_variables.personality_*).ServiceTier—lowercasefor serde (config_types.rs:415)JSON values:
"fast" | "flex".On the wire,
request_value()rewrites:Fast → "priority",Flex → "flex". (line 421-426). Wire spelling is different from catalog spelling. The catalogservice_tiers[].idfield is a free-form string, butsupports_service_tierchecks againstServiceTier::from_request_value, which accepts"fast" | "priority" | "flex".ModelVisibility—lowercase(openai_models.rs:171)ConfigShellToolType—snake_case(openai_models.rs:194)ApplyPatchToolType—snake_case(openai_models.rs:204)Only one variant.
WebSearchToolType—snake_case(openai_models.rs:212)Default:
"text".TruncationMode—snake_case(openai_models.rs:221)InputModality—lowercase(openai_models.rs:80)Default vec when omitted:
["text", "image"].WebSearchUserLocationType—lowercase(config_types.rs:362)Only one variant; only relevant via runtime
web_search_config, not catalog.WebSearchMode—lowercase(config_types.rs:289)Runtime only (not in the catalog).
WebSearchContextSize—lowercase(config_types.rs:299)Runtime only.
---
8. Constraints that can't be changed without forking
Reach for a proxy-side transform or a fork when you need to alter any of these:
tool_choice: "auto"— hardcoded; no way to force"none"or a specific tool from config.stream: true— hardcoded; Codex always uses streaming responses.store: provider.is_azure_responses_endpoint()— only Azure providers set this true.include: ["reasoning.encrypted_content"]— list contents are hardcoded; you cannot add/remove items via catalog.prompt_cache_key— always set to the thread id; cannot be disabled.client_metadata.x-codex-installation-id— always sent.Option<Reasoning>skips whensupports_reasoning_summaries=false, but theReasoningstruct itself omitseffort/summaryindependently viaskip_serializing_if. You can't change this serialization.Freeform(lark) exists. There is no function-call variant of apply_patch available in this commit.prompt.mdcontent — the bundled base instructions are included viainclude_str!inmodel_info.rs:16. They are used as the literalbase_instructionsfor the fallback metadata, but for your catalog entries you supply the full string yourself.model_catalog_jsonis set, you getStaticModelsManagerwith no network refresh and no ETag handling. There is no opt-in to "use my catalog and refresh from upstream."experimental_supported_tools— the only string that does anything is"test_sync_tool"; the field is effectively useless for anything else without a fork.Personalityenum values — fixed set of three. Templates substitute one of these or empty string.---
9. Minimum-viable catalog entry
Every "required key" must appear, even if its value is
null. This is the smallest object that successfully deserializes againstModelInfo, derived from the working test fixture atopenai_models.rs:782-810and what is and isn't#[serde(default)]:Why each field is here:
| Field | Reason |
|---|---|
|
slug| Required; lookup key + wiremodel. ||
display_name| Required (no#[serde(default)]). ||
description| Key required;nullallowed. ||
supported_reasoning_levels| Required; empty vec acceptable. ||
shell_type| Required enum."shell_command"registers the plain shell handler. ||
visibility| Required enum."list"shows it in the picker. ||
supported_in_api| Required bool. ||
priority| Required i32. ||
availability_nux| Key required;nullallowed. ||
upgrade| Key required;nullallowed. ||
base_instructions| Required string. Sent as the wireinstructionsfield. ||
supports_reasoning_summaries| Required bool; controls whetherreasoningis on the wire. ||
support_verbosity| Required bool. ||
default_verbosity| Key required;nullallowed. ||
apply_patch_tool_type| Key required;nullallowed (no apply_patch tool). ||
truncation_policy| Required struct. ||
supports_parallel_tool_calls| Required bool. Becomes wireparallel_tool_calls. ||
experimental_supported_tools| Required vec; empty is fine. |The following are omitted because they have
#[serde(default)]annotations and pick up sensible defaults:default_reasoning_level(None),additional_speed_tiers([]),service_tiers([]),model_messages(None),default_reasoning_summary("auto"),web_search_tool_type("text"),supports_image_detail_original(false),context_window(None),max_context_window(None),auto_compact_token_limit(None),effective_context_window_percent(95),input_modalities(["text","image"]),supports_search_tool(false).If you remove any field from the minimum list above (other than the
default-annotated ones),serde_jsonwill reject the file with "missing field …" and Codex will refuse to start.