From 5ecbebb0d4bee769826302c24ac00a00c799521b Mon Sep 17 00:00:00 2001 From: seonghobae Date: Mon, 24 Aug 2026 18:56:58 +0900 Subject: [PATCH 01/11] fix(strix): route openai-direct fallback to the OpenAI API base 0c6b9a64 mapped the openai-direct prefix and routed the OpenAI key, but the child scan still read LLM_API_BASE_FILE -- the primary provider's endpoint. Observed on LineageWeave#570 (run 32701426812): after both NVIDIA models 429'd, the openai-direct/gpt-5.6-luna fallback reached integrate.api.nvidia.com with an unknown model path and died in 4s with '404 page not found'. When the candidate is explicit-openai and STRIX_OPENAI_FALLBACK_API_BASE_FILE is configured, select that file as the api-base source, exactly mirroring the existing GitHub Models cross-provider routing. Key routing was already correct. --- scripts/ci/strix_quick_gate.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 36ec3e5f8..fa676a0d5 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2417,7 +2417,14 @@ 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_explicit_openai_model "$model" && [ -n "${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" ]; then + # Cross-provider fallback: openai-direct/* candidates must reach the + # direct OpenAI API even when the primary provider selected a + # different LLM_API_BASE_FILE endpoint (e.g. NVIDIA NIM). Without + # this the fallback hits the primary gateway and 404s. + 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. From 84eb37943d2b8ad57fd405c858721f71e3144ca6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 19:15:47 +0900 Subject: [PATCH 02/11] fix(strix): wire direct OpenAI fallback endpoint --- .github/workflows/strix.yml | 4 ++++ scripts/ci/test_strix_quick_gate.sh | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index c8c054e42..8eb5be301 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -706,6 +706,9 @@ jobs: openai_fallback_key_file="$RUNNER_TEMP/openai_fallback_key.txt" printf '%s' "$openai_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" fi - name: Prepare Vertex AI credentials @@ -838,6 +841,7 @@ jobs: 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_KEY_FILE: ${{ env.STRIX_OPENAI_FALLBACK_KEY_FILE }} + STRIX_OPENAI_FALLBACK_API_BASE_FILE: ${{ env.STRIX_OPENAI_FALLBACK_API_BASE_FILE }} STRIX_FAIL_ON_PROVIDER_SIGNAL: "1" STRIX_VERTEX_FALLBACK_MODELS: "" NPM_CONFIG_IGNORE_SCRIPTS: "true" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 945eb3fb3..cc601e5dc 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -365,6 +365,8 @@ 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" "STRIX_OPENAI_FALLBACK_API_BASE_FILE" "strix workflow routes direct-OpenAI fallbacks through a trusted API base file" + assert_file_contains "$workflow_file" "https://api.openai.com/v1" "strix workflow uses the OpenAI platform endpoint for direct fallbacks" 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" From 689212bebd5e84d67f263ea04648df516b238507 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 15:20:26 +0900 Subject: [PATCH 03/11] test(strix): pin direct-OpenAI fallback base routing and default-endpoint contract Adds a regression contract for the openai-direct fallback API-base fix: override file routes to https://api.openai.com/v1, absence resolves no override (litellm default endpoint) so explicit OpenAI models can never inherit a foreign primary gateway, NVIDIA/GitHub Models primaries keep their bases, and non-https overrides fail configuration. Also records the doctoring evidence and CHANGELOG entry for the routing fix. --- CHANGELOG.md | 34 +++ .../strix-openai-fallback-api-base-routing.md | 74 ++++++ scripts/ci/strix_quick_gate.sh | 7 + tests/test_strix_openai_fallback_api_base.py | 211 ++++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 docs/doctoring/strix-openai-fallback-api-base-routing.md create mode 100644 tests/test_strix_openai_fallback_api_base.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1630c32d4..00c525c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,40 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Route Strix cross-provider fallbacks to explicit direct-OpenAI models + (`openai-direct/...`) through the OpenAI inference endpoint instead of + inheriting the primary provider's `LLM_API_BASE`: the workflow now + provisions `STRIX_OPENAI_FALLBACK_API_BASE_FILE` + (`https://api.openai.com/v1`), the gate prefers it for explicit OpenAI + fallback models and otherwise resolves no override so litellm defaults to + OpenAI, and a non-https override fails configuration. This removes the + NVIDIA-NIM-edge `404 page not found` that made the contracted final + fallback unreachable after NIM exhaustion. +- Align stale `gpt-5.6-luna` test expectations with the valid `gpt-5.4` + contract left behind by the earlier model rename. + +- Honor each trusted base project's exact, integrity-bearing pnpm + `packageManager` specification in OpenCode coverage images through the pinned + Node distribution's Corepack runtime, instead of admitting the specification + during materialization and then rejecting every version except pnpm 11.5.3; + route generic coverage and docstring package scripts through the same + Corepack boundary instead of invoking a removed bare `pnpm` binary. +- Keep `--trust-lockfile` only for pnpm 11.3 and newer + (`trustLockfile` landed in pnpm 11.3). pnpm 9, 10, and 11.0–11.2 reject + that flag and previously failed LineageWeave JavaScript coverage before + tests could run. Jest test scripts still receive `--coverage` because Jest + documents a native coverage flag. +- Run declared JavaScript test scripts without synthesizing `--coverage` when + the package does not declare a compatible coverage command, but keep the + coverage result failed until the repository adds a lock-pinned provider and + owned coverage command. A generic `c8`, `nyc`, or Istanbul dependency no + longer makes an unrelated test runner receive an unsupported flag. +- Fix OpenCode coverage evidence for exact-base, organization-owned Python VCS + dependencies without weakening registry hashes or the networkless PR sandbox, + reject namespace, ambiguous, linked, native-extension, and installed-metadata + layouts, and make exact roots readable by the unprivileged coverage user. + + - Honor each trusted base project's exact, integrity-bearing pnpm `packageManager` specification in OpenCode coverage images through the pinned diff --git a/docs/doctoring/strix-openai-fallback-api-base-routing.md b/docs/doctoring/strix-openai-fallback-api-base-routing.md new file mode 100644 index 000000000..4f7112303 --- /dev/null +++ b/docs/doctoring/strix-openai-fallback-api-base-routing.md @@ -0,0 +1,74 @@ +# Strix direct-OpenAI fallback API-base routing: evidence and design record + +## Decision + +A Strix cross-provider fallback to an explicit direct-OpenAI model +(`openai-direct/...` or `openai_direct/...`) must route through the OpenAI +inference endpoint, never through the primary provider's `LLM_API_BASE`. The +gate now prefers an explicit `STRIX_OPENAI_FALLBACK_API_BASE_FILE` for such +models and, when the override is absent, resolves no API-base override so +litellm uses its default `https://api.openai.com/v1` endpoint. + +The central workflow writes `https://api.openai.com/v1` into +`$RUNNER_TEMP/openai_fallback_api_base.txt` and exports +`STRIX_OPENAI_FALLBACK_API_BASE_FILE` whenever it publishes the OpenAI +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. + +## Failure this fixes + +Required-CI evidence (BandScope PR #1021 strix run 32800796577, 2026-08-25) +showed the NVIDIA NIM primary and first fallback exhausting provider +availability, then the contracted final fallback `openai-direct/gpt-5.4` +failing with a plain-text gateway error: + +``` +LLM CONNECTION FAILED +Could not establish connection to the language model. +Error: 404 page not found +``` + +Root cause: with `provider_mode=nvidia_nim`, the workflow sets +`LLM_API_BASE_FILE=https://integrate.api.nvidia.com/v1`. The gate reused that +base for the openai-direct fallback child, so litellm sent OpenAI requests to +the NVIDIA NIM edge, whose Go gateway answered `404 page not found`. The +fallback key was already routed correctly (`STRIX_OPENAI_FALLBACK_KEY_FILE`); +only the base URL leaked from the primary provider. Because no vulnerability +report artifact was produced, the gate failed closed — correct policy on an +incomplete scan, but caused by routing rather than by any repository finding. + +## Trust boundary + +The override is a runner-provisioned regular file under `$RUNNER_TEMP`, +resolved through the same `resolve_trusted_input_file` boundary as the other +API-base files: it must be a regular non-symlink file inside the trusted input +root, must trim to a single `https://` URL, and must not contain whitespace or +control characters. Absent or empty overrides fall back to litellm's default +endpoint instead of failing configuration, preserving standalone local gate +runs where no workflow provisioning exists. + +## Verification contract + +Regression evidence proves that: + +1. with a NVIDIA NIM primary base configured, `openai-direct/gpt-5.4` + resolves through the explicit OpenAI fallback base when provided; +2. without an override, the resolver returns no base so litellm defaults to + `https://api.openai.com/v1`; +3. NVIDIA NIM primary attempts keep resolving through the NIM edge; +4. `github_models/*` fallbacks keep their dedicated GitHub Models endpoint; +5. a non-https override fails configuration (exit 2) instead of scanning; +6. the workflow provisions the override file and passes it into the gate env; +7. the required-workflow smoke contract pins both sides of the wiring; and +8. 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. + +## Limitations + +This change restores reachability of the final fallback; it does not create +OpenAI quota. If the OpenAI key is absent or exhausted after NIM exhaustion, +the gate still fails closed as provider-unavailable — by design, because no +complete authoritative scan exists. Hosted model catalogs may also change +independently of this repository; model-name updates remain manual contract +changes reviewed through CI. diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index fa676a0d5..a7493192e 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2415,6 +2415,13 @@ resolved_llm_api_base_for_model() { return 0 fi + if is_explicit_openai_model "$model" && [ -z "${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" ]; then + # Without workflow provisioning, an explicit direct-OpenAI model must + # still not inherit a foreign primary base: resolve no override so + # litellm defaults to https://api.openai.com/v1. + return 0 + fi + local api_base_file="$LLM_API_BASE_FILE" local api_base_file_name="LLM_API_BASE_FILE" if is_explicit_openai_model "$model" && [ -n "${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" ]; then diff --git a/tests/test_strix_openai_fallback_api_base.py b/tests/test_strix_openai_fallback_api_base.py new file mode 100644 index 000000000..f2c4237a7 --- /dev/null +++ b/tests/test_strix_openai_fallback_api_base.py @@ -0,0 +1,211 @@ +"""Regression contract for direct-OpenAI fallback API-base routing. + +When the Strix primary provider is NVIDIA NIM (or OpenRouter / GitHub Models), +the workflow's ``LLM_API_BASE_FILE`` points at that provider's endpoint. A +cross-provider fallback to ``openai-direct/gpt-5.4`` must never inherit that +base: routing an OpenAI model through the NVIDIA NIM edge yields a plain-text +gateway 404 ("404 page not found") instead of OpenAI responses, so the final +contracted fallback could never complete a scan. + +The gate must therefore prefer an explicit +``STRIX_OPENAI_FALLBACK_API_BASE_FILE`` for explicit direct-OpenAI models, and +fall back to litellm's default OpenAI endpoint when the override is absent. +""" + +from __future__ import annotations + +import re +import subprocess +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +STRIX_GATE = REPOSITORY_ROOT / "scripts" / "ci" / "strix_quick_gate.sh" +STRIX_MODEL_UTILS = REPOSITORY_ROOT / "scripts" / "ci" / "strix_model_utils.sh" +STRIX_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "strix.yml" +OPENAI_FALLBACK_BASE = "https://api.openai.com/v1" + + +def _function_block(source: str, function_name: str) -> str: + """Return one top-level Bash function, including its closing brace.""" + + match = re.search( + rf"(?ms)^{re.escape(function_name)}\(\) \{{\n.*?^\}}\n", + source, + ) + if match is None: + raise AssertionError(f"missing Bash function: {function_name}") + return match.group(0) + + +def _resolver_helpers() -> list[str]: + """Collect every helper the production API-base resolver depends on.""" + + gate_source = STRIX_GATE.read_text(encoding="utf-8") + utils_source = STRIX_MODEL_UTILS.read_text(encoding="utf-8") + helper_names = ( + ("is_vertex_model", gate_source), + ("is_github_models_model", gate_source), + ("is_github_models_api_base", gate_source), + ("is_github_models_api_compatible_model", gate_source), + ("is_explicit_openai_model", gate_source), + ("resolve_trusted_input_file", gate_source), + ("trim_whitespace", utils_source), + ) + helpers: list[str] = [] + for name, source in helper_names: + try: + helpers.append(_function_block(source, name)) + except AssertionError as exc: # pragma: no cover - shape drift guard + raise AssertionError(f"resolver helper missing: {name}") from exc + return helpers + + +def _resolve_api_base(env: dict[str, str], model: str) -> tuple[int, str]: + """Execute the production API-base resolver for one model and env.""" + + gate_source = STRIX_GATE.read_text(encoding="utf-8") + resolver_source = _function_block(gate_source, "resolved_llm_api_base_for_model") + helper_sources = _resolver_helpers() + with tempfile.TemporaryDirectory(prefix="strix-openai-fallback-base-") as temp_dir: + base_path = Path(temp_dir) / "primary_base.txt" + if "LLM_API_BASE_FILE" in env: + base_path.write_text(env["LLM_API_BASE_FILE"], encoding="utf-8") + env = {**env, "LLM_API_BASE_FILE": str(base_path)} + fallback_path = Path(temp_dir) / "openai_fallback_api_base.txt" + if "STRIX_OPENAI_FALLBACK_API_BASE_FILE" in env: + fallback_path.write_text( + env["STRIX_OPENAI_FALLBACK_API_BASE_FILE"], + encoding="utf-8", + ) + env = { + **env, + "STRIX_OPENAI_FALLBACK_API_BASE_FILE": str(fallback_path), + } + github_models_path = Path(temp_dir) / "github_models_api_base.txt" + if "STRIX_GITHUB_MODELS_API_BASE_FILE" in env: + github_models_path.write_text( + env["STRIX_GITHUB_MODELS_API_BASE_FILE"], + encoding="utf-8", + ) + env = { + **env, + "STRIX_GITHUB_MODELS_API_BASE_FILE": str(github_models_path), + } + script_lines = [ + "set -euo pipefail", + f'STRIX_INPUT_FILE_ROOT="{temp_dir}"', + *helper_sources, + resolver_source, + ] + command_env = { + key: value + for key, value in env.items() + if key in { + "LLM_API_BASE_FILE", + "STRIX_GITHUB_MODELS_API_BASE_FILE", + "STRIX_OPENAI_FALLBACK_API_BASE_FILE", + } + } + completed = subprocess.run( + [ + "bash", + "-c", + "\n".join([*script_lines, 'resolved_llm_api_base_for_model "$1"']), + "strix-resolver", + model, + ], + check=False, + capture_output=True, + text=True, + env={ + "PATH": "/usr/bin:/bin:/usr/local/bin", + "HOME": temp_dir, + **command_env, + }, + ) + return completed.returncode, completed.stdout.strip() + + +class ExplicitOpenAIFallbackRouting(unittest.TestCase): + """Direct-OpenAI fallbacks must not inherit the primary provider base.""" + + def test_nvidia_primary_with_override_routes_to_openai(self) -> None: + """openai-direct/gpt-5.4 uses the explicit OpenAI API base file.""" + + rc, api_base = _resolve_api_base( + { + "LLM_API_BASE_FILE": "https://integrate.api.nvidia.com/v1", + "STRIX_OPENAI_FALLBACK_API_BASE_FILE": OPENAI_FALLBACK_BASE, + }, + "openai-direct/gpt-5.4", + ) + self.assertEqual(rc, 0) + self.assertEqual(api_base, OPENAI_FALLBACK_BASE) + + def test_nvidia_primary_without_override_uses_default_openai(self) -> None: + """Without an override the resolver returns no base (litellm default).""" + + rc, api_base = _resolve_api_base( + {"LLM_API_BASE_FILE": "https://integrate.api.nvidia.com/v1"}, + "openai-direct/gpt-5.4", + ) + self.assertEqual(rc, 0) + self.assertEqual(api_base, "") + + def test_primary_provider_models_keep_their_base(self) -> None: + """NVIDIA NIM primary attempts still resolve through the NIM edge.""" + + rc, api_base = _resolve_api_base( + {"LLM_API_BASE_FILE": "https://integrate.api.nvidia.com/v1"}, + "nvidia_nim/nvidia/nemotron-3-super-120b-a12b", + ) + self.assertEqual(rc, 0) + self.assertEqual(api_base, "https://integrate.api.nvidia.com/v1") + + def test_github_models_fallback_keeps_github_models_base(self) -> None: + """github_models/* fallbacks keep their dedicated inference endpoint.""" + + rc, api_base = _resolve_api_base( + { + "LLM_API_BASE_FILE": "https://integrate.api.nvidia.com/v1", + "STRIX_GITHUB_MODELS_API_BASE_FILE": "https://models.github.ai/inference", + }, + "github_models/openai/gpt-5.4", + ) + self.assertEqual(rc, 0) + self.assertEqual(api_base, "https://models.github.ai/inference") + + def test_invalid_https_override_is_configuration_failure(self) -> None: + """A non-https override fails configuration instead of scanning.""" + + rc, _ = _resolve_api_base( + { + "LLM_API_BASE_FILE": "https://integrate.api.nvidia.com/v1", + "STRIX_OPENAI_FALLBACK_API_BASE_FILE": "http://api.example.com/v1", + }, + "openai-direct/gpt-5.4", + ) + self.assertEqual(rc, 2) + + +class WorkflowProvisionsFallbackBase(unittest.TestCase): + """The workflow must publish and pass the explicit OpenAI fallback base.""" + + def test_workflow_writes_openai_fallback_api_base_file(self) -> None: + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + self.assertIn("STRIX_OPENAI_FALLBACK_API_BASE_FILE=", workflow) + self.assertIn(OPENAI_FALLBACK_BASE, workflow) + + def test_workflow_passes_override_into_gate_environment(self) -> None: + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + self.assertIn( + "STRIX_OPENAI_FALLBACK_API_BASE_FILE: ${{ env.STRIX_OPENAI_FALLBACK_API_BASE_FILE }}", + workflow, + ) + + +if __name__ == "__main__": + unittest.main() From 7c596f38eb2630328f0cee7fd050a5219408820e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 15:49:06 +0900 Subject: [PATCH 04/11] test(strix): align remaining gpt-5.6-luna expectations with the gpt-5.4 contract The model rename in a724582 missed two test expectations that still asserted the nonexistent gpt-5.6-luna default and fallback names; align them with the shipped gpt-5.4 contract so required CI passes. --- tests/test_required_workflow_queue_contract.py | 2 +- tests/test_strix_nvidia_nim_not_found_fallback.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index e58f5e6c0..1d79f1daa 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -505,7 +505,7 @@ def test_nvidia_nim_defaults_preserve_existing_fallbacks_without_secret( assert strix.returncode == 0, strix.stderr assert { "provider_mode=openai_direct", - "strix_model=gpt-5.6-luna", + "strix_model=gpt-5.4", } <= set(strix_output.read_text().splitlines()) assert ( "STRIX_MODEL: ${{ steps.gate.outputs.strix_model }}" diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index 990269725..17f0e9a30 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -192,7 +192,7 @@ def test_workflow_uses_available_free_first_nvidia_plan(self) -> None: workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") default_expression = ( "steps.target_visibility.outputs.is_private == 'false' && " - f"'{DEFAULT_NVIDIA_MODEL}' || 'gpt-5.6-luna'" + f"'{DEFAULT_NVIDIA_MODEL}' || 'gpt-5.4'" ) self.assertIn(default_expression, workflow) self.assertIn( @@ -202,7 +202,7 @@ def test_workflow_uses_available_free_first_nvidia_plan(self) -> None: ) self.assertIn( "steps.gate.outputs.provider_mode == 'nvidia_nim' && " - f"'{FREE_NVIDIA_FALLBACK} openai-direct/gpt-5.6-luna'", + f"'{FREE_NVIDIA_FALLBACK} openai-direct/gpt-5.4'", workflow, ) From 2d982374e961ec76c93f07e233655935effbb00d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 16:37:38 +0900 Subject: [PATCH 05/11] fix(opencode-review): complete gpt-5.6-luna to gpt-5.4 rename across dispatch chain The a724582 rename updated strix.yml and the smoke contract but left the opencode-review-dispatch workflow, its agent-contract tests, the failed-check fallback-findings marker, and the pinned review-dispatch blob SHAs asserting the retired model name, so required CI failed with three missing-string markers. Align every remaining reference with the shipped gpt-5.4 direct-OpenAI contract and refresh the paired blob SHAs. --- .github/workflows/opencode-review-dispatch.yml | 6 +++--- .../ci/emit_opencode_failed_check_fallback_findings.sh | 2 +- tests/test_opencode_agent_contract.py | 8 ++++---- tests/test_pr_review_autofix_nvidia_nim_contract.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index dd65d90e1..33a4fd08e 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -4058,8 +4058,8 @@ jobs: "apiKey": "{env:OPENAI_API_KEY}" }, "models": { - "gpt-5.6-luna": { - "name": "OpenAI GPT-5.6 Luna (direct)", + "gpt-5.4": { + "name": "OpenAI GPT-5.4 (direct)", "tool_call": true, "reasoning": true, "options": { @@ -4481,7 +4481,7 @@ jobs: # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free opencode-free/hy3-free opencode-free/minimax-m3-free opencode-free/glm-5-free opencode-free/kimi-k2.5-free opencode-free/qwen3.6-plus-free ' || '' }}opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free opencode-free/hy3-free opencode-free/minimax-m3-free opencode-free/glm-5-free opencode-free/kimi-k2.5-free opencode-free/qwen3.6-plus-free ' || '' }}opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.4 openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. diff --git a/scripts/ci/emit_opencode_failed_check_fallback_findings.sh b/scripts/ci/emit_opencode_failed_check_fallback_findings.sh index ccd35a273..b8a755e6e 100755 --- a/scripts/ci/emit_opencode_failed_check_fallback_findings.sh +++ b/scripts/ci/emit_opencode_failed_check_fallback_findings.sh @@ -956,7 +956,7 @@ extract_strix_failed_check_block "$EVIDENCE_FILE" "$strix_evidence_file" emit_known_missing_string_finding \ "$EVIDENCE_FILE" \ - "steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna'" \ + "steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.4'" \ "Strix public scans must default to NVIDIA NIM while private scans retain the contracted provider" \ ".github/workflows/strix.yml" \ "scripts/ci/test_strix_quick_gate.sh" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 8ee6e86fc..2360fdb26 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -186,7 +186,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ["opencode-free", "qwen3.6-plus-free"], ["opencode", "gpt-5.6-terra"], ["github-models", "deepseek/deepseek-v3-0324"], - ["openai", "gpt-5.6-luna"], + ["openai", "gpt-5.4"], ["openrouter", "deepseek/deepseek-v3.2"], ["openrouter", "qwen/qwen3-coder"], ["github-models", "openai/gpt-4.1"], @@ -197,7 +197,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ["github-models", "deepseek/deepseek-r1"], ] assert zen_models == ["gpt-5.6-terra"] - assert direct_openai_models == ["gpt-5.6-luna"] + assert direct_openai_models == ["gpt-5.4"] assert openrouter_models == [ "deepseek/deepseek-v3.2", "qwen/qwen3-coder", @@ -1740,7 +1740,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert ( "opencode/gpt-5.6-terra " "github-models/deepseek/deepseek-v3-0324 " - "openai/gpt-5.6-luna " + "openai/gpt-5.4 " "openrouter/deepseek/deepseek-v3.2 " "openrouter/qwen/qwen3-coder " "github-models/openai/gpt-4.1 " @@ -1887,7 +1887,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): ) assert ( "github-models/deepseek/deepseek-v3-0324 " - "openai/gpt-5.6-luna " + "openai/gpt-5.4 " "openrouter/deepseek/deepseek-v3.2 " "openrouter/qwen/qwen3-coder " "github-models/openai/gpt-4.1 " diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 799b9e9fb..3b6c3a054 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "dd65d90e10e5040562b501ade1a40f89572f0984" +REVIEW_DISPATCH_BLOB_SHA = "33a4fd08e93e653e715b380ad9f9523e11d231e6" def _workflow_text(path: Path) -> str: From 9288031cb4b9f5959b356c4f7407212595a271ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 18:50:02 +0900 Subject: [PATCH 06/11] fix(strix): provision OpenAI fallback credentials for github_models primary; dedupe CHANGELOG - CodeRabbit major: the github_models primary's STRIX_FALLBACK_MODELS chain ends in openai-direct/gpt-5.4, but 'Prepare GitHub Models fallback credentials' excluded the github_models mode, so that fallback could not authenticate or route after primary exhaustion. Include github_models in the provisioning step. - Devin bug: remove the four accidentally duplicated [Unreleased] bullets from CHANGELOG.md, keeping a single copy before '### Added'. - Add markdownlint MD040 language to the doctoring evidence fence. --- .github/workflows/strix.yml | 5 ++- CHANGELOG.md | 44 ------------------- .../strix-openai-fallback-api-base-routing.md | 2 +- 3 files changed, 5 insertions(+), 46 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 6ba4fb4a8..bb656dc56 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -674,7 +674,10 @@ jobs: echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV" - name: Prepare GitHub Models fallback credentials - if: steps.gate.outputs.provider_mode == 'openai_direct' || steps.gate.outputs.provider_mode == 'openrouter' || steps.gate.outputs.provider_mode == 'nvidia_nim' + # github_models is included because its STRIX_FALLBACK_MODELS chain + # ends in openai-direct/gpt-5.4, which needs the direct-OpenAI key and + # API base to authenticate and route after the primary is exhausted. + if: steps.gate.outputs.provider_mode == 'openai_direct' || steps.gate.outputs.provider_mode == 'openrouter' || steps.gate.outputs.provider_mode == 'nvidia_nim' || steps.gate.outputs.provider_mode == 'github_models' env: GITHUB_MODELS_FALLBACK_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} OPENAI_FALLBACK_KEY: ${{ secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c525c80..523766d5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,50 +17,6 @@ Semantic Versioning where the repository publishes a release. - Align stale `gpt-5.6-luna` test expectations with the valid `gpt-5.4` contract left behind by the earlier model rename. -- Honor each trusted base project's exact, integrity-bearing pnpm - `packageManager` specification in OpenCode coverage images through the pinned - Node distribution's Corepack runtime, instead of admitting the specification - during materialization and then rejecting every version except pnpm 11.5.3; - route generic coverage and docstring package scripts through the same - Corepack boundary instead of invoking a removed bare `pnpm` binary. -- Keep `--trust-lockfile` only for pnpm 11.3 and newer - (`trustLockfile` landed in pnpm 11.3). pnpm 9, 10, and 11.0–11.2 reject - that flag and previously failed LineageWeave JavaScript coverage before - tests could run. Jest test scripts still receive `--coverage` because Jest - documents a native coverage flag. -- Run declared JavaScript test scripts without synthesizing `--coverage` when - the package does not declare a compatible coverage command, but keep the - coverage result failed until the repository adds a lock-pinned provider and - owned coverage command. A generic `c8`, `nyc`, or Istanbul dependency no - longer makes an unrelated test runner receive an unsupported flag. -- Fix OpenCode coverage evidence for exact-base, organization-owned Python VCS - dependencies without weakening registry hashes or the networkless PR sandbox, - reject namespace, ambiguous, linked, native-extension, and installed-metadata - layouts, and make exact roots readable by the unprivileged coverage user. - - - -- Honor each trusted base project's exact, integrity-bearing pnpm - `packageManager` specification in OpenCode coverage images through the pinned - Node distribution's Corepack runtime, instead of admitting the specification - during materialization and then rejecting every version except pnpm 11.5.3; - route generic coverage and docstring package scripts through the same - Corepack boundary instead of invoking a removed bare `pnpm` binary. -- Keep `--trust-lockfile` only for pnpm 11.3 and newer - (`trustLockfile` landed in pnpm 11.3). pnpm 9, 10, and 11.0–11.2 reject - that flag and previously failed LineageWeave JavaScript coverage before - tests could run. Jest test scripts still receive `--coverage` because Jest - documents a native coverage flag. -- Run declared JavaScript test scripts without synthesizing `--coverage` when - the package does not declare a compatible coverage command, but keep the - coverage result failed until the repository adds a lock-pinned provider and - owned coverage command. A generic `c8`, `nyc`, or Istanbul dependency no - longer makes an unrelated test runner receive an unsupported flag. -- Fix OpenCode coverage evidence for exact-base, organization-owned Python VCS - dependencies without weakening registry hashes or the networkless PR sandbox, - reject namespace, ambiguous, linked, native-extension, and installed-metadata - layouts, and make exact roots readable by the unprivileged coverage user. - ### Added - Refresh the live product and technical gap baseline against the current diff --git a/docs/doctoring/strix-openai-fallback-api-base-routing.md b/docs/doctoring/strix-openai-fallback-api-base-routing.md index 4f7112303..c7a2a1890 100644 --- a/docs/doctoring/strix-openai-fallback-api-base-routing.md +++ b/docs/doctoring/strix-openai-fallback-api-base-routing.md @@ -23,7 +23,7 @@ showed the NVIDIA NIM primary and first fallback exhausting provider availability, then the contracted final fallback `openai-direct/gpt-5.4` failing with a plain-text gateway error: -``` +```text LLM CONNECTION FAILED Could not establish connection to the language model. Error: 404 page not found From 91c79dc882992deae9d2f0118f001c0f77c0f1af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 18:56:53 +0900 Subject: [PATCH 07/11] fix(opencode-review): restore deduplicated changelog entries and rename Luna comment - Devin bug: the CHANGELOG dedup accidentally removed the four legitimate [Unreleased] bullets entirely; restore them once alongside the two Strix entries so pending history is not dropped. - Devin bug: the OPENCODE_MODEL_CANDIDATES rationale comment still named the retired 'GPT-5.6 Luna' slot with stale pricing; rewrite it for the valid gpt-5.4 direct-OpenAI contract. --- .../workflows/opencode-review-dispatch.yml | 10 +++++----- CHANGELOG.md | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 33a4fd08e..478788d41 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -4471,16 +4471,16 @@ jobs: # or used for product/model improvement, so private repositories # include neither NIM nor anonymous free candidates and start at the # existing keyed fallback list: OpenCode Zen GPT-5.6 Terra, DeepSeek - # V3, the direct GPT-5.6 Luna slot, and pinned PAID + # V3, the direct GPT-5.4 slot, and pinned PAID # OpenRouter coder models (free-tier candidates hit the shared # free-models-per-day cap and hung for the full candidate timeout, # so the OpenRouter slots use cheap paid models billed against the # org's OpenRouter credits), then the full-size GPT-4.1 long-context # endpoint and provider-specific GPT/o3 fallbacks. - # The direct-OpenAI slot runs GPT-5.6 Luna: the newest family's - # cost-efficient tier, cheaper than the legacy gpt-5 it replaced - # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget - # stretches further between top-ups. + # The direct-OpenAI slot runs GPT-5.4: the current valid model name + # on the direct OpenAI platform (the retired gpt-5.6-luna catalog + # entry returned 404s), kept cost-efficient against the org's + # OpenAI budget between top-ups. OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free opencode-free/hy3-free opencode-free/minimax-m3-free opencode-free/glm-5-free opencode-free/kimi-k2.5-free opencode-free/qwen3.6-plus-free ' || '' }}opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.4 openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole diff --git a/CHANGELOG.md b/CHANGELOG.md index 523766d5e..056b3cff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,26 @@ Semantic Versioning where the repository publishes a release. fallback unreachable after NIM exhaustion. - Align stale `gpt-5.6-luna` test expectations with the valid `gpt-5.4` contract left behind by the earlier model rename. +- Honor each trusted base project's exact, integrity-bearing pnpm + `packageManager` specification in OpenCode coverage images through the pinned + Node distribution's Corepack runtime, instead of admitting the specification + during materialization and then rejecting every version except pnpm 11.5.3; + route generic coverage and docstring package scripts through the same + Corepack boundary instead of invoking a removed bare `pnpm` binary. +- Keep `--trust-lockfile` only for pnpm 11.3 and newer + (`trustLockfile` landed in pnpm 11.3). pnpm 9, 10, and 11.0–11.2 reject + that flag and previously failed LineageWeave JavaScript coverage before + tests could run. Jest test scripts still receive `--coverage` because Jest + documents a native coverage flag. +- Run declared JavaScript test scripts without synthesizing `--coverage` when + the package does not declare a compatible coverage command, but keep the + coverage result failed until the repository adds a lock-pinned provider and + owned coverage command. A generic `c8`, `nyc`, or Istanbul dependency no + longer makes an unrelated test runner receive an unsupported flag. +- Fix OpenCode coverage evidence for exact-base, organization-owned Python VCS + dependencies without weakening registry hashes or the networkless PR sandbox, + reject namespace, ambiguous, linked, native-extension, and installed-metadata + layouts, and make exact roots readable by the unprivileged coverage user. ### Added From c5f1c74cda6cfa315f1c69e4680df72c091ce328 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:05:17 +0900 Subject: [PATCH 08/11] test: repin review workflow contract to exact blob --- tests/test_pr_review_autofix_nvidia_nim_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 3b6c3a054..3a8ddcae1 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "33a4fd08e93e653e715b380ad9f9523e11d231e6" +REVIEW_DISPATCH_BLOB_SHA = "478788d41ed3e0d8389292a059d225336a925830" def _workflow_text(path: Path) -> str: From 3c7f430f59df0eec2b6d70c8272c60b906d071dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:01:24 +0900 Subject: [PATCH 09/11] fix(strix): preserve standalone custom OpenAI bases --- scripts/ci/strix_quick_gate.sh | 13 +++++-------- tests/test_strix_openai_fallback_api_base.py | 14 +++++++++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index dd27b63d2..b2ae0fd22 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2426,14 +2426,7 @@ resolved_llm_api_base_for_model() { if is_vertex_model "$model"; then return 0 fi - if is_explicit_openai_model "$model" && [ -z "${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" ]; then - # Without workflow provisioning, an explicit direct-OpenAI model must - # still not inherit a foreign primary base: resolve no override so - # litellm defaults to https://api.openai.com/v1. - return 0 - fi - - local api_base_file="$LLM_API_BASE_FILE" + local api_base_file="${LLM_API_BASE_FILE:-}" local api_base_file_name="LLM_API_BASE_FILE" if is_explicit_openai_model "$model" && [ -n "${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" ]; then # Cross-provider fallback: openai-direct/* candidates must reach the @@ -2442,6 +2435,10 @@ resolved_llm_api_base_for_model() { # this the fallback hits the primary gateway and 404s. api_base_file="$STRIX_OPENAI_FALLBACK_API_BASE_FILE" api_base_file_name="STRIX_OPENAI_FALLBACK_API_BASE_FILE" + # The workflow always provisions this file for cross-provider fallbacks. + # In standalone runs, an explicitly supplied LLM_API_BASE_FILE remains + # a caller-owned custom OpenAI-compatible endpoint rather than being + # silently discarded. 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 diff --git a/tests/test_strix_openai_fallback_api_base.py b/tests/test_strix_openai_fallback_api_base.py index f2c4237a7..f472ddeac 100644 --- a/tests/test_strix_openai_fallback_api_base.py +++ b/tests/test_strix_openai_fallback_api_base.py @@ -9,7 +9,8 @@ The gate must therefore prefer an explicit ``STRIX_OPENAI_FALLBACK_API_BASE_FILE`` for explicit direct-OpenAI models, and -fall back to litellm's default OpenAI endpoint when the override is absent. + fall back to a caller-supplied ``LLM_API_BASE_FILE`` for standalone custom + endpoints, or to litellm's default OpenAI endpoint when no base is supplied. """ from __future__ import annotations @@ -145,14 +146,21 @@ def test_nvidia_primary_with_override_routes_to_openai(self) -> None: self.assertEqual(rc, 0) self.assertEqual(api_base, OPENAI_FALLBACK_BASE) - def test_nvidia_primary_without_override_uses_default_openai(self) -> None: - """Without an override the resolver returns no base (litellm default).""" + def test_standalone_custom_base_is_honored_without_override(self) -> None: + """A standalone caller's explicit custom endpoint remains effective.""" rc, api_base = _resolve_api_base( {"LLM_API_BASE_FILE": "https://integrate.api.nvidia.com/v1"}, "openai-direct/gpt-5.4", ) self.assertEqual(rc, 0) + self.assertEqual(api_base, "https://integrate.api.nvidia.com/v1") + + def test_direct_openai_without_any_base_uses_default_openai(self) -> None: + """With no base file, LiteLLM still selects the native OpenAI endpoint.""" + + rc, api_base = _resolve_api_base({}, "openai-direct/gpt-5.4") + self.assertEqual(rc, 0) self.assertEqual(api_base, "") def test_primary_provider_models_keep_their_base(self) -> None: From 0547c6b3815865fb1c5d018266c6f033b56fde0e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:15:01 +0900 Subject: [PATCH 10/11] fix(strix): reject foreign GitHub Models fallback bases --- CHANGELOG.md | 15 +++++---- .../strix-openai-fallback-api-base-routing.md | 33 +++++++++++-------- scripts/ci/strix_quick_gate.sh | 9 +++++ tests/test_strix_openai_fallback_api_base.py | 10 ++++++ 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5f5fd856..c6899d38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,14 @@ Semantic Versioning where the repository publishes a release. ## [Unreleased] - Route Strix cross-provider fallbacks to explicit direct-OpenAI models (`openai-direct/...`) through the OpenAI inference endpoint instead of - inheriting the primary provider's `LLM_API_BASE`: the workflow now - provisions `STRIX_OPENAI_FALLBACK_API_BASE_FILE` - (`https://api.openai.com/v1`), the gate prefers it for explicit OpenAI - fallback models and otherwise resolves no override so litellm defaults to - OpenAI, and a non-https override fails configuration. This removes the - NVIDIA-NIM-edge `404 page not found` that made the contracted final - fallback unreachable after NIM exhaustion. + inheriting a provider-specific primary base: the workflow now provisions + `STRIX_OPENAI_FALLBACK_API_BASE_FILE` (`https://api.openai.com/v1`), while + standalone caller-supplied `LLM_API_BASE_FILE` values remain honored for + OpenAI-compatible endpoints. A GitHub Models base is never inherited, and + LiteLLM uses native OpenAI defaults only when no base is supplied. A + non-https override fails configuration. This removes the NVIDIA-NIM-edge + `404 page not found` that made the contracted final fallback unreachable + after NIM exhaustion. - Align stale `gpt-5.6-luna` test expectations with the valid `gpt-5.4` contract left behind by the earlier model rename. - Honor each trusted base project's exact, integrity-bearing pnpm diff --git a/docs/doctoring/strix-openai-fallback-api-base-routing.md b/docs/doctoring/strix-openai-fallback-api-base-routing.md index c7a2a1890..a51734954 100644 --- a/docs/doctoring/strix-openai-fallback-api-base-routing.md +++ b/docs/doctoring/strix-openai-fallback-api-base-routing.md @@ -6,8 +6,11 @@ A Strix cross-provider fallback to an explicit direct-OpenAI model (`openai-direct/...` or `openai_direct/...`) must route through the OpenAI inference endpoint, never through the primary provider's `LLM_API_BASE`. The gate now prefers an explicit `STRIX_OPENAI_FALLBACK_API_BASE_FILE` for such -models and, when the override is absent, resolves no API-base override so -litellm uses its default `https://api.openai.com/v1` endpoint. +models. In standalone runs, a caller-supplied `LLM_API_BASE_FILE` remains in +force for an OpenAI-compatible endpoint; a GitHub Models primary endpoint is +not inherited. Litellm uses its default `https://api.openai.com/v1` endpoint +only when no base file is supplied (or when that provider-specific base is +rejected). The central workflow writes `https://api.openai.com/v1` into `$RUNNER_TEMP/openai_fallback_api_base.txt` and exports @@ -44,9 +47,11 @@ The override is a runner-provisioned regular file under `$RUNNER_TEMP`, resolved through the same `resolve_trusted_input_file` boundary as the other API-base files: it must be a regular non-symlink file inside the trusted input root, must trim to a single `https://` URL, and must not contain whitespace or -control characters. Absent or empty overrides fall back to litellm's default -endpoint instead of failing configuration, preserving standalone local gate -runs where no workflow provisioning exists. +control characters. Absent or empty overrides preserve a caller-supplied +`LLM_API_BASE_FILE` for standalone local gate runs; when both files are absent, +litellm selects its default endpoint. A GitHub Models base is explicitly +rejected for a direct OpenAI model so a missing OpenAI key remains a +provider-unavailable outcome instead of a configuration error. ## Verification contract @@ -54,14 +59,16 @@ Regression evidence proves that: 1. with a NVIDIA NIM primary base configured, `openai-direct/gpt-5.4` resolves through the explicit OpenAI fallback base when provided; -2. without an override, the resolver returns no base so litellm defaults to - `https://api.openai.com/v1`; -3. NVIDIA NIM primary attempts keep resolving through the NIM edge; -4. `github_models/*` fallbacks keep their dedicated GitHub Models endpoint; -5. a non-https override fails configuration (exit 2) instead of scanning; -6. the workflow provisions the override file and passes it into the gate env; -7. the required-workflow smoke contract pins both sides of the wiring; and -8. the stale `gpt-5.6-luna` expectations left behind by the model rename are +2. without either base file, the resolver returns no base so litellm defaults + to `https://api.openai.com/v1`; +3. a standalone caller-supplied custom `LLM_API_BASE_FILE` remains effective; +4. a GitHub Models primary base is not inherited by a direct-OpenAI fallback; +5. NVIDIA NIM primary attempts keep resolving through the NIM edge; +6. `github_models/*` fallbacks keep their dedicated GitHub Models endpoint; +7. a non-https override fails configuration (exit 2) instead of scanning; +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. ## Limitations diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index b2ae0fd22..c5426a6ca 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2475,6 +2475,15 @@ resolved_llm_api_base_for_model() { echo "ERROR: LLM_API_BASE must be an https URL when configured." >&2 return 2 fi + # Never let a provider-specific GitHub Models base leak into an explicit + # direct-OpenAI fallback when no separate OpenAI override was provisioned. + # Other caller-supplied OpenAI-compatible endpoints remain valid standalone + # configuration and are intentionally preserved. + if is_explicit_openai_model "$model" \ + && [ -z "${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" ] \ + && is_github_models_api_base "$llm_api_base_value"; then + return 0 + fi if is_github_models_api_base "$llm_api_base_value" && ! is_github_models_api_compatible_model "$model"; then echo "ERROR: LLM_API_BASE may route through GitHub Models only when STRIX_LLM uses a GitHub Models-compatible model." >&2 return 2 diff --git a/tests/test_strix_openai_fallback_api_base.py b/tests/test_strix_openai_fallback_api_base.py index f472ddeac..7cdf50f28 100644 --- a/tests/test_strix_openai_fallback_api_base.py +++ b/tests/test_strix_openai_fallback_api_base.py @@ -163,6 +163,16 @@ def test_direct_openai_without_any_base_uses_default_openai(self) -> None: self.assertEqual(rc, 0) self.assertEqual(api_base, "") + def test_github_models_base_is_not_inherited_without_override(self) -> None: + """A cross-provider fallback never inherits GitHub Models routing.""" + + rc, api_base = _resolve_api_base( + {"LLM_API_BASE_FILE": "https://models.github.ai/inference"}, + "openai-direct/gpt-5.4", + ) + self.assertEqual(rc, 0) + self.assertEqual(api_base, "") + def test_primary_provider_models_keep_their_base(self) -> None: """NVIDIA NIM primary attempts still resolve through the NIM edge.""" From 4fc261ff03f28b4ab8b44ef6d9f14cdb138eb3ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:28:51 +0900 Subject: [PATCH 11/11] fix(strix): reject known foreign fallback bases --- CHANGELOG.md | 6 +++--- .../strix-openai-fallback-api-base-routing.md | 18 ++++++++++-------- scripts/ci/strix_quick_gate.sh | 15 +++++++++++++-- tests/test_strix_openai_fallback_api_base.py | 13 ++++++++++++- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6899d38b..cef0acda6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,9 @@ Semantic Versioning where the repository publishes a release. inheriting a provider-specific primary base: the workflow now provisions `STRIX_OPENAI_FALLBACK_API_BASE_FILE` (`https://api.openai.com/v1`), while standalone caller-supplied `LLM_API_BASE_FILE` values remain honored for - OpenAI-compatible endpoints. A GitHub Models base is never inherited, and - LiteLLM uses native OpenAI defaults only when no base is supplied. A - non-https override fails configuration. This removes the NVIDIA-NIM-edge + OpenAI-compatible endpoints. Known GitHub Models, NVIDIA NIM, and OpenRouter + bases are never inherited, and LiteLLM uses native OpenAI defaults only when + no base is supplied. A non-https override fails configuration. This removes the NVIDIA-NIM-edge `404 page not found` that made the contracted final fallback unreachable after NIM exhaustion. - Align stale `gpt-5.6-luna` test expectations with the valid `gpt-5.4` diff --git a/docs/doctoring/strix-openai-fallback-api-base-routing.md b/docs/doctoring/strix-openai-fallback-api-base-routing.md index a51734954..d38d222d4 100644 --- a/docs/doctoring/strix-openai-fallback-api-base-routing.md +++ b/docs/doctoring/strix-openai-fallback-api-base-routing.md @@ -7,10 +7,10 @@ A Strix cross-provider fallback to an explicit direct-OpenAI model inference endpoint, never through the primary provider's `LLM_API_BASE`. The gate now prefers an explicit `STRIX_OPENAI_FALLBACK_API_BASE_FILE` for such models. In standalone runs, a caller-supplied `LLM_API_BASE_FILE` remains in -force for an OpenAI-compatible endpoint; a GitHub Models primary endpoint is -not inherited. Litellm uses its default `https://api.openai.com/v1` endpoint -only when no base file is supplied (or when that provider-specific base is -rejected). +force for an OpenAI-compatible endpoint; known GitHub Models, NVIDIA NIM, and +OpenRouter primary endpoints are not inherited. Litellm uses its default +`https://api.openai.com/v1` endpoint only when no base file is supplied (or +when that provider-specific base is rejected). The central workflow writes `https://api.openai.com/v1` into `$RUNNER_TEMP/openai_fallback_api_base.txt` and exports @@ -49,9 +49,10 @@ API-base files: it must be a regular non-symlink file inside the trusted input root, must trim to a single `https://` URL, and must not contain whitespace or control characters. Absent or empty overrides preserve a caller-supplied `LLM_API_BASE_FILE` for standalone local gate runs; when both files are absent, -litellm selects its default endpoint. A GitHub Models base is explicitly -rejected for a direct OpenAI model so a missing OpenAI key remains a -provider-unavailable outcome instead of a configuration error. +litellm selects its default endpoint. Known GitHub Models, NVIDIA NIM, and +OpenRouter bases are explicitly rejected for a direct OpenAI model so a +missing OpenAI key remains a provider-unavailable outcome instead of a +configuration error. ## Verification contract @@ -62,7 +63,8 @@ Regression evidence proves that: 2. without either base file, the resolver returns no base so litellm defaults to `https://api.openai.com/v1`; 3. a standalone caller-supplied custom `LLM_API_BASE_FILE` remains effective; -4. a GitHub Models primary base is not inherited by a direct-OpenAI fallback; +4. known GitHub Models, NVIDIA NIM, and OpenRouter primary bases are not + inherited by a direct-OpenAI fallback; 5. NVIDIA NIM primary attempts keep resolving through the NIM edge; 6. `github_models/*` fallbacks keep their dedicated GitHub Models endpoint; 7. a non-https override fails configuration (exit 2) instead of scanning; diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index c5426a6ca..c9aa41545 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -827,6 +827,17 @@ is_github_models_api_base() { esac } +is_known_foreign_provider_api_base() { + case "$1" in + https://models.github.ai/* | https://integrate.api.nvidia.com/* | https://openrouter.ai/*) + return 0 + ;; + *) + return 1 + ;; + esac +} + PRIMARY_MODEL="$(normalize_model "$STRIX_LLM")" if [ "$PRIMARY_MODEL" != "$STRIX_LLM" ]; then echo "Normalized STRIX_LLM to provider-qualified model '$PRIMARY_MODEL'." @@ -2475,13 +2486,13 @@ resolved_llm_api_base_for_model() { echo "ERROR: LLM_API_BASE must be an https URL when configured." >&2 return 2 fi - # Never let a provider-specific GitHub Models base leak into an explicit + # Never let a known provider-specific base leak into an explicit # direct-OpenAI fallback when no separate OpenAI override was provisioned. # Other caller-supplied OpenAI-compatible endpoints remain valid standalone # configuration and are intentionally preserved. if is_explicit_openai_model "$model" \ && [ -z "${STRIX_OPENAI_FALLBACK_API_BASE_FILE:-}" ] \ - && is_github_models_api_base "$llm_api_base_value"; then + && is_known_foreign_provider_api_base "$llm_api_base_value"; then return 0 fi if is_github_models_api_base "$llm_api_base_value" && ! is_github_models_api_compatible_model "$model"; then diff --git a/tests/test_strix_openai_fallback_api_base.py b/tests/test_strix_openai_fallback_api_base.py index 7cdf50f28..f078dd2a0 100644 --- a/tests/test_strix_openai_fallback_api_base.py +++ b/tests/test_strix_openai_fallback_api_base.py @@ -50,6 +50,7 @@ def _resolver_helpers() -> list[str]: ("is_vertex_model", gate_source), ("is_github_models_model", gate_source), ("is_github_models_api_base", gate_source), + ("is_known_foreign_provider_api_base", gate_source), ("is_github_models_api_compatible_model", gate_source), ("is_explicit_openai_model", gate_source), ("resolve_trusted_input_file", gate_source), @@ -149,12 +150,22 @@ def test_nvidia_primary_with_override_routes_to_openai(self) -> None: def test_standalone_custom_base_is_honored_without_override(self) -> None: """A standalone caller's explicit custom endpoint remains effective.""" + rc, api_base = _resolve_api_base( + {"LLM_API_BASE_FILE": "https://api.example.com/v1"}, + "openai-direct/gpt-5.4", + ) + self.assertEqual(rc, 0) + self.assertEqual(api_base, "https://api.example.com/v1") + + def test_nvidia_base_is_not_inherited_without_override(self) -> None: + """A cross-provider fallback never inherits the NVIDIA NIM base.""" + rc, api_base = _resolve_api_base( {"LLM_API_BASE_FILE": "https://integrate.api.nvidia.com/v1"}, "openai-direct/gpt-5.4", ) self.assertEqual(rc, 0) - self.assertEqual(api_base, "https://integrate.api.nvidia.com/v1") + self.assertEqual(api_base, "") def test_direct_openai_without_any_base_uses_default_openai(self) -> None: """With no base file, LiteLLM still selects the native OpenAI endpoint."""