From f2a6aab1fa7297c801b9aa324f52d8e979671ca0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:36:28 +0900 Subject: [PATCH 1/4] fix(strix): recognize the hyphenated openai-direct fallback alias STRIX_FALLBACK_MODELS' NVIDIA NIM entry ends in the hyphenated openai-direct/gpt-5.6-luna alias (the workflow's user-facing input spelling, also pinned verbatim by protected main's own trusted strix_required_workflow_smoke.sh, so that exact string cannot change). child_model_for_api_base() only recognized the underscored openai_direct/ form the primary-model case statement produces internally, so the fallback alias passed through unrewritten and reached LiteLLM as an unrecognized provider string. Observed three times in CI: NVIDIA NIM rate-limited the primary and first fallback model, the run advanced to the third fallback, and litellm.BadRequestError: LLM Provider NOT provided ended the scan instead of completing against direct OpenAI. Filed as a standalone fix against main rather than bundled into ContextualWisdomLab/.github#1052 (whose branch already carries this same change): pull_request_target resolves job.workflow_sha to the base branch commit, so any .github PR's own "strix" check always fetches scripts/ci/strix_quick_gate.sh from protected main regardless of what the PR branch itself contains. A PR that touches this file cannot verify its own fix via that check; only a merge to main can. --- CHANGELOG.md | 11 ++++ scripts/ci/strix_quick_gate.sh | 4 +- ...est_strix_nvidia_nim_not_found_fallback.py | 51 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0ef8d44..a20be9e93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,17 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Recognize the hyphenated `openai-direct/` fallback alias (pinned verbatim by + protected main's own trusted `strix_required_workflow_smoke.sh`, so the + `STRIX_FALLBACK_MODELS` string itself cannot change) in + `child_model_for_api_base`, alongside the existing underscored + `openai_direct/` form. Previously the hyphenated alias passed through + unrecognized and unrewritten, so a NIM-exhaustion fallback to + `openai-direct/gpt-5.6-luna` reached LiteLLM as a literal, unrecognized + provider string (`litellm.BadRequestError: LLM Provider NOT provided`) + instead of the intended `openai/gpt-5.6-luna`, observed after NVIDIA NIM + rate-limited both the primary and first fallback model in three + consecutive Strix runs. - Publish only the sanitized cumulative Strix report tree, avoiding a later copy of relative scanner output that could reintroduce known internal warning text into uploaded security evidence. diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 337373001..fadf3f753 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2448,8 +2448,8 @@ child_model_for_api_base() { fi case "$model" in - openai_direct/*) - printf 'openai/%s\n' "${model#openai_direct/}" + openai_direct/* | openai-direct/*) + printf 'openai/%s\n' "${model#*/}" return 0 ;; esac diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index 990269725..1ad0e1af8 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -73,6 +73,34 @@ def _classifies_as_nvidia_not_found(log_text: str) -> bool: return completed.returncode == 0 +def _child_model_for_api_base(model: str, llm_api_base_value: str) -> str: + """Execute the production model-alias normalizer against one input pair.""" + + gate_source = STRIX_GATE.read_text(encoding="utf-8") + function_source = "\n".join( + _function_block(gate_source, name) + for name in ( + "is_github_models_api_base", + "is_github_models_model", + "child_model_for_api_base", + ) + ) + script = "\n".join( + ( + "set -euo pipefail", + function_source, + 'child_model_for_api_base "$1" "$2"', + ) + ) + completed = subprocess.run( + ["bash", "-c", script, "strix-normalizer", model, llm_api_base_value], + check=True, + capture_output=True, + text=True, + ) + return completed.stdout.strip() + + def _workflow_signal_pattern(workflow: str, variable_name: str) -> str: """Extract one single-quoted POSIX ERE assigned in the Strix workflow.""" @@ -213,6 +241,29 @@ def test_workflow_uses_available_free_first_nvidia_plan(self) -> None: )[0] self.assertNotIn(RETIRED_PRIMARY_MODEL, default_gate) + def test_gate_normalizes_hyphenated_openai_direct_fallback_alias(self) -> None: + """Route the NIM-exhaustion fallback alias to a real LiteLLM provider. + + `STRIX_FALLBACK_MODELS`' NVIDIA NIM entry ends in the hyphenated + `openai-direct/gpt-5.6-luna` alias (the workflow's user-facing input + spelling, also pinned verbatim by protected main's own trusted + `strix_required_workflow_smoke.sh`, so this exact string cannot + change). The gate must still resolve it to LiteLLM's `openai/` + provider -- the same target the underscored `openai_direct/` alias + already reaches -- or NVIDIA NIM rate-limiting the primary and first + fallback model leaves the run one hop from + `litellm.BadRequestError: LLM Provider NOT provided`. + """ + + self.assertEqual( + _child_model_for_api_base("openai-direct/gpt-5.6-luna", ""), + "openai/gpt-5.6-luna", + ) + self.assertEqual( + _child_model_for_api_base("openai_direct/gpt-5.6-luna", ""), + "openai/gpt-5.6-luna", + ) + def test_outer_workflow_requires_litellm_context_for_nvidia_404(self) -> None: """Reject provider-like target text in the outer neutralization gate.""" From bad3ebdba60bc8f93d65557562650c3eb4134225 Mon Sep 17 00:00:00 2001 From: seonghobae Date: Mon, 24 Aug 2026 13:35:15 +0900 Subject: [PATCH 2/4] fix(strix): route the openai-direct terminal fallback to OpenAI credentials Production evidence (LineageWeave#551 strix run 32678825167): with provider_mode=nvidia_nim the primary and first fallback both hit NIM 429, then the configured openai-direct/gpt-5.6-luna terminal fallback failed in 3s with litellm BadRequestError 'LLM Provider NOT provided ... You passed model=openai-direct/gpt-5.6-luna'. Two defects: - child_model_for_api_base only translated the underscore alias openai_direct/* while the workflow's fallback matrices spell the model openai-direct/*, so litellm received an unknown provider prefix. - even translated, the child would have called LLM_API_BASE_FILE (the primary provider endpoint) with the primary provider key. Fixes: - accept both spellings in child_model_for_api_base via a shared is_openai_direct_model helper. - mirror the GitHub Models cross-provider pattern: optional STRIX_OPENAI_FALLBACK_KEY_FILE / STRIX_OPENAI_FALLBACK_API_BASE_FILE switch key and endpoint for openai-direct candidates. - strix.yml writes those files from STRIX_OPENAI_API_KEY || OPENAI_API_KEY whenever nvidia_nim or openrouter is primary, and forwards them into the gate run env. test_strix_quick_gate.sh: PASS (full suite). --- .github/workflows/strix.yml | 25 ++++++++++++++ scripts/ci/strix_quick_gate.sh | 61 ++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index b3248d943..67a0567f3 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -696,6 +696,29 @@ jobs: printf '%s' 'https://models.github.ai/inference' > "$github_models_api_base_file" echo "STRIX_GITHUB_MODELS_API_BASE_FILE=$github_models_api_base_file" >> "$GITHUB_ENV" + - name: Prepare direct-OpenAI fallback credentials + # The nvidia_nim, github_models, and openrouter fallback matrices all + # end with openai-direct/gpt-5.6-luna. Without OpenAI credentials those + # candidates previously ran against the primary provider endpoint with + # the wrong key and failed closed on every provider outage. + if: steps.gate.outputs.provider_mode == 'nvidia_nim' || steps.gate.outputs.provider_mode == 'openrouter' + env: + OPENAI_FALLBACK_KEY: ${{ secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} + run: | + umask 077 + sanitized="$(printf '%s' "$OPENAI_FALLBACK_KEY" | tr -d '\r\n')" + trimmed="$(printf '%s' "$sanitized" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + if [ -z "$trimmed" ]; then + echo '::notice::No direct-OpenAI key available; Strix scans run without the openai-direct terminal fallback.' + exit 0 + fi + openai_fallback_key_file="$RUNNER_TEMP/openai_fallback_key.txt" + printf '%s' "$trimmed" > "$openai_fallback_key_file" + echo "STRIX_OPENAI_FALLBACK_KEY_FILE=$openai_fallback_key_file" >> "$GITHUB_ENV" + openai_fallback_api_base_file="$RUNNER_TEMP/openai_fallback_api_base.txt" + printf '%s' 'https://api.openai.com/v1' > "$openai_fallback_api_base_file" + echo "STRIX_OPENAI_FALLBACK_API_BASE_FILE=$openai_fallback_api_base_file" >> "$GITHUB_ENV" + - name: Prepare Vertex AI credentials if: steps.gate.outputs.provider_mode == 'vertex_ai' env: @@ -825,6 +848,8 @@ jobs: STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'openai-direct/gpt-5.6-luna' || steps.gate.outputs.provider_mode == 'openai_direct' && 'openai-direct/gpt-5.6-luna' || steps.gate.outputs.provider_mode == 'openrouter' && 'openai-direct/gpt-5.6-luna' || steps.gate.outputs.provider_mode == 'nvidia_nim' && 'nvidia_nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 openai-direct/gpt-5.6-luna' || '' }} STRIX_GITHUB_MODELS_API_BASE_FILE: ${{ env.STRIX_GITHUB_MODELS_API_BASE_FILE }} STRIX_GITHUB_MODELS_KEY_FILE: ${{ env.STRIX_GITHUB_MODELS_KEY_FILE }} + STRIX_OPENAI_FALLBACK_API_BASE_FILE: ${{ env.STRIX_OPENAI_FALLBACK_API_BASE_FILE }} + STRIX_OPENAI_FALLBACK_KEY_FILE: ${{ env.STRIX_OPENAI_FALLBACK_KEY_FILE }} STRIX_FAIL_ON_PROVIDER_SIGNAL: "1" STRIX_VERTEX_FALLBACK_MODELS: "" NPM_CONFIG_IGNORE_SCRIPTS: "true" diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index fadf3f753..3ab8c44f9 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -380,6 +380,38 @@ if [ -n "$STRIX_GITHUB_MODELS_KEY_FILE" ]; then fi fi +# Optional cross-provider fallback credentials for openai-direct/ (and the +# underscore alias openai_direct/) fallback models. When the primary provider +# is NVIDIA NIM, GitHub Models, or OpenRouter, those fallbacks must +# authenticate against the direct OpenAI API with an OpenAI key instead of +# reusing LLM_API_KEY_FILE / LLM_API_BASE_FILE. Both files are optional; +# without them openai-direct fallbacks keep requiring LLM_API_BASE_FILE. +STRIX_OPENAI_FALLBACK_KEY_FILE="${STRIX_OPENAI_FALLBACK_KEY_FILE:-}" +if [ -n "$STRIX_OPENAI_FALLBACK_KEY_FILE" ] && { [ ! -f "$STRIX_OPENAI_FALLBACK_KEY_FILE" ] || [ -L "$STRIX_OPENAI_FALLBACK_KEY_FILE" ]; }; then + echo "ERROR: STRIX_OPENAI_FALLBACK_KEY_FILE must reference a regular file containing the API key." >&2 + exit 2 +fi +if [ -n "$STRIX_OPENAI_FALLBACK_KEY_FILE" ] && ! STRIX_OPENAI_FALLBACK_KEY_FILE="$(resolve_trusted_input_file "STRIX_OPENAI_FALLBACK_KEY_FILE" "$STRIX_OPENAI_FALLBACK_KEY_FILE")"; then + exit 2 +fi +STRIX_OPENAI_FALLBACK_KEY="" +if [ -n "$STRIX_OPENAI_FALLBACK_KEY_FILE" ]; then + STRIX_OPENAI_FALLBACK_KEY="$(trim_whitespace "$(cat -- "$STRIX_OPENAI_FALLBACK_KEY_FILE")")" + if [ -z "$STRIX_OPENAI_FALLBACK_KEY" ]; then + echo "ERROR: STRIX_OPENAI_FALLBACK_KEY_FILE must contain a non-empty API key." >&2 + exit 2 + fi +fi + +STRIX_OPENAI_FALLBACK_API_BASE_FILE="${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" +if [ -n "$STRIX_OPENAI_FALLBACK_API_BASE_FILE" ] && { [ ! -f "$STRIX_OPENAI_FALLBACK_API_BASE_FILE" ] || [ -L "$STRIX_OPENAI_FALLBACK_API_BASE_FILE" ]; }; then + echo "ERROR: STRIX_OPENAI_FALLBACK_API_BASE_FILE must reference a regular file containing the API base URL." >&2 + exit 2 +fi +if [ -n "$STRIX_OPENAI_FALLBACK_API_BASE_FILE" ] && ! STRIX_OPENAI_FALLBACK_API_BASE_FILE="$(resolve_trusted_input_file "STRIX_OPENAI_FALLBACK_API_BASE_FILE" "$STRIX_OPENAI_FALLBACK_API_BASE_FILE")"; then + exit 2 +fi + require_non_negative_integer() { local value="$1" local label="$2" @@ -755,6 +787,20 @@ is_github_models_model() { esac } +## True when the model routes to the direct OpenAI API through one of the two +## accepted spellings. The workflow's fallback matrices use the hyphen alias +## while some self-tests and callers pass the underscore form. +is_openai_direct_model() { + case "$1" in + openai-direct/* | openai_direct/*) + return 0 + ;; + *) + return 1 + ;; + esac +} + is_github_models_api_compatible_model() { case "$1" in openai/openai/* | github_models/* | \ @@ -2383,7 +2429,13 @@ resolved_llm_api_base_for_model() { local api_base_file="$LLM_API_BASE_FILE" local api_base_file_name="LLM_API_BASE_FILE" - if is_github_models_model "$model" && [ -n "${STRIX_GITHUB_MODELS_API_BASE_FILE:-}" ]; then + if is_openai_direct_model "$model" && [ -n "${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" ]; then + # Cross-provider fallback: openai-direct/* (and openai_direct/*) + # candidates must reach the direct OpenAI API even when the primary + # provider selected a different LLM_API_BASE_FILE endpoint. + api_base_file="$STRIX_OPENAI_FALLBACK_API_BASE_FILE" + api_base_file_name="STRIX_OPENAI_FALLBACK_API_BASE_FILE" + elif is_github_models_model "$model" && [ -n "${STRIX_GITHUB_MODELS_API_BASE_FILE:-}" ]; then # Cross-provider fallback: when the active primary provider uses a # different API base (for example OpenRouter), github_models/* fallback # attempts must still route through the GitHub Models inference endpoint. @@ -2499,7 +2551,12 @@ run_strix_once() { local child_llm_api_key="" 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 + if is_openai_direct_model "$model" && [ -n "$STRIX_OPENAI_FALLBACK_KEY" ]; then + # Cross-provider fallback: openai-direct/* (and openai_direct/*) + # models authenticate with the direct-OpenAI key, not the primary + # provider key in LLM_API_KEY_FILE. + child_llm_api_key="$STRIX_OPENAI_FALLBACK_KEY" + elif is_github_models_model "$(normalize_model "$model")" && [ -n "$STRIX_GITHUB_MODELS_KEY" ]; then # Cross-provider fallback: github_models/* models authenticate # with the GitHub Models token, not the direct-OpenAI key. child_llm_api_key="$STRIX_GITHUB_MODELS_KEY" From 8f3ee4af5fd281e3a74281be627efb02070242ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:28:48 +0900 Subject: [PATCH 3/4] fix(strix): prepare OpenAI fallback for GitHub Models --- .github/workflows/strix.yml | 2 +- tests/test_strix_nvidia_nim_not_found_fallback.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 67a0567f3..3e4a428f4 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -701,7 +701,7 @@ jobs: # end with openai-direct/gpt-5.6-luna. Without OpenAI credentials those # candidates previously ran against the primary provider endpoint with # the wrong key and failed closed on every provider outage. - if: steps.gate.outputs.provider_mode == 'nvidia_nim' || steps.gate.outputs.provider_mode == 'openrouter' + if: steps.gate.outputs.provider_mode == 'nvidia_nim' || steps.gate.outputs.provider_mode == 'openrouter' || steps.gate.outputs.provider_mode == 'github_models' env: OPENAI_FALLBACK_KEY: ${{ secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} run: | diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index 1ad0e1af8..45f899412 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -233,6 +233,12 @@ def test_workflow_uses_available_free_first_nvidia_plan(self) -> None: f"'{FREE_NVIDIA_FALLBACK} openai-direct/gpt-5.6-luna'", workflow, ) + self.assertIn( + "steps.gate.outputs.provider_mode == 'nvidia_nim' || " + "steps.gate.outputs.provider_mode == 'openrouter' || " + "steps.gate.outputs.provider_mode == 'github_models'", + workflow, + ) default_gate = workflow.split("- name: Gate Strix secrets", maxsplit=1)[1] default_gate = default_gate.split( From 370250c57fa81c632b3c64d6cd4d4c0eb1179cd6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:30:08 +0900 Subject: [PATCH 4/4] fix(strix): prepare OpenAI fallback for GitHub Models --- scripts/ci/test_strix_quick_gate.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index bf0a8693e..644217600 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -365,6 +365,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'nvidia_nim' && 'nvidia_nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 openai-direct/gpt-5.6-luna'" "strix workflow gives NVIDIA NIM scans contracted fallbacks" assert_file_not_contains "$workflow_file" "STRIX_FALLBACK_MODELS: \${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/openai/o3" "strix workflow fallback list must not depend on GitHub Models, which is in platform-wide retirement" assert_file_contains "$workflow_file" "Prepare GitHub Models fallback credentials" "strix workflow provisions GitHub Models fallback credentials for direct-OpenAI scans" + assert_file_contains "$workflow_file" "if: steps.gate.outputs.provider_mode == 'nvidia_nim' || steps.gate.outputs.provider_mode == 'openrouter' || steps.gate.outputs.provider_mode == 'github_models'" "strix workflow provisions direct-OpenAI fallback credentials for GitHub Models scans" assert_file_contains "$GATE_SCRIPT" "STRIX_GITHUB_MODELS_KEY_FILE" "strix gate reads the optional GitHub Models fallback key file" assert_file_contains "$GATE_SCRIPT" "STRIX_GITHUB_MODELS_API_BASE_FILE" "strix gate routes github_models fallback models through the GitHub Models endpoint" assert_file_not_contains "$workflow_file" 'github_models/deepseek/deepseek-r1-0528 | github_models/deepseek/deepseek-v3-0324)' "strix workflow keeps DeepSeek GitHub Models restricted to fallback-only routing"