fix(backend): derive Codex metadata from provider catalogs

Share Codex Responses request semantics across OpenAI and xAI providers so reasoning effort, presets, request kind, subagent kind, and compaction observations stay consistent.

Read xAI reasoning efforts from official catalog feature options instead of model-name hardcoding, and make Codex service tiers data-driven through ModelPresentation rather than API adapter defaults.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-07-27 16:27:41 +08:00
co-authored by Codex
parent 87496b5656
commit 2e9213a784
15 changed files with 545 additions and 168 deletions
@@ -189,6 +189,22 @@ fn codex_model_json(profile: &PublicModelProfile, index: usize) -> Value {
} else {
vec!["text"]
};
let additional_speed_tiers = presentation
.service_tiers()
.iter()
.filter_map(|tier| tier.speed_tier())
.collect::<Vec<_>>();
let service_tiers = presentation
.service_tiers()
.iter()
.map(|tier| {
json!({
"id": tier.id(),
"name": tier.name(),
"description": tier.description(),
})
})
.collect::<Vec<_>>();
json!({
"slug": id,
@@ -200,8 +216,8 @@ fn codex_model_json(profile: &PublicModelProfile, index: usize) -> Value {
"visibility": if presentation.hidden() { "hide" } else { "list" },
"supported_in_api": true,
"priority": index.saturating_add(1),
"additional_speed_tiers": [],
"service_tiers": [],
"additional_speed_tiers": additional_speed_tiers,
"service_tiers": service_tiers,
"availability_nux": Value::Null,
"upgrade": Value::Null,
"base_instructions": CODEX_BASE_INSTRUCTIONS,
@@ -10,7 +10,9 @@ use gateway_core::engine::execution::{
StartedExecution,
};
use gateway_core::error::{GatewayError, GatewayErrorKind};
use gateway_core::routing::{ModelPresentation, PublicModelId, PublicModelProfile};
use gateway_core::routing::{
ModelPresentation, ModelServiceTier, PublicModelId, PublicModelProfile,
};
use tower::ServiceExt;
use super::{api_router, authenticated_client};
@@ -73,7 +75,15 @@ impl ExecutionService for ModelsExecution {
)
.with_context_window_tokens(Some(500_000))
.with_image_input(true)
.with_agent_tools(true, true),
.with_agent_tools(true, true)
.with_service_tiers(vec![
ModelServiceTier::new(
"priority",
"Fast",
"Route the request through Codex fast mode.",
)
.with_speed_tier("fast"),
]),
)]
}
@@ -145,7 +155,15 @@ async fn models_should_encode_provider_profiles_for_current_codex_clients() {
assert_eq!(model["default_reasoning_level"], "medium");
assert_eq!(model["context_window"], 500_000);
assert_eq!(model["apply_patch_tool_type"], "freeform");
assert_eq!(model["service_tiers"], serde_json::json!([]));
assert_eq!(model["additional_speed_tiers"], serde_json::json!(["fast"]));
assert_eq!(
model["service_tiers"],
serde_json::json!([{
"id": "priority",
"name": "Fast",
"description": "Route the request through Codex fast mode."
}])
);
assert_eq!(
model["supported_reasoning_levels"],
serde_json::json!([
@@ -152,9 +152,61 @@ pub struct ModelPresentation {
search_tool: bool,
image_detail_original: bool,
verbosity: bool,
service_tiers: Vec<ModelServiceTier>,
hidden: bool,
}
/// Provider 声明给 Codex 客户端的服务档位。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelServiceTier {
id: String,
name: String,
description: String,
speed_tier: Option<String>,
}
impl ModelServiceTier {
#[must_use]
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
description: description.into(),
speed_tier: None,
}
}
#[must_use]
pub fn with_speed_tier(mut self, speed_tier: impl Into<String>) -> Self {
self.speed_tier = Some(speed_tier.into());
self
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn description(&self) -> &str {
&self.description
}
#[must_use]
pub fn speed_tier(&self) -> Option<&str> {
self.speed_tier.as_deref()
}
}
impl ModelPresentation {
#[must_use]
pub fn new(display_name: Option<String>, description: Option<String>) -> Self {
@@ -213,6 +265,12 @@ impl ModelPresentation {
self
}
#[must_use]
pub fn with_service_tiers(mut self, service_tiers: Vec<ModelServiceTier>) -> Self {
self.service_tiers = service_tiers;
self
}
#[must_use]
pub const fn with_hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
@@ -274,6 +332,11 @@ impl ModelPresentation {
self.verbosity
}
#[must_use]
pub fn service_tiers(&self) -> &[ModelServiceTier] {
&self.service_tiers
}
#[must_use]
pub const fn hidden(&self) -> bool {
self.hidden
@@ -0,0 +1,126 @@
use serde_json::{Map, Value};
const MULTI_AGENT_MODE_OPEN_TAG: &str = "<multi_agent_mode>";
const MULTI_AGENT_MODE_CLOSE_TAG: &str = "</multi_agent_mode>";
const PROACTIVE_MULTI_AGENT_MODE_PREFIX: &str = "Proactive multi-agent delegation is active.";
/// 从 OpenAI Responses 请求中提取的稳定 Codex 请求语义。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CodexResponsesRequestSemantics {
/// 客户端原始选择的推理强度。
pub reasoning_effort: Option<String>,
/// Codex turn metadata 中的请求类型。
pub request_kind: Option<String>,
/// Codex turn metadata 中的子代理类型。
pub subagent_kind: Option<String>,
/// 网关侧派生出的推理预设。
pub reasoning_preset: Option<&'static str>,
/// 请求是否为 Codex 压缩请求。
pub compact: bool,
}
/// 从 Responses body 与非 wire 协议上下文中提取 Codex 语义。
#[must_use]
pub fn codex_responses_request_semantics(
body: &Map<String, Value>,
context: &Map<String, Value>,
) -> CodexResponsesRequestSemantics {
codex_responses_request_semantics_with_turn_metadata(
body,
non_empty_string(context.get("turn_metadata")),
)
}
/// 调用方已解析权威 turn metadata 时,从 Responses body 中提取 Codex 语义。
#[must_use]
pub fn codex_responses_request_semantics_with_turn_metadata(
body: &Map<String, Value>,
turn_metadata: Option<&str>,
) -> CodexResponsesRequestSemantics {
let reasoning_effort = body
.get("reasoning")
.and_then(Value::as_object)
.and_then(|reasoning| non_empty_string(reasoning.get("effort")))
.map(ToOwned::to_owned);
let parsed_turn_metadata = turn_metadata
.or_else(|| request_turn_metadata(body))
.and_then(|value| serde_json::from_str::<Value>(value).ok());
let request_kind = parsed_turn_metadata
.as_ref()
.and_then(|metadata| non_empty_string(metadata.get("request_kind")))
.map(ToOwned::to_owned);
let subagent_kind = parsed_turn_metadata
.as_ref()
.and_then(|metadata| non_empty_string(metadata.get("subagent_kind")))
.map(ToOwned::to_owned);
let proactive_multi_agent = latest_multi_agent_mode(input_items(body).iter())
.is_some_and(|mode| mode.starts_with(PROACTIVE_MULTI_AGENT_MODE_PREFIX));
let compact_trigger = input_items(body)
.iter()
.any(|item| non_empty_string(item.get("type")) == Some("compaction_trigger"));
let reasoning_preset = (subagent_kind.is_none()
&& reasoning_effort.as_deref() == Some("max")
&& proactive_multi_agent)
.then_some("ultra");
let compact = request_kind.as_deref() == Some("compaction") || compact_trigger;
CodexResponsesRequestSemantics {
reasoning_effort,
request_kind,
subagent_kind,
reasoning_preset,
compact,
}
}
fn request_turn_metadata(body: &Map<String, Value>) -> Option<&str> {
non_empty_string(body.get("turnMetadata"))
.or_else(|| non_empty_string(body.get("turn_metadata")))
.or_else(|| non_empty_string(body.get("x-codex-turn-metadata")))
.or_else(|| client_metadata_string(body, "x-codex-turn-metadata"))
.or_else(|| client_metadata_string(body, "turnMetadata"))
.or_else(|| client_metadata_string(body, "turn_metadata"))
}
fn client_metadata_string<'a>(body: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
body.get("client_metadata")
.and_then(Value::as_object)
.and_then(|metadata| non_empty_string(metadata.get(key)))
}
fn input_items(body: &Map<String, Value>) -> &[Value] {
body.get("input")
.and_then(Value::as_array)
.map_or(&[], Vec::as_slice)
}
fn latest_multi_agent_mode<'a>(
input: impl DoubleEndedIterator<Item = &'a Value>,
) -> Option<&'a str> {
input.rev().find_map(|item| {
if non_empty_string(item.get("role")) != Some("developer") {
return None;
}
item.get("content")?
.as_array()?
.iter()
.rev()
.filter_map(|content| non_empty_string(content.get("text")))
.find_map(multi_agent_mode_from_text)
})
}
fn multi_agent_mode_from_text(text: &str) -> Option<&str> {
let close = text.rfind(MULTI_AGENT_MODE_CLOSE_TAG)?;
let before_close = &text[..close];
let open = before_close.rfind(MULTI_AGENT_MODE_OPEN_TAG)?;
let body = &before_close[open + MULTI_AGENT_MODE_OPEN_TAG.len()..];
Some(body.trim())
}
fn non_empty_string(value: Option<&Value>) -> Option<&str> {
value
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
}
@@ -9,7 +9,14 @@ pub const WS_REQUEST_HEADER_RESPONSES_LITE_CLIENT_METADATA_KEY: &str =
/// Codex memory consolidation 请求标记。
pub const X_OPENAI_MEMGEN_REQUEST_HEADER: &str = "x-openai-memgen-request";
/// OpenAI Responses 请求中携带的 Codex 专属语义。
pub mod codex;
/// OpenAI/Codex 事件语义、用量与限流字段编解码。
pub mod events;
/// Server-Sent Events 帧的解析与编码。
pub mod sse;
pub use codex::{
CodexResponsesRequestSemantics, codex_responses_request_semantics,
codex_responses_request_semantics_with_turn_metadata,
};
@@ -0,0 +1,61 @@
use gateway_protocol::openai::codex_responses_request_semantics;
use serde_json::{Map, Value, json};
#[test]
fn codex_semantics_should_use_protocol_context_before_body_metadata() {
let body = json!({
"model": "grok-4.5",
"reasoning": {"effort": " xhigh "},
"client_metadata": {
"x-codex-turn-metadata": "{\"request_kind\":\"review\"}"
},
"input": [
{"type": "message", "role": "user", "content": "history"},
{"type": "compaction_trigger"}
]
});
let body = object(body);
let context = Map::from_iter([(
"turn_metadata".to_owned(),
json!("{\"request_kind\":\"compaction\",\"subagent_kind\":\"compact\"}"),
)]);
let semantics = codex_responses_request_semantics(&body, &context);
assert_eq!(semantics.reasoning_effort.as_deref(), Some("xhigh"));
assert_eq!(semantics.request_kind.as_deref(), Some("compaction"));
assert_eq!(semantics.subagent_kind.as_deref(), Some("compact"));
assert_eq!(semantics.reasoning_preset, None);
assert!(semantics.compact);
}
#[test]
fn codex_semantics_should_detect_ultra_preset_from_proactive_multi_agent() {
let body = object(json!({
"model": "gpt-test",
"reasoning": {"effort": "max"},
"input": [
{
"type": "message",
"role": "developer",
"content": [{
"type": "input_text",
"text": "<multi_agent_mode>Proactive multi-agent delegation is active.</multi_agent_mode>"
}]
}
]
}));
let semantics = codex_responses_request_semantics(&body, &Map::new());
assert_eq!(semantics.reasoning_effort.as_deref(), Some("max"));
assert_eq!(semantics.reasoning_preset, Some("ultra"));
assert!(!semantics.compact);
}
fn object(value: Value) -> Map<String, Value> {
let Value::Object(object) = value else {
panic!("test value must be an object");
};
object
}
@@ -1,2 +1,3 @@
mod codex;
mod events;
mod sse;
@@ -177,14 +177,9 @@ impl Provider for CodexProvider {
input.extend(encoded.input().iter().cloned());
encoded.set_input(input);
}
let reasoning_effort = encoded
.reasoning()
.and_then(|reasoning| reasoning.get("effort"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_owned);
(encoded.semantics(), reasoning_effort)
let semantics = encoded.semantics();
let reasoning_effort = semantics.reasoning_effort.clone();
(semantics, reasoning_effort)
})
.unwrap_or_default();
ProviderRequestObservation {
@@ -1,13 +1,12 @@
use std::fmt;
use gateway_protocol::openai::events;
use gateway_protocol::openai::{
CodexResponsesRequestSemantics as CodexRequestSemantics,
codex_responses_request_semantics_with_turn_metadata, events,
};
use serde::Serialize;
use serde_json::{Map, Value};
const MULTI_AGENT_MODE_OPEN_TAG: &str = "<multi_agent_mode>";
const MULTI_AGENT_MODE_CLOSE_TAG: &str = "</multi_agent_mode>";
const PROACTIVE_MULTI_AGENT_MODE_PREFIX: &str = "Proactive multi-agent delegation is active.";
/// Codex Responses 上游请求体。
///
/// 发往上游的 Responses 请求。`body` 持有客户端原始 JSON object,逐字段(含顺序、
@@ -93,19 +92,6 @@ pub enum PreviousResponseScope {
ExternalUnknown,
}
/// Responses 请求携带的 Codex 运行语义。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CodexRequestSemantics {
/// Codex turn metadata 中的请求类型。
pub request_kind: Option<String>,
/// Codex turn metadata 中的子代理类型。
pub subagent_kind: Option<String>,
/// Codex 客户端选择的推理预设。
pub reasoning_preset: Option<&'static str>,
/// 请求是否为远端压缩。
pub compact: bool,
}
impl Serialize for CodexResponsesRequest {
/// 上游 body 序列化即原始 `body` map(HTTP SSE 直发;WebSocket 在外层前置 `type`)。
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -608,34 +594,12 @@ impl CodexResponsesRequest {
/// 提取 Codex 请求类型、子代理类型、推理预设与压缩语义。
pub fn semantics(&self) -> CodexRequestSemantics {
let turn_metadata = self.turn_metadata.as_deref().or_else(|| {
self.client_metadata()?
.get("x-codex-turn-metadata")?
.as_str()
});
let effort = self
.reasoning()
.and_then(|value| value.get("effort"))
.and_then(Value::as_str);
let proactive_multi_agent = self
.latest_multi_agent_mode()
.is_some_and(|mode| mode.starts_with(PROACTIVE_MULTI_AGENT_MODE_PREFIX));
let compact_trigger = self
.input()
.iter()
.any(|item| item.get("type").and_then(Value::as_str) == Some("compaction_trigger"));
codex_request_semantics_from_parts(
turn_metadata,
effort,
proactive_multi_agent,
compact_trigger,
codex_responses_request_semantics_with_turn_metadata(
self.body(),
self.turn_metadata.as_deref(),
)
}
fn latest_multi_agent_mode(&self) -> Option<&str> {
latest_multi_agent_mode(self.input().iter())
}
/// 设置 / 合并 client metadata。
pub fn set_client_metadata(&mut self, client_metadata: Option<Value>) {
match client_metadata {
@@ -664,61 +628,3 @@ impl CodexResponsesRequest {
}
}
}
fn non_empty_owned_string(value: &str) -> Option<String> {
let value = value.trim();
(!value.is_empty()).then(|| value.to_string())
}
pub(crate) fn codex_request_semantics_from_parts(
turn_metadata: Option<&str>,
effort: Option<&str>,
proactive_multi_agent: bool,
compact_trigger: bool,
) -> CodexRequestSemantics {
let turn_metadata = turn_metadata.and_then(|value| serde_json::from_str::<Value>(value).ok());
let request_kind = turn_metadata
.as_ref()
.and_then(|metadata| metadata.get("request_kind"))
.and_then(Value::as_str)
.and_then(non_empty_owned_string);
let subagent_kind = turn_metadata
.as_ref()
.and_then(|metadata| metadata.get("subagent_kind"))
.and_then(Value::as_str)
.and_then(non_empty_owned_string);
let reasoning_preset =
(subagent_kind.is_none() && effort == Some("max") && proactive_multi_agent)
.then_some("ultra");
let compact = request_kind.as_deref() == Some("compaction") || compact_trigger;
CodexRequestSemantics {
request_kind,
subagent_kind,
reasoning_preset,
compact,
}
}
fn latest_multi_agent_mode<'a>(
input: impl DoubleEndedIterator<Item = &'a Value>,
) -> Option<&'a str> {
input.rev().find_map(|item| {
if item.get("role").and_then(Value::as_str) != Some("developer") {
return None;
}
item.get("content")?
.as_array()?
.iter()
.rev()
.filter_map(|content| content.get("text").and_then(Value::as_str))
.find_map(multi_agent_mode_from_text)
})
}
fn multi_agent_mode_from_text(text: &str) -> Option<&str> {
let close = text.rfind(MULTI_AGENT_MODE_CLOSE_TAG)?;
let before_close = &text[..close];
let open = before_close.rfind(MULTI_AGENT_MODE_OPEN_TAG)?;
let body = &before_close[open + MULTI_AGENT_MODE_OPEN_TAG.len()..];
Some(body.trim())
}
+22 -34
View File
@@ -37,6 +37,7 @@ use gateway_core::task::{
WorkerKind, WorkerLeaseRequest, WorkerRegistration, WorkerRunnable, WorkerSchedule,
WorkerTaskError,
};
use gateway_protocol::openai::codex_responses_request_semantics;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use url::Url;
@@ -124,19 +125,17 @@ impl Provider for GrokBuildProvider {
let Operation::Generate(request) = operation else {
return ProviderRequestObservation::default();
};
let reasoning_effort = request
.protocol_payload()
.body()
.get("reasoning")
.and_then(Value::as_object)
.and_then(|reasoning| reasoning.get("effort"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_owned);
let payload = request.protocol_payload();
if payload.protocol() != "openai" {
return ProviderRequestObservation::default();
}
let semantics = codex_responses_request_semantics(payload.body(), payload.context());
ProviderRequestObservation {
reasoning_effort,
..ProviderRequestObservation::default()
reasoning_effort: semantics.reasoning_effort,
reasoning_preset: semantics.reasoning_preset.map(str::to_owned),
request_kind: semantics.request_kind,
subagent_kind: semantics.subagent_kind,
compact: semantics.compact,
}
}
@@ -426,12 +425,6 @@ fn default_grok_model_presentation() -> ModelPresentation {
Some("Grok 4.5".to_owned()),
Some("xAI Grok 4.5 frontier model with reasoning and vision.".to_owned()),
)
.with_reasoning(
Some("medium".to_owned()),
["low", "medium", "high", "xhigh"]
.map(str::to_owned)
.to_vec(),
)
.with_context_window_tokens(Some(500_000))
.with_image_input(true)
.with_agent_tools(true, true)
@@ -452,25 +445,20 @@ fn grok_model_presentation(model: &GrokCatalogModel) -> ModelPresentation {
.default_reasoning_effort()
.map(|effort| effort.as_str().to_owned());
let (default_reasoning, reasoning_efforts) = match reasoning_evidence {
GrokCatalogCapabilityEvidence::DeclaredNative if !catalog_reasoning_efforts.is_empty() => (
catalog_default_reasoning.or_else(|| catalog_reasoning_efforts.first().cloned()),
catalog_reasoning_efforts,
),
GrokCatalogCapabilityEvidence::DeclaredNative => (
catalog_default_reasoning.or_else(|| Some("medium".to_owned())),
["low", "medium", "high", "xhigh"]
.map(str::to_owned)
.to_vec(),
),
GrokCatalogCapabilityEvidence::DeclaredNative if !catalog_reasoning_efforts.is_empty() => {
let default_reasoning = catalog_default_reasoning
.filter(|default| {
catalog_reasoning_efforts
.iter()
.any(|effort| effort == default)
})
.or_else(|| catalog_reasoning_efforts.first().cloned());
(default_reasoning, catalog_reasoning_efforts)
}
GrokCatalogCapabilityEvidence::DeclaredNative => (None, Vec::new()),
GrokCatalogCapabilityEvidence::DeclaredUnsupported => {
(Some("none".to_owned()), vec!["none".to_owned()])
}
GrokCatalogCapabilityEvidence::Unknown if known_grok_4_5 => (
catalog_default_reasoning.or_else(|| Some("medium".to_owned())),
["low", "medium", "high", "xhigh"]
.map(str::to_owned)
.to_vec(),
),
GrokCatalogCapabilityEvidence::Unknown => (None, Vec::new()),
};
let context_window_tokens = model
@@ -901,6 +901,7 @@ struct GrokModelWire {
reasoning_effort: Option<GrokCatalogReasoningEffort>,
#[serde(default, rename = "reasoningEfforts", alias = "reasoning_efforts")]
reasoning_efforts: Vec<GrokReasoningEffortOptionWire>,
features: Option<GrokModelFeaturesWire>,
#[serde(rename = "supportsBackendSearch", alias = "supports_backend_search")]
supports_backend_search: Option<bool>,
#[serde(rename = "streamToolCalls", alias = "stream_tool_calls")]
@@ -908,6 +909,21 @@ struct GrokModelWire {
hidden: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct GrokModelFeaturesWire {
reasoning: Option<bool>,
#[serde(rename = "reasoningEffortOptions", alias = "reasoning_effort_options")]
reasoning_effort_options: Option<GrokReasoningEffortOptionsWire>,
}
#[derive(Debug, Deserialize)]
struct GrokReasoningEffortOptionsWire {
#[serde(default, rename = "supportedEfforts", alias = "supported_efforts")]
supported_efforts: Vec<GrokCatalogReasoningEffort>,
#[serde(rename = "defaultEffort", alias = "default_effort")]
default_effort: Option<GrokCatalogReasoningEffort>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum GrokReasoningEffortOptionWire {
@@ -993,29 +1009,49 @@ fn normalize_model(wire: GrokModelWire) -> Result<GrokCatalogModel, GrokModelCat
.api_backend
.filter(|backend| *backend != GrokCatalogApiBackend::Unknown);
let responses_api = responses_evidence(wire.supported_in_api, api_backend);
let mut menu_default = None;
let features = wire.features;
let feature_reasoning = features.as_ref().and_then(|features| features.reasoning);
let feature_reasoning_options = features.and_then(|features| features.reasoning_effort_options);
let mut top_level_menu_default = None;
// 未识别的 effort 值不进入能力投影,也不作为默认值候选。
let reasoning_efforts = wire
.reasoning_efforts
.into_iter()
.filter_map(|option| {
let (effort, is_default) = option.into_parts();
if effort == GrokCatalogReasoningEffort::Unknown {
return None;
}
if is_default && menu_default.is_none() {
menu_default = Some(effort);
}
Some(effort)
let top_level_reasoning_efforts =
reasoning_effort_menu(wire.reasoning_efforts, &mut top_level_menu_default);
let (feature_reasoning_efforts, feature_default_reasoning_effort) = feature_reasoning_options
.map_or_else(
|| (Vec::new(), None),
|options| {
(
options
.supported_efforts
.into_iter()
.filter(|effort| *effort != GrokCatalogReasoningEffort::Unknown)
.collect::<Vec<_>>(),
options
.default_effort
.filter(|effort| *effort != GrokCatalogReasoningEffort::Unknown),
)
},
);
let reasoning_efforts = if feature_reasoning_efforts.is_empty() {
top_level_reasoning_efforts
} else {
feature_reasoning_efforts
};
let default_reasoning_effort = feature_default_reasoning_effort
.or_else(|| {
wire.reasoning_effort
.filter(|effort| *effort != GrokCatalogReasoningEffort::Unknown)
})
.collect::<Vec<_>>();
let default_reasoning_effort = wire
.reasoning_effort
.filter(|effort| *effort != GrokCatalogReasoningEffort::Unknown)
.or(menu_default)
.or(top_level_menu_default)
.or_else(|| reasoning_efforts.first().copied());
let default_reasoning_effort = default_reasoning_effort
.filter(|default| reasoning_efforts.iter().any(|effort| effort == default))
.or_else(|| reasoning_efforts.first().copied());
let reasoning_effort = if reasoning_efforts.is_empty() {
GrokCatalogCapabilityEvidence::from_wire(wire.supports_reasoning_effort)
GrokCatalogCapabilityEvidence::from_wire(
wire.supports_reasoning_effort.or(feature_reasoning),
)
} else {
GrokCatalogCapabilityEvidence::DeclaredNative
};
@@ -1044,6 +1080,25 @@ fn normalize_model(wire: GrokModelWire) -> Result<GrokCatalogModel, GrokModelCat
})
}
fn reasoning_effort_menu(
values: Vec<GrokReasoningEffortOptionWire>,
menu_default: &mut Option<GrokCatalogReasoningEffort>,
) -> Vec<GrokCatalogReasoningEffort> {
values
.into_iter()
.filter_map(|option| {
let (effort, is_default) = option.into_parts();
if effort == GrokCatalogReasoningEffort::Unknown {
return None;
}
if is_default && menu_default.is_none() {
*menu_default = Some(effort);
}
Some(effort)
})
.collect()
}
fn responses_evidence(
supported_in_api: Option<bool>,
api_backend: Option<GrokCatalogApiBackend>,
@@ -193,9 +193,10 @@ impl GrokResponsesRequest {
if should_consume_terminal_compaction_trigger {
consume_terminal_compaction_trigger(&mut body)?;
}
// `provider_options` 是已废弃的网关私有 envelope,不是 xAI 上游协议字段。
// 这些字段属于 Codex/OpenAI 侧请求控制,不是 xAI 上游协议字段。
// OpenAI 透明路径会保留未知字段;这里只在 xAI adapter 内做最小剥离。
body.remove("provider_options");
body.remove("service_tier");
let session_seed = explicit_session_seed(request, &body);
let enable_cache_route = allow_cache_route && session_seed.is_some();
let identity = resolve_session_identity(
@@ -72,6 +72,27 @@ const CATALOG_WITHOUT_FEATURE_METADATA: &[u8] = br#"{
"supportsReasoningEffort": true
}]
}"#;
const CATALOG_WITH_FEATURE_REASONING_OPTIONS: &[u8] = br#"{
"object": "list",
"data": [{
"id": "grok-4.5",
"model": "grok-4.5",
"contextWindow": 500000,
"maxCompletionTokens": 131072,
"apiBackend": "responses",
"supportedInApi": true,
"reasoningEfforts": ["high", {"value": "medium", "default": true}, "low"],
"features": {
"reasoning": true,
"reasoningEffortOptions": {
"supportedEfforts": ["low", "medium", "high", "xhigh"],
"defaultEffort": "high"
}
},
"supportsReasoningEffort": true,
"streamToolCalls": true
}]
}"#;
const SUCCESS_SSE: &[u8] = concat!(
"event: response.created\n",
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_xai\",\"model\":\"grok-4.5\"}}\n\n",
@@ -381,6 +402,19 @@ impl GrokModelCatalogTransport for CatalogWithoutFeatureMetadataTransport {
}
}
struct CatalogWithFeatureReasoningOptionsTransport;
impl GrokModelCatalogTransport for CatalogWithFeatureReasoningOptionsTransport {
fn execute(&self, _: GrokModelCatalogRequest) -> GrokModelCatalogTransportFuture<'_> {
Box::pin(async {
Ok(GrokModelCatalogTransportResponse::new(
CATALOG_WITH_FEATURE_REASONING_OPTIONS,
None,
))
})
}
}
struct UnavailableCatalogTransport;
impl GrokModelCatalogTransport for UnavailableCatalogTransport {
@@ -547,6 +581,13 @@ fn operation_with_reasoning_effort(effort: &str) -> Operation {
Operation::Generate(GenerateRequest::from_protocol_payload(payload))
}
fn object(value: serde_json::Value) -> Map<String, serde_json::Value> {
let serde_json::Value::Object(object) = value else {
panic!("request body must be an object");
};
object
}
fn operation_with_invalid_tools() -> Operation {
let payload = ProtocolPayload::json_object(
"openai",
@@ -708,6 +749,35 @@ async fn request_observation_preserves_raw_xai_reasoning_effort() {
);
}
#[tokio::test]
async fn request_observation_preserves_codex_semantics_for_xai() {
let provider = provider(StubSelector::success(), StubInferenceTransport::success()).await;
let payload = ProtocolPayload::json_object(
"openai",
object(json!({
"model": "client-model",
"reasoning": {"effort": "xhigh"},
"input": [
{"type": "message", "role": "user", "content": "history"},
{"type": "compaction_trigger"}
]
})),
)
.expect("OpenAI payload")
.with_context(Map::from_iter([(
"turn_metadata".to_owned(),
json!("{\"request_kind\":\"compaction\",\"subagent_kind\":\"compact\"}"),
)]));
let operation = Operation::Generate(GenerateRequest::from_protocol_payload(payload));
let observation = provider.request_observation(&operation);
assert_eq!(observation.reasoning_effort.as_deref(), Some("xhigh"));
assert_eq!(observation.request_kind.as_deref(), Some("compaction"));
assert_eq!(observation.subagent_kind.as_deref(), Some("compact"));
assert!(observation.compact);
}
fn operation_with_state(body: serde_json::Value, state: ProviderSessionState) -> Operation {
let serde_json::Value::Object(body) = body else {
panic!("request body must be an object");
@@ -1924,11 +1994,37 @@ async fn provider_publishes_default_codex_profile_when_catalog_is_unavailable()
.presentation()
.expect("fallback Grok model presentation");
assert_eq!(presentation.display_name(), Some("Grok 4.5"));
assert_eq!(presentation.default_reasoning_effort(), None);
assert!(presentation.supported_reasoning_efforts().is_empty());
assert_eq!(presentation.context_window_tokens(), Some(500_000));
assert!(presentation.agent_tools());
assert!(presentation.parallel_tool_calls());
}
#[tokio::test]
async fn provider_publishes_reasoning_efforts_from_feature_options() {
let provider = provider_with_catalog_transport(
StubSelector::success(),
StubInferenceTransport::success(),
StubRecovery::new(GrokCredentialRecoveryOutcome::Unavailable),
Arc::new(CatalogWithFeatureReasoningOptionsTransport),
)
.await;
let capabilities = provider
.query_model_capabilities()
.await
.expect("capabilities");
let presentation = capabilities[0]
.presentation()
.expect("Grok model presentation");
assert_eq!(presentation.default_reasoning_effort(), Some("high"));
assert_eq!(
presentation.supported_reasoning_efforts(),
["low", "medium", "high", "xhigh"]
);
}
#[tokio::test]
async fn missing_catalog_tool_metadata_keeps_build_tools_routable() {
let provider = provider_with_catalog_transport(
@@ -376,6 +376,35 @@ fn reasoning_effort_menu_should_be_capability_evidence_without_legacy_flag() {
);
}
#[test]
fn reasoning_effort_options_should_read_official_features_shape() {
let snapshot = parse_grok_model_catalog(
br#"{"object":"list","data":[{"id":"grok-4.5","reasoningEfforts":["high"],"features":{"reasoning":true,"reasoningEffortOptions":{"supportedEfforts":["low","medium","high","xhigh"],"defaultEffort":"high"}}}]}"#,
None,
)
.expect("official feature reasoning options should parse");
let capabilities = snapshot.models()[0].capabilities();
assert_eq!(
(
capabilities.reasoning_effort(),
capabilities
.reasoning_efforts()
.iter()
.map(|effort| effort.as_str())
.collect::<Vec<_>>(),
capabilities
.default_reasoning_effort()
.map(|effort| effort.as_str()),
),
(
GrokCatalogCapabilityEvidence::DeclaredNative,
vec!["low", "medium", "high", "xhigh"],
Some("high"),
)
);
}
#[test]
fn unknown_reasoning_effort_values_should_not_fail_the_snapshot() {
let snapshot = parse_grok_model_catalog(
@@ -92,6 +92,21 @@ fn encoder_should_strip_openai_only_client_metadata_before_grok_build() {
);
}
#[test]
fn encoder_should_strip_openai_service_tier_before_grok_build() {
let request = raw_request(json!({
"model": "client-model",
"input": "hello",
"service_tier": "priority"
}));
let encoded = GrokResponsesRequest::encode(&request, "grok-routed", &client_key())
.expect("sanitized request");
let body = Value::Object(encoded.body().clone());
assert_eq!(body.pointer("/service_tier"), None);
}
#[test]
fn account_identity_should_be_removed_without_touching_prompt_content() {
let request = raw_request(json!({