Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions benchmark/codex_model_catalog_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def _fallback_codex_model_template() -> dict[str, Any]:
"upgrade": None,
"base_instructions": "You are Codex, a coding agent.",
"supports_reasoning_summaries": True,
"supports_reasoning_summary_parameter": True,
"default_reasoning_summary": "none",
"support_verbosity": True,
"default_verbosity": "low",
Expand Down Expand Up @@ -108,6 +109,7 @@ def _build_codex_model_catalog(
"""Build Codex catalog JSON for Switchyard route ids."""
template = _load_codex_model_template(codex_bin)
template.setdefault("supports_reasoning_summaries", True)
template.setdefault("supports_reasoning_summary_parameter", True)
models: list[dict[str, Any]] = []
for priority, (model_id, display_name, description) in enumerate(entries):
model = copy.deepcopy(template)
Expand Down
6 changes: 6 additions & 0 deletions crates/switchyard-runner/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ struct RouteConfig {
context_window: Option<u32>,
tool_calling: Option<bool>,
reasoning: Option<bool>,
reasoning_summaries: Option<bool>,
vision: Option<bool>,
algorithm: AlgorithmSpec,
}
Expand All @@ -87,6 +88,7 @@ impl<'de> Deserialize<'de> for RouteConfig {
let context_window = take_optional(&mut table, "context_window")?;
let tool_calling = take_optional(&mut table, "tool_calling")?;
let reasoning = take_optional(&mut table, "reasoning")?;
let reasoning_summaries = take_optional(&mut table, "reasoning_summaries")?;
let vision = take_optional(&mut table, "vision")?;
let algorithm = AlgorithmSpec::deserialize(toml::Value::Table(table))
.map_err(serde::de::Error::custom)?;
Expand All @@ -95,6 +97,7 @@ impl<'de> Deserialize<'de> for RouteConfig {
context_window,
tool_calling,
reasoning,
reasoning_summaries,
vision,
algorithm,
})
Expand Down Expand Up @@ -127,6 +130,7 @@ impl RouteConfig {
context_window: self.context_window,
tool_calling: self.tool_calling,
reasoning: self.reasoning,
reasoning_summaries: self.reasoning_summaries,
vision: self.vision,
}
}
Expand Down Expand Up @@ -684,6 +688,7 @@ id = "switchyard/random"
context_window = 128000
tool_calling = true
reasoning = false
reasoning_summaries = true
targets = ["fast", "strong"]
weights = [1.0, 2.0]
seed = 7
Expand All @@ -695,6 +700,7 @@ seed = 7
assert_eq!(route.context_window, Some(128_000));
assert_eq!(route.tool_calling, Some(true));
assert_eq!(route.reasoning, Some(false));
assert_eq!(route.reasoning_summaries, Some(true));
assert_eq!(route.algorithm.routing_target_names(), ["fast", "strong"]);
}

Expand Down
3 changes: 3 additions & 0 deletions crates/switchyard-runner/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub struct ModelCapabilities {
/// probe this, so a route opts in via config; undeclared routes advertise as
/// non-reasoning to Codex (fail closed).
pub reasoning: Option<bool>,
/// Whether the routed model accepts reasoning summary controls. When unset,
/// model discovery preserves the legacy behavior and follows `reasoning`.
pub reasoning_summaries: Option<bool>,
/// Whether the routed model accepts image input. Declared per route for the same
/// reason as `reasoning`, and failing closed matters more here: a route may
/// resolve to a target with no vision at all.
Expand Down
9 changes: 8 additions & 1 deletion crates/switchyard-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,7 @@ fn codex_model_entry_json(model: &str, capabilities: ModelCapabilities, priority
// Option separately for clients that want the undeclared state.
let tool_calling = capabilities.tool_calling.unwrap_or(true);
let reasoning = capabilities.reasoning.unwrap_or(false);
let reasoning_summaries = capabilities.reasoning_summaries.unwrap_or(reasoning);
json!({
"slug": model,
"display_name": model,
Expand All @@ -1565,7 +1566,13 @@ fn codex_model_entry_json(model: &str, capabilities: ModelCapabilities, priority
// Required `ModelInfo` string. Unlike the launcher, the server cannot read
// Codex's bundled prompt, so it sends a minimal stub.
"base_instructions": "You are Codex, a coding agent.",
"supports_reasoning_summaries": reasoning,
// Codex renamed this capability. 0.144.x reads `supports_reasoning_summaries`
// and gates the whole reasoning control on it, so a false there also drops
// reasoning effort. 0.145+ reads `supports_reasoning_summary_parameter` and
// only omits `reasoning.summary`. Publish both so either version sees the
// route's declared value.
"supports_reasoning_summaries": reasoning_summaries,
"supports_reasoning_summary_parameter": reasoning_summaries,
"default_reasoning_summary": "none",
"support_verbosity": reasoning,
"default_verbosity": if reasoning { json!("low") } else { Value::Null },
Expand Down
37 changes: 36 additions & 1 deletion crates/switchyard-server/tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2642,6 +2642,13 @@ type = "passthrough"
target = "shared"
reasoning = true

[routes.reasoning_without_summaries]
id = "reasoning-without-summaries"
type = "passthrough"
target = "shared"
reasoning = true
reasoning_summaries = false

[routes.undeclared]
id = "undeclared"
type = "passthrough"
Expand Down Expand Up @@ -2671,7 +2678,7 @@ target = "shared"
.collect::<BTreeMap<_, _>>();
// This checks the shape the server emits. That Codex 0.144.5 actually decodes it
// (context_window: null included) is verified by a live Codex run in SWITCH-1225.
assert_eq!(codex_metadata.len(), 4);
assert_eq!(codex_metadata.len(), 5);
assert_eq!(
codex_metadata["declared"]["context_window"],
json!(1_000_000)
Expand Down Expand Up @@ -2717,11 +2724,35 @@ target = "shared"
codex_metadata["reasoning"]["supports_reasoning_summaries"],
json!(true)
);
assert_eq!(
codex_metadata["reasoning"]["supports_reasoning_summary_parameter"],
json!(true)
);
assert_eq!(
codex_metadata["reasoning"]["support_verbosity"],
json!(true)
);
assert_eq!(codex_metadata["reasoning"]["default_verbosity"], "low");
// Summary support defaults to reasoning for existing configurations, but routes
// that may select a target rejecting `reasoning.summary` can disable it.
assert_eq!(
codex_metadata["reasoning-without-summaries"]["default_reasoning_level"],
"xhigh"
);
assert_eq!(
codex_metadata["reasoning-without-summaries"]["supported_reasoning_levels"]
.as_array()
.map(Vec::len),
Some(4)
);
assert_eq!(
codex_metadata["reasoning-without-summaries"]["supports_reasoning_summaries"],
json!(false)
);
assert_eq!(
codex_metadata["reasoning-without-summaries"]["supports_reasoning_summary_parameter"],
json!(false)
);
// An undeclared route: null context window, non-reasoning, but tools default on so Codex
// remains usable when connected directly to the server.
assert_eq!(codex_metadata["undeclared"]["context_window"], json!(null));
Expand All @@ -2737,6 +2768,10 @@ target = "shared"
codex_metadata["undeclared"]["supports_reasoning_summaries"],
json!(false)
);
assert_eq!(
codex_metadata["undeclared"]["supports_reasoning_summary_parameter"],
json!(false)
);
assert_eq!(codex_metadata["undeclared"]["shell_type"], "shell_command");
assert_eq!(
codex_metadata["undeclared"]["apply_patch_tool_type"],
Expand Down
1 change: 1 addition & 0 deletions docs/reference/toml_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Every route takes the common keys below, plus the keys for its type.
| `context_window` | No | unset | Positive token count advertised for this route by `GET /v1/models`. Unset values appear as `null`. This does not enforce a request limit. |
| `tool_calling` | No | unset | Whether `GET /v1/models` advertises tool-calling support for this route. Unset values appear as `null`. |
| `reasoning` | No | unset | Whether `GET /v1/models` advertises reasoning support to Codex direct-provider discovery. Unset routes are advertised as non-reasoning. |
| `reasoning_summaries` | No | `reasoning` | Whether Codex direct-provider discovery advertises support for `reasoning.summary`. Set this to `false` when any target the route can select supports reasoning effort but not summaries. Codex 0.144.x gates its whole reasoning control on this, so it also stops sending reasoning effort; Codex 0.145+ keeps reasoning effort and only omits `reasoning.summary`. |
| `vision` | No | unset | Whether `GET /v1/models` advertises **image input** to Codex direct-provider discovery. Unset routes are advertised as text-only. This is not cosmetic: Codex reads `input_modalities` from the model card and, when it reads text-only, replaces an attached image with the text `image content omitted because you do not support image input` **before sending**, so a route whose target can see but which does not declare `vision = true` loses the image in the client. Declare it only when every target the route can select accepts images. |

### `noop`
Expand Down