diff --git a/backend/crates/gateway-api/src/openai/models.rs b/backend/crates/gateway-api/src/openai/models.rs index 6aea76f91..7d6357526 100644 --- a/backend/crates/gateway-api/src/openai/models.rs +++ b/backend/crates/gateway-api/src/openai/models.rs @@ -209,6 +209,7 @@ fn codex_model_json(profile: &PublicModelProfile, index: usize) -> Value { fn reasoning_effort_description(effort: &str) -> &'static str { match effort { "none" => "No reasoning", + "minimal" => "Minimal reasoning", "low" => "Fast responses with lighter reasoning", "medium" => "Balances speed and reasoning depth for everyday tasks", "high" => "Greater reasoning depth for complex problems", diff --git a/backend/crates/gateway-api/tests/openai/models.rs b/backend/crates/gateway-api/tests/openai/models.rs index 9b27c9db6..7ddb559c4 100644 --- a/backend/crates/gateway-api/tests/openai/models.rs +++ b/backend/crates/gateway-api/tests/openai/models.rs @@ -67,7 +67,9 @@ impl ExecutionService for ModelsExecution { ) .with_reasoning( Some("medium".to_owned()), - ["low", "medium", "high"].map(str::to_owned).to_vec(), + ["low", "medium", "high", "xhigh"] + .map(str::to_owned) + .to_vec(), ) .with_context_window_tokens(Some(500_000)) .with_image_input(true) @@ -138,6 +140,27 @@ async fn models_should_encode_provider_profiles_for_current_codex_clients() { 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["supported_reasoning_levels"], + serde_json::json!([ + { + "effort": "low", + "description": "Fast responses with lighter reasoning" + }, + { + "effort": "medium", + "description": "Balances speed and reasoning depth for everyday tasks" + }, + { + "effort": "high", + "description": "Greater reasoning depth for complex problems" + }, + { + "effort": "xhigh", + "description": "Extra high reasoning depth for complex problems" + } + ]) + ); } #[tokio::test] diff --git a/backend/crates/providers/xai/src/lib.rs b/backend/crates/providers/xai/src/lib.rs index 9addecbad..966bd01d3 100644 --- a/backend/crates/providers/xai/src/lib.rs +++ b/backend/crates/providers/xai/src/lib.rs @@ -57,24 +57,25 @@ pub use transport::{ GrokBillingTransportError, GrokBillingTransportErrorKind, GrokBillingTransportFuture, GrokBillingTransportResponse, GrokCanonicalDecoder, GrokCatalogApiBackend, GrokCatalogCapabilities, GrokCatalogCapabilityEvidence, GrokCatalogLimits, GrokCatalogMetadata, - GrokCatalogModel, GrokClientIdentity, GrokCompactionDecodeError, GrokCompactionRequest, - GrokCompactionSummaryDecoder, GrokCredentialFailure, GrokCredentialFeedbackFuture, - GrokDnsResolutionError, GrokDnsResolutionPlan, GrokDnsResolutionPolicy, GrokEndpointPolicy, - GrokHeader, GrokHeaderValue, GrokInferenceChunkStream, GrokInferenceClientCacheStatus, - GrokInferenceDnsObservation, GrokInferenceDnsSource, GrokInferenceRequest, - GrokInferenceResponse, GrokInferenceTransport, GrokInferenceTransportError, - GrokInferenceTransportErrorKind, GrokInferenceTransportFuture, GrokInferenceTransportMetrics, - GrokModelCatalogClient, GrokModelCatalogError, GrokModelCatalogRequest, - GrokModelCatalogSession, GrokModelCatalogSessionError, GrokModelCatalogSnapshot, - GrokModelCatalogTransport, GrokModelCatalogTransportError, GrokModelCatalogTransportErrorKind, - GrokModelCatalogTransportFuture, GrokModelCatalogTransportResponse, GrokProviderConfigError, - GrokProviderTransport, GrokRequestEncodeError, GrokReqwestTransportBuildError, - GrokResponsesRequest, GrokSessionAffinityKey, GrokSessionBinding, GrokSessionDataError, - GrokSessionLeaseGuard, GrokSessionSelection, GrokSessionSelector, GrokSessionSelectorError, - GrokSessionSelectorFuture, MAX_GROK_BILLING_BYTES, MAX_GROK_MODEL_CATALOG_BYTES, - OfficialGrokEndpointPolicy, ReqwestGrokInferenceTransport, ReqwestGrokModelCatalogTransport, - ReqwestOAuthTransport, SelectedGrokSession, XAI_PROVIDER_NAME, build_grok_headers, - grok_billing_breakdown, parse_grok_billing, parse_grok_model_catalog, + GrokCatalogModel, GrokCatalogReasoningEffort, GrokClientIdentity, GrokCompactionDecodeError, + GrokCompactionRequest, GrokCompactionSummaryDecoder, GrokCredentialFailure, + GrokCredentialFeedbackFuture, GrokDnsResolutionError, GrokDnsResolutionPlan, + GrokDnsResolutionPolicy, GrokEndpointPolicy, GrokHeader, GrokHeaderValue, + GrokInferenceChunkStream, GrokInferenceClientCacheStatus, GrokInferenceDnsObservation, + GrokInferenceDnsSource, GrokInferenceRequest, GrokInferenceResponse, GrokInferenceTransport, + GrokInferenceTransportError, GrokInferenceTransportErrorKind, GrokInferenceTransportFuture, + GrokInferenceTransportMetrics, GrokModelCatalogClient, GrokModelCatalogError, + GrokModelCatalogRequest, GrokModelCatalogSession, GrokModelCatalogSessionError, + GrokModelCatalogSnapshot, GrokModelCatalogTransport, GrokModelCatalogTransportError, + GrokModelCatalogTransportErrorKind, GrokModelCatalogTransportFuture, + GrokModelCatalogTransportResponse, GrokProviderConfigError, GrokProviderTransport, + GrokRequestEncodeError, GrokReqwestTransportBuildError, GrokResponsesRequest, + GrokSessionAffinityKey, GrokSessionBinding, GrokSessionDataError, GrokSessionLeaseGuard, + GrokSessionSelection, GrokSessionSelector, GrokSessionSelectorError, GrokSessionSelectorFuture, + MAX_GROK_BILLING_BYTES, MAX_GROK_MODEL_CATALOG_BYTES, OfficialGrokEndpointPolicy, + ReqwestGrokInferenceTransport, ReqwestGrokModelCatalogTransport, ReqwestOAuthTransport, + SelectedGrokSession, XAI_PROVIDER_NAME, build_grok_headers, grok_billing_breakdown, + parse_grok_billing, parse_grok_model_catalog, }; /// xAI 初始化后交给组装根的最小能力集。 diff --git a/backend/crates/providers/xai/src/provider.rs b/backend/crates/providers/xai/src/provider.rs index ece77fd61..c0e912481 100644 --- a/backend/crates/providers/xai/src/provider.rs +++ b/backend/crates/providers/xai/src/provider.rs @@ -430,7 +430,9 @@ fn default_grok_model_presentation() -> ModelPresentation { ) .with_reasoning( Some("medium".to_owned()), - ["low", "medium", "high"].map(str::to_owned).to_vec(), + ["low", "medium", "high", "xhigh"] + .map(str::to_owned) + .to_vec(), ) .with_context_window_tokens(Some(500_000)) .with_image_input(true) @@ -444,17 +446,35 @@ fn grok_model_presentation(model: &GrokCatalogModel) -> ModelPresentation { DEFAULT_GROK_MODEL | "grok-4.5-latest" | "grok-4.5-build-free" | "grok-build-latest" ); let reasoning_evidence = model.capabilities().reasoning_effort(); + let catalog_reasoning_efforts = model + .capabilities() + .reasoning_efforts() + .iter() + .map(|effort| effort.as_str().to_owned()) + .collect::>(); + let catalog_default_reasoning = model + .capabilities() + .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 => ( - Some("medium".to_owned()), - ["low", "medium", "high"].map(str::to_owned).to_vec(), + catalog_default_reasoning.or_else(|| Some("medium".to_owned())), + ["low", "medium", "high", "xhigh"] + .map(str::to_owned) + .to_vec(), ), GrokCatalogCapabilityEvidence::DeclaredUnsupported => { (Some("none".to_owned()), vec!["none".to_owned()]) } GrokCatalogCapabilityEvidence::Unknown if known_grok_4_5 => ( - Some("medium".to_owned()), - ["low", "medium", "high"].map(str::to_owned).to_vec(), + catalog_default_reasoning.or_else(|| Some("medium".to_owned())), + ["low", "medium", "high", "xhigh"] + .map(str::to_owned) + .to_vec(), ), GrokCatalogCapabilityEvidence::Unknown => (None, Vec::new()), }; diff --git a/backend/crates/providers/xai/src/transport/catalog.rs b/backend/crates/providers/xai/src/transport/catalog.rs index 4937f2170..4edff3093 100644 --- a/backend/crates/providers/xai/src/transport/catalog.rs +++ b/backend/crates/providers/xai/src/transport/catalog.rs @@ -589,11 +589,42 @@ pub enum GrokCatalogApiBackend { Unknown, } +/// Grok 官方目录允许声明的 reasoning effort。 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum GrokCatalogReasoningEffort { + None, + Minimal, + Low, + Medium, + High, + Xhigh, + Max, +} + +impl GrokCatalogReasoningEffort { + /// 返回 Responses API 使用的规范值。 + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::None => "none", + Self::Minimal => "minimal", + Self::Low => "low", + Self::Medium => "medium", + Self::High => "high", + Self::Xhigh => "xhigh", + Self::Max => "max", + } + } +} + /// Grok 目录中允许进入控制面的能力证据。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct GrokCatalogCapabilities { responses_api: GrokCatalogCapabilityEvidence, reasoning_effort: GrokCatalogCapabilityEvidence, + reasoning_efforts: Vec, + default_reasoning_effort: Option, backend_search: GrokCatalogCapabilityEvidence, streaming_tool_calls: GrokCatalogCapabilityEvidence, api_backend: Option, @@ -612,6 +643,18 @@ impl GrokCatalogCapabilities { self.reasoning_effort } + /// 返回上游声明的可选 reasoning effort,顺序与目录一致。 + #[must_use] + pub fn reasoning_efforts(&self) -> &[GrokCatalogReasoningEffort] { + &self.reasoning_efforts + } + + /// 返回上游声明或菜单推导出的默认 reasoning effort。 + #[must_use] + pub const fn default_reasoning_effort(&self) -> Option { + self.default_reasoning_effort + } + /// 返回后端 search 支持证据。 #[must_use] pub const fn backend_search(&self) -> GrokCatalogCapabilityEvidence { @@ -850,6 +893,10 @@ struct GrokModelWire { alias = "supports_reasoning_effort" )] supports_reasoning_effort: Option, + #[serde(rename = "reasoningEffort", alias = "reasoning_effort")] + reasoning_effort: Option, + #[serde(default, rename = "reasoningEfforts", alias = "reasoning_efforts")] + reasoning_efforts: Vec, #[serde(rename = "supportsBackendSearch", alias = "supports_backend_search")] supports_backend_search: Option, #[serde(rename = "streamToolCalls", alias = "stream_tool_calls")] @@ -857,6 +904,26 @@ struct GrokModelWire { hidden: Option, } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum GrokReasoningEffortOptionWire { + Bare(GrokCatalogReasoningEffort), + Detailed { + value: GrokCatalogReasoningEffort, + #[serde(default)] + default: bool, + }, +} + +impl GrokReasoningEffortOptionWire { + fn into_parts(self) -> (GrokCatalogReasoningEffort, bool) { + match self { + Self::Bare(value) => (value, false), + Self::Detailed { value, default } => (value, default), + } + } +} + /// 解析唯一受支持的 Grok 官方完整快照。 /// /// # Errors @@ -922,15 +989,36 @@ fn normalize_model(wire: GrokModelWire) -> Result>(); + let default_reasoning_effort = wire + .reasoning_effort + .or(menu_default) + .or_else(|| reasoning_efforts.first().copied()); + let reasoning_effort = if reasoning_efforts.is_empty() { + GrokCatalogCapabilityEvidence::from_wire(wire.supports_reasoning_effort) + } else { + GrokCatalogCapabilityEvidence::DeclaredNative + }; Ok(GrokCatalogModel { request_model, display_name: wire.name, capabilities: GrokCatalogCapabilities { responses_api, - reasoning_effort: GrokCatalogCapabilityEvidence::from_wire( - wire.supports_reasoning_effort, - ), + reasoning_effort, + reasoning_efforts, + default_reasoning_effort, backend_search: GrokCatalogCapabilityEvidence::from_wire(wire.supports_backend_search), streaming_tool_calls: GrokCatalogCapabilityEvidence::from_wire(wire.stream_tool_calls), api_backend, diff --git a/backend/crates/providers/xai/src/transport/mod.rs b/backend/crates/providers/xai/src/transport/mod.rs index f3f32e3b2..a2a3608d0 100644 --- a/backend/crates/providers/xai/src/transport/mod.rs +++ b/backend/crates/providers/xai/src/transport/mod.rs @@ -21,10 +21,10 @@ pub use catalog::{ GrokBillingRequest, GrokBillingSnapshot, GrokBillingTransport, GrokBillingTransportError, GrokBillingTransportErrorKind, GrokBillingTransportFuture, GrokBillingTransportResponse, GrokCatalogApiBackend, GrokCatalogCapabilities, GrokCatalogCapabilityEvidence, - GrokCatalogLimits, GrokCatalogMetadata, GrokCatalogModel, GrokModelCatalogClient, - GrokModelCatalogError, GrokModelCatalogRequest, GrokModelCatalogSession, - GrokModelCatalogSessionError, GrokModelCatalogSnapshot, GrokModelCatalogTransport, - GrokModelCatalogTransportError, GrokModelCatalogTransportErrorKind, + GrokCatalogLimits, GrokCatalogMetadata, GrokCatalogModel, GrokCatalogReasoningEffort, + GrokModelCatalogClient, GrokModelCatalogError, GrokModelCatalogRequest, + GrokModelCatalogSession, GrokModelCatalogSessionError, GrokModelCatalogSnapshot, + GrokModelCatalogTransport, GrokModelCatalogTransportError, GrokModelCatalogTransportErrorKind, GrokModelCatalogTransportFuture, GrokModelCatalogTransportResponse, MAX_GROK_BILLING_BYTES, MAX_GROK_MODEL_CATALOG_BYTES, parse_grok_billing, parse_grok_model_catalog, }; diff --git a/backend/crates/providers/xai/tests/provider/contract.rs b/backend/crates/providers/xai/tests/provider/contract.rs index f431d53f6..66ddbf1fb 100644 --- a/backend/crates/providers/xai/tests/provider/contract.rs +++ b/backend/crates/providers/xai/tests/provider/contract.rs @@ -1827,7 +1827,7 @@ async fn provider_compiles_realtime_catalog_capabilities() { assert_eq!(presentation.display_name(), Some("Grok 4.5")); assert_eq!( presentation.supported_reasoning_efforts(), - ["low", "medium", "high"] + ["low", "medium", "high", "xhigh"] ); assert_eq!(presentation.context_window_tokens(), Some(1_000_000)); assert!(presentation.agent_tools()); diff --git a/backend/crates/providers/xai/tests/transport/catalog.rs b/backend/crates/providers/xai/tests/transport/catalog.rs index 8ba2ccc6b..561f7999d 100644 --- a/backend/crates/providers/xai/tests/transport/catalog.rs +++ b/backend/crates/providers/xai/tests/transport/catalog.rs @@ -261,6 +261,16 @@ fn official_fixture_should_use_actual_model_and_whitelisted_metadata() { ( model.capabilities().responses_api(), model.capabilities().reasoning_effort(), + model + .capabilities() + .reasoning_efforts() + .iter() + .map(|effort| effort.as_str()) + .collect::>(), + model + .capabilities() + .default_reasoning_effort() + .map(|effort| effort.as_str()), model.capabilities().backend_search(), model.capabilities().streaming_tool_calls(), ), @@ -278,6 +288,8 @@ fn official_fixture_should_use_actual_model_and_whitelisted_metadata() { ( GrokCatalogCapabilityEvidence::DeclaredNative, GrokCatalogCapabilityEvidence::DeclaredNative, + vec!["low", "medium", "high", "xhigh"], + Some("medium"), GrokCatalogCapabilityEvidence::DeclaredNative, GrokCatalogCapabilityEvidence::DeclaredNative, ), @@ -335,6 +347,35 @@ fn missing_capability_fields_should_remain_unknown() { ); } +#[test] +fn reasoning_effort_menu_should_be_capability_evidence_without_legacy_flag() { + let snapshot = parse_grok_model_catalog( + br#"{"object":"list","data":[{"id":"grok-reasoning","reasoningEfforts":[{"value":"xhigh","default":true},"low"]}]}"#, + None, + ) + .expect("reasoning effort menu"); + let capabilities = snapshot.models()[0].capabilities(); + + assert_eq!( + ( + capabilities.reasoning_effort(), + capabilities + .reasoning_efforts() + .iter() + .map(|effort| effort.as_str()) + .collect::>(), + capabilities + .default_reasoning_effort() + .map(|effort| effort.as_str()), + ), + ( + GrokCatalogCapabilityEvidence::DeclaredNative, + vec!["xhigh", "low"], + Some("xhigh"), + ) + ); +} + #[test] fn model_id_should_take_priority_over_catalog_id() { let snapshot = parse_grok_model_catalog( diff --git a/backend/crates/providers/xai/tests/transport/catalog/fixtures/official_grok_models_snapshot.json b/backend/crates/providers/xai/tests/transport/catalog/fixtures/official_grok_models_snapshot.json index 63d8d0c01..fd8111699 100644 --- a/backend/crates/providers/xai/tests/transport/catalog/fixtures/official_grok_models_snapshot.json +++ b/backend/crates/providers/xai/tests/transport/catalog/fixtures/official_grok_models_snapshot.json @@ -12,6 +12,13 @@ "apiBackend": "responses", "supportedInApi": true, "supportsReasoningEffort": true, + "reasoningEffort": "medium", + "reasoningEfforts": [ + "low", + { "value": "medium", "label": "Balanced", "default": true }, + "high", + { "id": "extended", "value": "xhigh", "label": "Extended" } + ], "supportsBackendSearch": true, "streamToolCalls": true, "hidden": false, diff --git a/frontend/src/views/api-keys/components/ApiKeyUseModal.vue b/frontend/src/views/api-keys/components/ApiKeyUseModal.vue index adf120e62..2b0b96e71 100644 --- a/frontend/src/views/api-keys/components/ApiKeyUseModal.vue +++ b/frontend/src/views/api-keys/components/ApiKeyUseModal.vue @@ -35,19 +35,30 @@ const configPath = computed(() => ? '%userprofile%\\.codex\\config.toml' : '~/.codex/config.toml', ) -const authPath = computed(() => - activePlatform.value === 'windows' ? '%userprofile%\\.codex\\auth.json' : '~/.codex/auth.json', +const tokenPath = computed(() => + activePlatform.value === 'windows' + ? '%userprofile%\\.codex\\codex-proxy.token' + : '~/.codex/codex-proxy.token', ) -const codexAuthJson = computed(() => JSON.stringify({ OPENAI_API_KEY: keyValue.value }, null, 2)) const defaultModel = computed(() => props.apiKey?.providerKind?.trim().toLowerCase() === 'xai' ? 'grok-4.5' : 'gpt-5.5', ) +const providerAuthToml = computed(() => + activePlatform.value === 'windows' + ? `[model_providers.OpenAI.auth] +command = "powershell.exe" +args = ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "Get-Content -Raw (Join-Path (Join-Path $env:USERPROFILE '.codex') 'codex-proxy.token')"]` + : `[model_providers.OpenAI.auth] +command = "sh" +args = ["-c", 'cat "$HOME/.codex/codex-proxy.token"']`, +) const codexConfigToml = computed( () => `model_provider = "OpenAI" model = "${defaultModel.value}" review_model = "${defaultModel.value}" model_reasoning_effort = "xhigh" +service_tier = "default" disable_response_storage = true network_access = "enabled" windows_wsl_setup_acknowledged = true @@ -56,7 +67,8 @@ windows_wsl_setup_acknowledged = true name = "OpenAI" base_url = "${props.apiBaseUrl}" wire_api = "responses" -requires_openai_auth = true + +${providerAuthToml.value} [features] goals = true`, @@ -64,7 +76,7 @@ goals = true`, const visibleFiles = computed(() => [ { path: configPath.value, content: codexConfigToml.value }, - { path: authPath.value, content: codexAuthJson.value }, + { path: tokenPath.value, content: keyValue.value }, ])