1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
//! Runtime wiring for catalog-declared reasoning execution modes.
//!
//! The catalog (`[llm.models.<id>].reasoning_modes`) is the single source of
//! truth for provider knobs that change how MUCH work a model does before it
//! answers — today OpenAI's `reasoning.mode = "pro"` on the Responses API.
//!
//! This is deliberately a sibling of [`crate::llm::serving_tiers`] rather than
//! a new tier id inside it. A serving tier changes how fast the same work is
//! served and is priced by a different per-token RATE; a reasoning mode keeps
//! the rate and changes the token COUNT, because the provider bills the extra
//! work at the model's standard rates. Folding one into the other would make
//! `cost_multiplier` mean two different things and would let `speed = "fast"`
//! silently select an execution mode.
//!
//! `reasoning.mode` is also nested, sharing its object with `reasoning.effort`,
//! so the knob is addressed by a path and merged into any object already
//! present instead of overwriting it.
//!
//! Not here yet: reading the provider's echoed mode back onto the usage
//! record. Unlike a serving tier, a reasoning mode changes no per-token rate,
//! so no billing decision depends on the echo and a reader would be dead
//! weight today. The catalog already carries `response_values` for it, so the
//! data contract is in place when a surface needs to show "you asked for pro
//! and pro is what ran".
use crate::llm_config::{model_catalog_entry, ReasoningModeDef};
/// The catalog id of the default mode. It is never declared in the catalog;
/// requesting it is the same as requesting nothing.
pub(crate) const STANDARD_MODE_ID: &str = "standard";
/// Catalog lifecycle status that disqualifies a reasoning mode from use.
const DEPRECATED_STATUS: &str = "deprecated";
/// Resolve a named reasoning mode from the catalog, if any.
pub(crate) fn lookup(model: &str, mode_id: &str) -> Option<ReasoningModeDef> {
model_catalog_entry(model).and_then(|entry| {
entry
.reasoning_modes
.into_iter()
.find(|mode| mode.id == mode_id)
})
}
/// Whether a reasoning mode is currently usable.
pub(crate) fn is_usable(mode: &ReasoningModeDef) -> bool {
mode.status.as_deref() != Some(DEPRECATED_STATUS)
}
/// Outcome of validating a reasoning-mode request against the catalog.
pub(crate) enum ReasoningModeGate {
/// The model offers this usable mode; engage it. The catalog metadata is
/// re-read by the provider body builder, so the variant carries no payload.
Usable,
/// The model does not declare this mode at all.
Unsupported,
/// The mode is declared but deprecated; carries the catalog note.
Deprecated { note: Option<String> },
}
/// Classify a non-default `reasoning_mode` request for the resolved model.
pub(crate) fn gate(model: &str, mode_id: &str) -> ReasoningModeGate {
match lookup(model, mode_id) {
None => ReasoningModeGate::Unsupported,
Some(mode) if !is_usable(&mode) => ReasoningModeGate::Deprecated { note: mode.note },
Some(mode) if mode.request.is_none() => ReasoningModeGate::Unsupported,
Some(_) => ReasoningModeGate::Usable,
}
}
/// Resolve a caller's `reasoning_mode` option against the catalog.
///
/// Returns the mode id to carry on the request, or `None` for the provider
/// default. `standard` is folded to `None` here so exactly one representation
/// of "no mode" reaches the rest of the pipeline. When `enforce_gates` is set,
/// a mode the model does not declare is a thrown error rather than a silently
/// ignored option: the caller is asking for materially more expensive work, so
/// failing to deliver it must not look like success.
pub(crate) fn resolve_requested(
requested: Option<&str>,
model: &str,
provider: &str,
enforce_gates: bool,
) -> Result<Option<String>, crate::value::VmError> {
let mode_id = match requested {
None | Some(STANDARD_MODE_ID) => return Ok(None),
Some(other) => other,
};
if enforce_gates {
let thrown = |message: String| {
crate::value::VmError::Thrown(crate::value::VmValue::String(arcstr::ArcStr::from(
message,
)))
};
match gate(model, mode_id) {
ReasoningModeGate::Usable => {}
ReasoningModeGate::Unsupported => {
return Err(thrown(format!(
"reasoning_mode: model \"{model}\" (provider \"{provider}\") declares no \
\"{mode_id}\" reasoning mode in the catalog; remove `reasoning_mode` or \
pick a model that advertises it under `reasoning_modes`"
)));
}
ReasoningModeGate::Deprecated { note } => {
let detail = note.map(|n| format!(" ({n})")).unwrap_or_default();
return Err(thrown(format!(
"reasoning_mode: the \"{mode_id}\" reasoning mode for model \"{model}\" is \
deprecated{detail}"
)));
}
}
}
Ok(Some(mode_id.to_string()))
}
/// Inject the reasoning-mode knob into an already-built provider body.
///
/// No-op when no mode is requested, when the request names the default mode,
/// or when the model has no usable declaration — so it is safe to call
/// unconditionally from every provider body builder.
///
/// Intermediate objects along `param_path` are created when absent. A value
/// already present at an intermediate segment is merged into, never replaced,
/// which is what keeps a caller's `reasoning.effort` intact.
pub(crate) fn apply_request_knob(body: &mut serde_json::Value, model: &str, mode_id: Option<&str>) {
let Some(mode_id) = mode_id.filter(|id| *id != STANDARD_MODE_ID) else {
return;
};
let Some(mode) = lookup(model, mode_id).filter(is_usable) else {
return;
};
let Some(request) = mode.request else {
return;
};
let Some((leaf, parents)) = request.param_path.split_last() else {
return;
};
let mut cursor = body;
for segment in parents {
if !cursor.is_object() {
return;
}
cursor = cursor
.as_object_mut()
.expect("checked object above")
.entry(segment.clone())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
}
let Some(object) = cursor.as_object_mut() else {
return;
};
object.insert(leaf.clone(), serde_json::Value::String(request.value));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lookup_resolves_openai_pro_mode_knob() {
let pro = lookup("gpt-5.6-sol", "pro").expect("sol advertises pro reasoning mode");
let request = pro.request.as_ref().expect("pro mode has a request knob");
assert_eq!(request.param_path, vec!["reasoning", "mode"]);
assert_eq!(request.value, "pro");
}
/// The whole reason this knob is path-addressed rather than flat: pro
/// mode shares the `reasoning` object with the effort the caller asked
/// for, and must not clobber it.
#[test]
fn applying_pro_mode_preserves_an_existing_effort() {
let mut body = serde_json::json!({
"model": "gpt-5.6-sol",
"reasoning": {"effort": "high"},
});
apply_request_knob(&mut body, "gpt-5.6-sol", Some("pro"));
assert_eq!(body["reasoning"]["effort"], "high");
assert_eq!(body["reasoning"]["mode"], "pro");
}
#[test]
fn applying_pro_mode_creates_the_reasoning_object_when_absent() {
let mut body = serde_json::json!({"model": "gpt-5.6-sol"});
apply_request_knob(&mut body, "gpt-5.6-sol", Some("pro"));
assert_eq!(body["reasoning"]["mode"], "pro");
}
/// Negative controls. Without these the assertions above would pass just
/// as happily if the function inserted `mode` unconditionally.
#[test]
fn standard_and_absent_modes_write_nothing() {
let baseline = serde_json::json!({"model": "gpt-5.6-sol"});
let mut standard = baseline.clone();
apply_request_knob(&mut standard, "gpt-5.6-sol", Some(STANDARD_MODE_ID));
assert_eq!(standard, baseline, "`standard` must be a no-op");
let mut none = baseline.clone();
apply_request_knob(&mut none, "gpt-5.6-sol", None);
assert_eq!(none, baseline, "no requested mode must be a no-op");
// A model with no `reasoning_modes` declaration at all.
let mut other = serde_json::json!({"model": "claude-opus-5"});
let other_baseline = other.clone();
apply_request_knob(&mut other, "claude-opus-5", Some("pro"));
assert_eq!(
other, other_baseline,
"a model that declares no pro mode must be untouched"
);
}
#[test]
fn gate_rejects_a_model_that_does_not_declare_the_mode() {
assert!(matches!(
gate("claude-opus-5", "pro"),
ReasoningModeGate::Unsupported
));
assert!(matches!(
gate("gpt-5.6-sol", "pro"),
ReasoningModeGate::Usable
));
}
}