Skip to content

Commit 934369a

Browse files
committed
feat(action): support any LLM provider via llm_provider input
The action hardcoded OPENROUTER_API_KEY + an OpenRouter-only preflight, so a user with an OpenAI/Anthropic/etc. key couldn't use it. Add an optional 'llm_provider' input that maps the key to the engine's env var by the <NAME>_API_KEY convention (with aws_bedrock/ollama exceptions); the engine then auto-selects the provider. Gate the OpenRouter preflight and the 'openrouter/' model-prefix guard behind provider==openrouter, export only the selected env var in both engine steps, and add llm_provider to the base-cache key. Default stays openrouter, so existing workflows are unchanged. No engine change.
1 parent 11b3939 commit 934369a

1 file changed

Lines changed: 57 additions & 40 deletions

File tree

action.yml

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ branding:
88

99
inputs:
1010
llm_api_key:
11-
description: 'LLM API key (OpenRouter by default). Required.'
11+
description: 'Your LLM provider API key (see llm_provider). Required.'
1212
required: true
13+
llm_provider:
14+
description: 'Provider for llm_api_key. The key is handed to the engine as that provider''s env var (anthropic -> ANTHROPIC_API_KEY, openai -> OPENAI_API_KEY, ...; aws_bedrock -> AWS_BEARER_TOKEN_BEDROCK, ollama -> OLLAMA_BASE_URL) and the engine auto-selects it. Default openrouter.'
15+
required: false
16+
default: 'openrouter'
1317
github_token:
1418
description: 'GITHUB_TOKEN used to post the PR comment. Defaults to the workflow token.'
1519
required: false
@@ -245,6 +249,7 @@ runs:
245249
shell: bash
246250
env:
247251
RAW_KEY: ${{ inputs.llm_api_key }}
252+
RAW_PROVIDER: ${{ inputs.llm_provider }}
248253
RAW_AGENT_MODEL: ${{ inputs.agent_model }}
249254
RAW_PARSING_MODEL: ${{ inputs.parsing_model }}
250255
run: |
@@ -254,49 +259,56 @@ runs:
254259
echo "::error::llm_api_key is empty. On fork PRs, repo secrets are withheld by GitHub."
255260
exit 1
256261
fi
257-
# Pasting a key into the secret UI often picks up trailing newlines,
258-
# wrapping quotes, or a whole `KEY=value` line. Normalize all of that.
262+
# Resolve the provider -> the env var the engine reads. Convention is
263+
# <NAME>_API_KEY; two providers don't follow it. The engine is the source
264+
# of truth: an unknown provider just yields an env var it won't recognize,
265+
# and the engine errors with the list of valid keys.
266+
PROVIDER="$(printf '%s' "$RAW_PROVIDER" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_')"
267+
PROVIDER="${PROVIDER:-openrouter}"
268+
case "$PROVIDER" in
269+
aws_bedrock) PROVIDER_ENV="AWS_BEARER_TOKEN_BEDROCK" ;;
270+
ollama) PROVIDER_ENV="OLLAMA_BASE_URL" ;;
271+
*) PROVIDER_ENV="$(printf '%s' "$PROVIDER" | tr '[:lower:]' '[:upper:]')_API_KEY" ;;
272+
esac
273+
274+
# Normalize a pasted key: strip whitespace/quotes and a leading `<ENV>=`.
259275
_strip() { printf '%s' "$1" | tr -d '[:space:]' | sed -e 's/^"//;s/"$//' -e "s/^'//;s/'\$//"; }
260276
KEY="$(_strip "$RAW_KEY")"
261-
case "$KEY" in
262-
OPENROUTER_API_KEY=*) KEY="${KEY#OPENROUTER_API_KEY=}";;
263-
openrouter_api_key=*) KEY="${KEY#openrouter_api_key=}";;
264-
esac
277+
case "$KEY" in "${PROVIDER_ENV}="*) KEY="${KEY#${PROVIDER_ENV}=}";; esac
265278
KEY="$(_strip "$KEY")"
266279
AGENT_MODEL="$(_strip "$RAW_AGENT_MODEL")"
267280
PARSING_MODEL="$(_strip "$RAW_PARSING_MODEL")"
268-
269-
# Catch the most common model-id mistake early: the engine calls OpenRouter
270-
# natively (langchain ChatOpenAI), so a model must be a BARE slug like
271-
# anthropic/claude-sonnet-4 — NOT the litellm 'openrouter/...' form.
272-
for M in "$AGENT_MODEL" "$PARSING_MODEL"; do
273-
case "$M" in
274-
openrouter/*)
275-
echo "::error::Invalid model '$M': drop the 'openrouter/' prefix and use a bare OpenRouter slug, e.g. anthropic/claude-sonnet-4."
276-
exit 1 ;;
277-
esac
278-
done
279-
280-
# Mask the cleaned value (it may differ from the registered secret).
281281
echo "::add-mask::$KEY"
282-
283-
case "$KEY" in sk-or-v1-*) PFX=1 ;; *) PFX=0 ;; esac
284-
echo "OPENROUTER_API_KEY length: ${#KEY}; looks-like-OpenRouter: $PFX"
285-
STATUS=$(curl -sS -o "$AUTH_FILE" -w "%{http_code}" \
286-
-H "Authorization: Bearer $KEY" --max-time 10 \
287-
https://openrouter.ai/api/v1/auth/key || echo "curl-fail")
288-
echo "OpenRouter /auth/key response: HTTP $STATUS"
289-
if [ "$STATUS" != "200" ]; then
290-
# Surface the upstream error MESSAGE only — never the whole auth body (avoid leaking).
291-
MSG="$(AUTH_FILE="$AUTH_FILE" python3 -c 'import json,os;print(json.load(open(os.environ["AUTH_FILE"])).get("error",{}).get("message",""))' 2>/dev/null || true)"
292-
echo "::error::OpenRouter rejected the API key (HTTP $STATUS). ${MSG:-Verify the OPENROUTER_API_KEY secret.}"
293-
exit 1
282+
echo "Provider: $PROVIDER -> $PROVIDER_ENV; key length: ${#KEY}"
283+
284+
if [ "$PROVIDER" = "openrouter" ]; then
285+
# OpenRouter-only checks. The litellm 'openrouter/...' model prefix 400s
286+
# the engine's native OpenRouter call; other providers use native ids.
287+
for M in "$AGENT_MODEL" "$PARSING_MODEL"; do
288+
case "$M" in
289+
openrouter/*)
290+
echo "::error::Invalid model '$M': drop the 'openrouter/' prefix and use a bare OpenRouter slug, e.g. anthropic/claude-sonnet-4."
291+
exit 1 ;;
292+
esac
293+
done
294+
# Cheap preflight; other providers are validated by the engine at run time.
295+
STATUS=$(curl -sS -o "$AUTH_FILE" -w "%{http_code}" \
296+
-H "Authorization: Bearer $KEY" --max-time 10 \
297+
https://openrouter.ai/api/v1/auth/key || echo "curl-fail")
298+
echo "OpenRouter /auth/key response: HTTP $STATUS"
299+
if [ "$STATUS" != "200" ]; then
300+
# Surface the upstream error MESSAGE only — never the whole auth body (avoid leaking).
301+
MSG="$(AUTH_FILE="$AUTH_FILE" python3 -c 'import json,os;print(json.load(open(os.environ["AUTH_FILE"])).get("error",{}).get("message",""))' 2>/dev/null || true)"
302+
echo "::error::OpenRouter rejected the API key (HTTP $STATUS). ${MSG:-Verify the OPENROUTER_API_KEY secret.}"
303+
exit 1
304+
fi
294305
fi
295306
296307
# Store key material in runner-temp files. Later shell steps read these
297308
# explicitly; third-party post-comment actions do not inherit the LLM key.
298309
umask 077
299-
printf '%s' "$KEY" > "${RUNNER_TEMP}/cb-openrouter-key"
310+
printf '%s' "$KEY" > "${RUNNER_TEMP}/cb-llm-key"
311+
printf '%s' "$PROVIDER_ENV" > "${RUNNER_TEMP}/cb-provider-env"
300312
printf '%s' "$AGENT_MODEL" > "${RUNNER_TEMP}/cb-agent-model"
301313
printf '%s' "$PARSING_MODEL" > "${RUNNER_TEMP}/cb-parsing-model"
302314
@@ -328,7 +340,7 @@ runs:
328340
uses: actions/cache/restore@v4
329341
with:
330342
path: ${{ runner.temp }}/cb-base
331-
key: cb-base-${{ runner.os }}-${{ steps.guard.outputs.base_sha }}-d${{ inputs.depth_level }}-${{ inputs.engine_ref }}-${{ inputs.agent_model }}-${{ inputs.parsing_model }}
343+
key: cb-base-${{ runner.os }}-${{ steps.guard.outputs.base_sha }}-d${{ inputs.depth_level }}-${{ inputs.engine_ref }}-${{ inputs.llm_provider }}-${{ inputs.agent_model }}-${{ inputs.parsing_model }}
332344

333345
- name: Generate base analysis (no committed baseline)
334346
if: steps.guard.outputs.skip != 'true' && steps.base.outputs.committed == 'false' && steps.basecache.outputs.cache-hit != 'true'
@@ -348,8 +360,10 @@ runs:
348360
DEPTH: ${{ inputs.depth_level }}
349361
BASE_SHA: ${{ steps.guard.outputs.base_sha }}
350362
run: |
351-
OPENROUTER_API_KEY="$(cat "${RUNNER_TEMP}/cb-openrouter-key")"
352-
export OPENROUTER_API_KEY
363+
# Export the key under the selected provider's env var (only this one),
364+
# so the engine auto-selects that provider.
365+
PROVIDER_ENV="$(cat "${RUNNER_TEMP}/cb-provider-env")"
366+
export "$PROVIDER_ENV"="$(cat "${RUNNER_TEMP}/cb-llm-key")"
353367
# Export the model env only when the user set it; empty -> the engine uses
354368
# its own valid per-provider default (no stale hardcoded model id to rot).
355369
AGENT_MODEL="$(cat "${RUNNER_TEMP}/cb-agent-model")"
@@ -382,7 +396,7 @@ runs:
382396
uses: actions/cache/save@v4
383397
with:
384398
path: ${{ runner.temp }}/cb-base
385-
key: cb-base-${{ runner.os }}-${{ steps.guard.outputs.base_sha }}-d${{ inputs.depth_level }}-${{ inputs.engine_ref }}-${{ inputs.agent_model }}-${{ inputs.parsing_model }}
399+
key: cb-base-${{ runner.os }}-${{ steps.guard.outputs.base_sha }}-d${{ inputs.depth_level }}-${{ inputs.engine_ref }}-${{ inputs.llm_provider }}-${{ inputs.agent_model }}-${{ inputs.parsing_model }}
386400

387401
- name: Analyze PR head (incremental from base)
388402
if: steps.guard.outputs.skip != 'true'
@@ -405,8 +419,10 @@ runs:
405419
BASE_SHA: ${{ steps.guard.outputs.base_sha }}
406420
HEAD_SHA: ${{ steps.guard.outputs.head_sha }}
407421
run: |
408-
OPENROUTER_API_KEY="$(cat "${RUNNER_TEMP}/cb-openrouter-key")"
409-
export OPENROUTER_API_KEY
422+
# Export the key under the selected provider's env var (only this one),
423+
# so the engine auto-selects that provider.
424+
PROVIDER_ENV="$(cat "${RUNNER_TEMP}/cb-provider-env")"
425+
export "$PROVIDER_ENV"="$(cat "${RUNNER_TEMP}/cb-llm-key")"
410426
# Export the model env only when the user set it; empty -> the engine uses
411427
# its own valid per-provider default (no stale hardcoded model id to rot).
412428
AGENT_MODEL="$(cat "${RUNNER_TEMP}/cb-agent-model")"
@@ -464,7 +480,8 @@ runs:
464480
if: always() && steps.guard.outputs.skip != 'true'
465481
shell: bash
466482
run: |
467-
rm -f "${RUNNER_TEMP}/cb-openrouter-key" \
483+
rm -f "${RUNNER_TEMP}/cb-llm-key" \
484+
"${RUNNER_TEMP}/cb-provider-env" \
468485
"${RUNNER_TEMP}/cb-agent-model" \
469486
"${RUNNER_TEMP}/cb-parsing-model"
470487

0 commit comments

Comments
 (0)