diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 9a76dbd57..6510febd3 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -855,7 +855,11 @@ jobs: VERTEX_LOCATION: ${{ secrets.VERTEX_LOCATION || 'us-central1' }} STRIX_TARGET_PATH: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && '__PR_SCOPE__' || './' }} STRIX_SOURCE_DIRS: ". backend frontend" - STRIX_REASONING_EFFORT: high + # OpenAI GPT-5.4's chat-completions endpoint rejects reasoning_effort + # when Strix supplies function tools. Keep high effort for providers + # that support it, but send the API-compatible neutral value to direct + # OpenAI scans so the security gate can complete. + STRIX_REASONING_EFFORT: ${{ steps.gate.outputs.provider_mode == 'openai_direct' && 'none' || 'high' }} STRIX_LLM_MAX_RETRIES: 1 STRIX_TRANSIENT_RETRY_PER_MODEL: 2 STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 60 diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index c9aa41545..7091b36cd 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2551,6 +2551,7 @@ run_strix_once() { local rc local llm_api_base_value local child_model + local child_reasoning_effort="${STRIX_REASONING_EFFORT:-high}" local resolved_target_path local timeout_seconds="$STRIX_PROCESS_TIMEOUT_SECONDS" local total_budget_limited_timeout=0 @@ -2574,6 +2575,12 @@ run_strix_once() { return 2 fi child_model="$(child_model_for_api_base "$model" "$llm_api_base_value")" + if is_explicit_openai_model "$model"; then + # GPT-5.4 chat-completions rejects reasoning_effort when Strix + # supplies function tools; apply the override to every direct-OpenAI + # fallback, regardless of the selected primary provider. + child_reasoning_effort="none" + fi if ! resolved_target_path="$(resolve_current_target_path "$TARGET_PATH")"; then return 1 fi @@ -2597,6 +2604,7 @@ run_strix_once() { set -o pipefail set +e STRIX_CHILD_MODEL="$child_model" \ + STRIX_CHILD_REASONING_EFFORT="$child_reasoning_effort" \ STRIX_CHILD_LLM_API_KEY="$child_llm_api_key" \ STRIX_CHILD_LLM_API_BASE="$llm_api_base_value" \ STRIX_CHILD_REPORTS_DIR="$ACTIVE_REPORTS_DIR" \ @@ -2671,7 +2679,6 @@ for key in ( "GEMINI_LOCATION", "LLM_TIMEOUT", "STRIX_MEMORY_COMPRESSOR_TIMEOUT", - "STRIX_REASONING_EFFORT", "STRIX_LLM_MAX_RETRIES", "GOOGLE_CLOUD_PROJECT", "GCP_PROJECT", @@ -2682,6 +2689,11 @@ for key in ( value = os.environ.get(key) if value: child_env[key] = value +child_reasoning_effort = os.environ.get("STRIX_CHILD_REASONING_EFFORT") or os.environ.get( + "STRIX_REASONING_EFFORT" +) +if child_reasoning_effort: + child_env["STRIX_REASONING_EFFORT"] = child_reasoning_effort llm_api_base = os.environ.get("STRIX_CHILD_LLM_API_BASE", "") if llm_api_base: child_env["LLM_API_BASE"] = llm_api_base diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 3d3449dae..0d1318171 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -358,7 +358,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "NVIDIA_NIM_API_KEY is required for Strix NVIDIA NIM scans" "strix workflow fails closed when NVIDIA credentials are absent" assert_file_contains "$workflow_file" 'PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }}' "strix workflow passes provider mode through env" assert_file_not_contains "$workflow_file" '[ "${{ steps.gate.outputs.provider_mode }}" = "openai_direct" ]' "strix workflow does not interpolate provider mode inside shell condition" - assert_file_contains "$workflow_file" "STRIX_REASONING_EFFORT: high" "strix workflow uses high reasoning effort when the selected provider/model supports it" + assert_file_contains "$workflow_file" "STRIX_REASONING_EFFORT: \${{ steps.gate.outputs.provider_mode == 'openai_direct' && 'none' || 'high' }}" "strix workflow disables unsupported OpenAI chat-completions reasoning effort while retaining high effort for other providers" assert_file_contains "$workflow_file" 'trimmed_openai_key="$(printf '"'"'%s'"'"' "$sanitized_openai_key" | sed '"'"'s/^[[:space:]]*//;s/[[:space:]]*$//'"'"')"' "strix workflow trims whitespace-only OpenAI keys before gate validation" assert_file_contains "$workflow_file" 'printf '"'"'%s'"'"' "$trimmed" > "$llm_api_key_file"' "strix workflow writes trimmed provider API keys into the trusted input file" assert_file_contains "$workflow_file" 'STRIX_LLM_DEFAULT_PROVIDER: ${{ steps.gate.outputs.provider_mode == '"'"'vertex_ai'"'"' && '"'"'vertex_ai'"'"' || steps.gate.outputs.provider_mode == '"'"'nvidia_nim'"'"' && '"'"'nvidia_nim'"'"' || '"'"'openai'"'"' }}' "strix workflow selects the correct default provider" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 1e3756169..b9aacd19e 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1981,7 +1981,10 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "falling back to current-head REST check-runs" in workflow strix_workflow = Path(".github/workflows/strix.yml").read_text(encoding="utf-8") - assert "STRIX_REASONING_EFFORT: high" in strix_workflow + assert ( + "STRIX_REASONING_EFFORT: ${{ steps.gate.outputs.provider_mode == 'openai_direct' && 'none' || 'high' }}" + in strix_workflow + ) prompt_template = Path("scripts/ci/opencode_review_prompt_template.md").read_text( encoding="utf-8" @@ -2993,3 +2996,15 @@ def test_r_package_load_deferral_requires_current_head_r_cmd_check(): assert ( "if (!is.na(pkg) && !requireNamespace(pkg, quietly = TRUE))" not in workflow ) + + +def test_strix_direct_openai_fallback_overrides_child_reasoning_effort(): + """Direct OpenAI fallbacks must not inherit an incompatible effort value.""" + quick_gate = Path("scripts/ci/strix_quick_gate.sh").read_text(encoding="utf-8") + + assert 'local child_reasoning_effort="${STRIX_REASONING_EFFORT:-high}"' in quick_gate + assert 'if is_explicit_openai_model "$model"; then' in quick_gate + assert 'child_reasoning_effort="none"' in quick_gate + assert 'STRIX_CHILD_REASONING_EFFORT="$child_reasoning_effort"' in quick_gate + assert 'child_reasoning_effort = os.environ.get("STRIX_CHILD_REASONING_EFFORT") or os.environ.get(' in quick_gate + assert 'child_env["STRIX_REASONING_EFFORT"] = child_reasoning_effort' in quick_gate