Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3153,7 +3153,7 @@ jobs:
"$schema": "https://opencode.ai/config.json",
"model": "github-models/deepseek/deepseek-r1-0528",
"small_model": "github-models/deepseek/deepseek-v3-0324",
"enabled_providers": ["openai", "github-models"],
"enabled_providers": ["openai", "openrouter", "github-models"],
"lsp": false,
"mcp": {},
"permission": {
Expand Down Expand Up @@ -3291,6 +3291,24 @@ jobs:
}
}
},
"openrouter": {
"npm": "@ai-sdk/openai-compatible",
"name": "OpenRouter",
"options": {
"baseURL": "https://openrouter.ai/api/v1",
"apiKey": "{env:OPENROUTER_API_KEY}"
},
"models": {
"openrouter/free": {
"name": "OpenRouter Free",
"tool_call": true,
"limit": {
"context": 128000,
"output": 4096
}
}
}
},
"github-models": {
"npm": "@ai-sdk/openai-compatible",
"name": "GitHub Models",
Expand Down Expand Up @@ -3513,18 +3531,20 @@ jobs:
# model a working, un-throttled backend. Resolves {env:OPENAI_API_KEY}
# in the opencode.jsonc "openai" provider block.
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
SHARE: "false"
NPM_CONFIG_IGNORE_SCRIPTS: "true"
NO_COLOR: "1"
# High-sensitivity review candidates only. DeepSeek V3 has been the
# most reliable first-pass reviewer in the org queue, then the pool
# falls through to the direct GPT-5.6 Luna slot, then the full-size
# GPT-4.1 long-context endpoint and provider-specific GPT/o3 fallbacks.
# falls through to the direct GPT-5.6 Luna slot, OpenRouter's free
# compatibility endpoint, 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.
OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna 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: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/openrouter/free 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.
Expand Down Expand Up @@ -5501,7 +5521,7 @@ jobs:
".github/workflows/strix.yml" \
"scripts/ci/test_strix_quick_gate.sh"
emit_known_missing_string_finding \
"STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, or an approved organization Vertex AI model" \
"STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" \
"Strix unsupported-model errors must name the allowed providers" \
".github/workflows/strix.yml" \
"scripts/ci/test_strix_quick_gate.sh"
Expand Down
120 changes: 120 additions & 0 deletions .github/workflows/openrouter-diag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: OpenRouter Diagnostic

on:
workflow_dispatch:
push:
branches:
- openrouter-diag-branch
Comment on lines +3 to +7

permissions:
contents: read

jobs:
diag:
runs-on: ubuntu-latest
steps:
Comment on lines +13 to +15
- name: Probe OpenRouter free model endpoint
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: |
set -euo pipefail
if [ -z "${OPENROUTER_API_KEY:-}" ]; then
echo '::error::OPENROUTER_API_KEY secret is empty or not visible to this workflow.'
exit 1
fi
key_len="$(printf '%s' "$OPENROUTER_API_KEY" | wc -c | tr -d ' ')"
echo "Key length: ${key_len} characters."

body='{"model":"openrouter/free","messages":[{"role":"user","content":"Say OK"}],"max_tokens":10}'
response_file="$(mktemp)"
status="$(curl -sS -o "$response_file" -w '%{http_code}' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-d "$body" \
https://openrouter.ai/api/v1/chat/completions)"
Comment on lines +30 to +34
Comment on lines +20 to +34
echo "HTTP status: $status"
echo "Response (first 1500 bytes):"
head -c 1500 "$response_file"
echo
Comment on lines +30 to +38

echo "--- tool-call probe ---"
tool_body='{"model":"openrouter/free","messages":[{"role":"user","content":"Call the ping tool"}],"tools":[{"type":"function","function":{"name":"ping","description":"ping","parameters":{"type":"object","properties":{}}}}],"max_tokens":50}'
tool_status="$(curl -sS -o "$response_file" -w '%{http_code}' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${OPENROUTER_API_KEY}" \
-d "$tool_body" \
https://openrouter.ai/api/v1/chat/completions)"
echo "HTTP status: $tool_status"
head -c 1500 "$response_file"
echo

echo "--- key info endpoint ---"
key_status="$(curl -sS -o "$response_file" -w '%{http_code}' \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
https://openrouter.ai/api/v1/auth/key)"
echo "HTTP status: $key_status"
head -c 1500 "$response_file"
echo

- name: Run OpenCode CLI against openrouter/openrouter/free
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENCODE_VERSION: "1.17.13"
OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348
run: |
set -euo pipefail
archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz"
install_dir="${HOME}/.opencode/bin"
mkdir -p "$install_dir"
curl -fsSL -o "$archive" \
"https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz"
printf '%s %s\n' "$OPENCODE_SHA256" "$archive" | sha256sum -c -
tar -xzf "$archive" -C "$RUNNER_TEMP"
install -m 0755 "${RUNNER_TEMP}/opencode" "${install_dir}/opencode"
export PATH="$install_dir:$PATH"
opencode --version

workdir="$(mktemp -d)"
cd "$workdir"
git init -q .
cat > opencode.jsonc <<'EOF'
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"openrouter": {
"npm": "@ai-sdk/openai-compatible",
"name": "OpenRouter",
"options": {
"baseURL": "https://openrouter.ai/api/v1",
"apiKey": "{env:OPENROUTER_API_KEY}"
},
"models": {
"openrouter/free": {
"name": "OpenRouter Free",
"tool_call": true,
"limit": { "context": 128000, "output": 4096 }
}
}
}
}
}
EOF
run_variant() {
local label="$1"; shift
echo "=== variant: $label ==="
set +e
timeout --kill-after=15s 120s \
opencode run "Reply with the single word OK and nothing else." \
--model openrouter/openrouter/free "$@" > out.json 2> err.log
local rc=$?
set -e
echo "exit code: $rc"
echo "--- stdout (first 3000 bytes) ---"
head -c 3000 out.json; echo
echo "--- stderr (first 2000 bytes) ---"
head -c 2000 err.log; echo
}
run_variant "plain"
run_variant "format-json" --format json
run_variant "pure" --pure
run_variant "pure-format-json" --pure --format json
40 changes: 33 additions & 7 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ jobs:
env:
STRIX_MODEL: ${{ github.event.client_payload.strix_llm || 'gpt-5.6-luna' }}
STRIX_OPENAI_API_KEY: ${{ secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
STRIX_OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
STRIX_VERTEX_CREDENTIALS: ${{ secrets.GCP_SA_KEY }}
STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}
run: |
Expand Down Expand Up @@ -458,6 +459,16 @@ jobs:
exit 1
fi
;;
openrouter/free | openrouter/openrouter/free)
echo 'enabled=true' >> "$GITHUB_OUTPUT"
echo 'provider_mode=openrouter' >> "$GITHUB_OUTPUT"
sanitized_openrouter_key="$(printf '%s' "$STRIX_OPENROUTER_API_KEY" | tr -d '\r\n')"
trimmed_openrouter_key="$(printf '%s' "$sanitized_openrouter_key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [ -z "$trimmed_openrouter_key" ]; then
echo '::error::OPENROUTER_API_KEY is required for Strix OpenRouter scans.'
exit 1
fi
;;
vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash)
echo 'enabled=true' >> "$GITHUB_OUTPUT"
echo 'provider_mode=vertex_ai' >> "$GITHUB_OUTPUT"
Expand All @@ -469,7 +480,7 @@ jobs:
fi
;;
*)
echo '::error::STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, or an approved organization Vertex AI model.'
echo '::error::STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model.'
exit 1
;;
esac
Expand Down Expand Up @@ -539,7 +550,7 @@ jobs:
- name: Mask LLM API key
if: steps.gate.outputs.enabled == 'true'
env:
LLM_API_KEY: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY) || '' }}
LLM_API_KEY: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY) || steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_API_KEY || '' }}
run: |
# Sanitize CR/LF before masking to prevent broken ::add-mask::
# commands and potential workflow command injection.
Expand All @@ -555,7 +566,7 @@ jobs:
- name: Prepare LLM API key input file
if: steps.gate.outputs.enabled == 'true'
env:
LLM_API_KEY_SECRET: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY) || '' }}
LLM_API_KEY_SECRET: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY) || steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_API_KEY || '' }}
PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }}
run: |
sanitized="$(printf '%s' "$LLM_API_KEY_SECRET" | tr -d '\r\n')"
Expand All @@ -568,11 +579,23 @@ jobs:
echo '::error::STRIX_OPENAI_API_KEY is required for Strix OpenAI Platform scans.'
exit 1
fi
if [ -z "$trimmed" ] && [ "$PROVIDER_MODE" = "openrouter" ]; then
echo '::error::OPENROUTER_API_KEY is required for Strix OpenRouter scans.'
exit 1
fi
umask 077
llm_api_key_file="$RUNNER_TEMP/llm_api_key.txt"
printf '%s' "$sanitized" > "$llm_api_key_file"
printf '%s' "$trimmed" > "$llm_api_key_file"
echo "LLM_API_KEY_FILE=$llm_api_key_file" >> "$GITHUB_ENV"

- name: Prepare OpenRouter API base
if: steps.gate.outputs.provider_mode == 'openrouter'
run: |
umask 077
llm_api_base_file="$RUNNER_TEMP/llm_api_base.txt"
printf '%s' 'https://openrouter.ai/api/v1' > "$llm_api_base_file"
echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV"

- name: Prepare GitHub Models API base
if: steps.gate.outputs.provider_mode == 'github_models'
run: |
Expand All @@ -582,7 +605,7 @@ 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'
if: steps.gate.outputs.provider_mode == 'openai_direct' || steps.gate.outputs.provider_mode == 'openrouter'
env:
GITHUB_MODELS_FALLBACK_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}
run: |
Expand Down Expand Up @@ -683,11 +706,14 @@ jobs:
gpt-*)
printf 'openai_direct/%s' "$strix_model" > "$strix_llm_file"
;;
openrouter/free | openrouter/openrouter/free)
printf '%s' 'openrouter/free' > "$strix_llm_file"
;;
vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash)
printf '%s' "$strix_model" > "$strix_llm_file"
;;
*)
echo '::error::STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, or an approved organization Vertex AI model.'
echo '::error::STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model.'
exit 1
;;
esac
Expand Down Expand Up @@ -724,7 +750,7 @@ jobs:
STRIX_LLM_MAX_RETRIES: 1
STRIX_TRANSIENT_RETRY_PER_MODEL: 2
STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 60
STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || '' }}
STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openrouter' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || '' }}
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_FAIL_ON_PROVIDER_SIGNAL: "1"
Expand Down
10 changes: 6 additions & 4 deletions requirements-strix-ci-hashes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1501,10 +1501,12 @@ protobuf==6.33.6 \
# grpc-google-iam-v1
# grpcio-status
# proto-plus
pyasn1==0.6.3 \
--hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \
--hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde
# via pyasn1-modules
pyasn1==0.6.4 \
--hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \
--hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b
# via
# -r requirements-strix-ci.txt
# pyasn1-modules
pyasn1-modules==0.4.2 \
--hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \
--hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6
Expand Down
1 change: 1 addition & 0 deletions requirements-strix-ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ google-cloud-aiplatform==1.133.0
protobuf<7.0.0
cryptography==49.0.0
python-multipart==0.0.31
pyasn1==0.6.4
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,7 @@ emit_known_missing_string_finding \
"scripts/ci/test_strix_quick_gate.sh"
emit_known_missing_string_finding \
"$EVIDENCE_FILE" \
"STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, or an approved organization Vertex AI model" \
"STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" \
"Strix unsupported-model errors must name the allowed providers" \
".github/workflows/strix.yml" \
"scripts/ci/test_strix_quick_gate.sh"
Expand Down
11 changes: 11 additions & 0 deletions scripts/ci/run_opencode_review_model_pool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@ is_direct_openai_candidate() {
esac
}

is_openrouter_candidate() {
case "$1" in
openrouter/*) return 0 ;;
*) return 1 ;;
esac
}

is_low_sensitivity_candidate() {
case "$1" in
openai/*-mini | openai/*-nano | \
Expand All @@ -334,6 +341,10 @@ should_skip_model_candidate() {
printf 'Skipping OpenCode %s because OPENAI_API_KEY is not configured; falling back to the next provider-qualified candidate.\n' "$model_candidate"
return 0
fi
if is_openrouter_candidate "$model_candidate" && [ -z "${OPENROUTER_API_KEY:-}" ]; then
printf 'Skipping OpenCode %s because OPENROUTER_API_KEY is not configured; falling back to the next provider-qualified candidate.\n' "$model_candidate"
return 0
fi
return 1
}

Expand Down
8 changes: 4 additions & 4 deletions scripts/ci/strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2224,10 +2224,10 @@ resolved_llm_api_base_for_model() {

local api_base_file="$LLM_API_BASE_FILE"
local api_base_file_name="LLM_API_BASE_FILE"
if [ -z "$api_base_file" ] && is_github_models_model "$model" && [ -n "${STRIX_GITHUB_MODELS_API_BASE_FILE:-}" ]; then
# Cross-provider fallback: a direct-OpenAI primary run keeps its own
# key and no API base, while github_models/* fallback models route
# through the GitHub Models inference endpoint supplied here.
if 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.
api_base_file="$STRIX_GITHUB_MODELS_API_BASE_FILE"
api_base_file_name="STRIX_GITHUB_MODELS_API_BASE_FILE"
fi
Expand Down
Loading
Loading