diff --git a/docs/doctoring/strix-openai-fallback-api-base-routing.md b/docs/doctoring/strix-openai-fallback-api-base-routing.md index 5e65387c4..189006e1a 100644 --- a/docs/doctoring/strix-openai-fallback-api-base-routing.md +++ b/docs/doctoring/strix-openai-fallback-api-base-routing.md @@ -19,6 +19,18 @@ fallback key file, so every provider chain that ends in `openai-direct/gpt-5.4` (NVIDIA NIM primary, OpenRouter primary, GitHub Models primary) inherits correct routing automatically. +Strix invokes function tools through chat completions. Direct OpenAI rejects +those tools when `reasoning_effort` is non-none, so the gate scopes +`STRIX_REASONING_EFFORT=none` to an explicit direct-OpenAI child using the +native OpenAI endpoint. NVIDIA, other providers, and standalone explicit-model +runs targeting a custom OpenAI-compatible endpoint retain their configured +effort. + +Contextual-orchestrator PR #881 run `32967361853` demonstrated this boundary: +the NVIDIA attempts exhausted with 429/410 provider responses, then the direct +OpenAI fallback reached the correct endpoint but returned HTTP 400 because +function tools were combined with `reasoning_effort=high`. + ## Failure this fixes Required-CI evidence (BandScope PR #1021 strix run 32800796577, 2026-08-25) @@ -71,11 +83,10 @@ Regression evidence proves that: 8. the workflow provisions the override file and passes it into the gate env; 9. the required-workflow smoke contract pins both sides of the wiring; and 10. the stale `gpt-5.6-luna` expectations left behind by the model rename are - aligned with the valid `gpt-5.4` contract in queue-contract tests; and -11. the direct GPT-5.4 fallback clears reasoning effort to `none` because the - pinned Strix scanner uses Chat Completions function tools, a combination - the provider rejects when `reasoning_effort` is present. Other models keep - the workflow's configured `high` effort. + aligned with the valid `gpt-5.4` contract in queue-contract tests; and +11. explicit direct-OpenAI attempts on the native endpoint receive reasoning + effort `none`, while custom compatible endpoints and other providers retain + their configured effort. ## Limitations diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 483149bed..c97a66ef9 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:-}" local resolved_target_path local timeout_seconds="$STRIX_PROCESS_TIMEOUT_SECONDS" local total_budget_limited_timeout=0 @@ -2574,20 +2575,19 @@ run_strix_once() { return 2 fi child_model="$(child_model_for_api_base "$model" "$llm_api_base_value")" + # Strix uses function tools. Direct OpenAI chat-completions rejects those + # tools when reasoning_effort is non-none, so scope the supported value to + # this provider attempt without weakening reasoning on other providers. + if is_explicit_openai_model "$model" && + { [ -z "$llm_api_base_value" ] || [ "${llm_api_base_value%/}" = "https://api.openai.com/v1" ]; }; then + child_reasoning_effort="none" + fi if ! resolved_target_path="$(resolve_current_target_path "$TARGET_PATH")"; then return 1 fi local start_epoch start_epoch="$(date +%s)" local child_llm_api_key="" - local child_reasoning_effort="${STRIX_REASONING_EFFORT:-}" - case "$(normalize_model "$model")" in - openai-direct/gpt-5.4 | openai_direct/gpt-5.4) - # OpenAI rejects function tools plus reasoning_effort for GPT-5.4 on - # Chat Completions, the transport used by the pinned Strix scanner. - child_reasoning_effort="none" - ;; - esac if ! is_vertex_model "$(normalize_model "$model")"; then child_llm_api_key="$LLM_API_KEY" if is_github_models_model "$(normalize_model "$model")" && [ -n "$STRIX_GITHUB_MODELS_KEY" ]; then diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 1fc84469e..f64af2334 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -3332,7 +3332,7 @@ printf '%s\n' "$target_path" >> "${FAKE_STRIX_TARGET_LOG:?}" STRIX_REPORTS_DIR="${STRIX_REPORTS_DIR:-strix_runs}" case "${FAKE_STRIX_SCENARIO:?}" in -success|runtime-env-forwarding|vertex-primary-success-timing-message|direct-openai-gpt-does-not-require-github-models-api-base|pr-executable-integrity-mismatch|pr-executable-group-writable) +success|runtime-env-forwarding|custom-openai-compatible-preserves-effort|vertex-primary-success-timing-message|direct-openai-gpt-does-not-require-github-models-api-base|pr-executable-integrity-mismatch|pr-executable-group-writable) echo "scan ok" exit 0 ;; @@ -3416,7 +3416,7 @@ REPORT ;; openai/gpt-5.4) if [ "${STRIX_REASONING_EFFORT:-}" != "none" ]; then - echo "direct OpenAI GPT-5.4 fallback retained unsupported reasoning effort (${STRIX_REASONING_EFFORT:-})" >&2 + echo "direct OpenAI function-tools fallback requires reasoning effort none" >&2 exit 29 fi if [ "${LLM_API_KEY:-}" != "openai-fallback-token" ]; then @@ -5660,7 +5660,7 @@ PY STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" STRIX_TARGET_PATH="$effective_target_path" ) - if [ "$scenario" = "runtime-env-forwarding" ]; then + if [ "$scenario" = "runtime-env-forwarding" ] || [ "$scenario" = "custom-openai-compatible-preserves-effort" ]; then env_cmd+=( LLM_TIMEOUT="90" STRIX_MEMORY_COMPRESSOR_TIMEOUT="10" @@ -5808,6 +5808,7 @@ PY -u STRIX_GEMINI_FALLBACK_MODELS \ -u STRIX_FALLBACK_MODELS \ -u STRIX_OPENAI_FALLBACK_KEY_FILE \ + -u STRIX_OPENAI_FALLBACK_API_BASE_FILE \ "${env_cmd[@]}" \ bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 ) @@ -5877,6 +5878,12 @@ PY "LLM_TIMEOUT=90;STRIX_MEMORY_COMPRESSOR_TIMEOUT=10;STRIX_REASONING_EFFORT=minimal;STRIX_LLM_MAX_RETRIES=1;GEMINI_LOCATION=GLOBAL;PYTHONWARNINGS=ignore:Pydantic serializer warnings:UserWarning:pydantic.main;NPM_CONFIG_IGNORE_SCRIPTS=true;PNPM_CONFIG_IGNORE_SCRIPTS=true;YARN_ENABLE_SCRIPTS=false;UNRELATED_SECRET=" \ "scenario=$scenario runtime env forwarding" fi + if [ "$scenario" = "custom-openai-compatible-preserves-effort" ]; then + assert_file_contains \ + "$runtime_env_log" \ + "STRIX_REASONING_EFFORT=minimal" \ + "scenario=$scenario custom compatible endpoint effort" + fi if [ "$scenario" = "report-known-internal-warning-sanitized" ]; then assert_file_not_contains \ @@ -6160,6 +6167,18 @@ run_filtered_gate_case_if_requested() { "" \ "github_models/deepseek/deepseek-v3-0324 github_models/deepseek/deepseek-r1-0528" ;; + custom-openai-compatible-preserves-effort) + run_gate_case "custom-openai-compatible-preserves-effort" \ + "openai-direct/gpt-5.4" \ + "" \ + "0" \ + "scan ok" \ + "1" \ + "openai/gpt-5.4" \ + "https://compatible.example/v1" \ + "openai" \ + "https://compatible.example/v1" + ;; nvidia-rate-limit-openai-direct-fallback-clears-api-base) run_gate_case_allow_provider_signal "nvidia-rate-limit-openai-direct-fallback-clears-api-base" \ "nvidia_nim/nvidia/rate-limited-primary" \ @@ -12526,6 +12545,17 @@ run_gate_case "github-models-model-prefix-requires-api-base" \ "openai" \ "" +run_gate_case "custom-openai-compatible-preserves-effort" \ + "openai-direct/gpt-5.4" \ + "" \ + "0" \ + "scan ok" \ + "1" \ + "openai/gpt-5.4" \ + "https://compatible.example/v1" \ + "openai" \ + "https://compatible.example/v1" + run_gate_case "github-models-api-base-rejected-for-direct-openai" \ "openai/o4-mini" \ "" \