ci/woodpecker/pr/pr-review Pipeline was successful
Addresses the a7890e0a review.
- pr-review.yml: strip REVIEW_TOKEN from the valid-model.sh call too (it runs a
script from $HOMELAB, branch-supplied for a homelab PR), and guard it with
`|| true` so a missing script can't kill the step -- restoring the stated "a
run never dies on selection" invariant. For a complex/security-sensitive
review, ignore the (injectable) triage pick and run on the CLI default
(strongest configured) model.
- valid-model.sh: reject a leading dash at the shape check (making the header
accurate) so an option-like value is refused even without a catalog.
- fetch-models.sh: --retry 2 --retry-connrefused so a transient blip doesn't
drop the run to the default; add a FETCH_MODELS_BODY_FILE test seam.
- pick-model.sh: pass --settings (new ci/agents/model-picker/settings.json,
Read deny-list) as belt-and-suspenders behind --allowedTools "".
- test-model-selection.sh: add fetch-models.sh mapping tests (well-formed body
-> non-null capability columns; degraded body -> loud warning) via the seam,
and wire the whole suite into .woodpecker/e2e.yml so it runs in CI.
- Condense the duplicated model-selection comment in the four fixed jobs to a
one-line pointer to pick-model.sh.
- PR description: state the cost / no-fixed-ceiling property change.
Deferred with rationale (in the PR thread): selected-model in the four crons'
posted artifacts (already logged per run), pagination past limit=1000 (API max;
has_more warns), and unit-testing pick-model.sh's JSON extraction (its output
always passes through the tested valid-model.sh gate, and the picker prompt is
developer-controlled, not attacker input).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
5.0 KiB
Bash
Executable File
114 lines
5.0 KiB
Bash
Executable File
#!/bin/sh
|
|
# Fetch the list of Claude models currently available to this account.
|
|
#
|
|
# The `claude` CLI has no "list models" subcommand, so the authoritative
|
|
# source is the same catalog the CLI itself uses: the Anthropic Models API
|
|
# (GET /v1/models), authenticated with the very CLAUDE_CODE_OAUTH_TOKEN the CI
|
|
# agents already run under. This lets the pipelines pick a model from what is
|
|
# live *today*, judged on the API's own capability metadata — no hardcoded
|
|
# model ids and no baked-in assumptions about model families. A model released
|
|
# tomorrow shows up here and becomes selectable the same day.
|
|
#
|
|
# Run with `sh` (POSIX; intentionally not `bash`, unlike the older
|
|
# scripts/ci/*.sh helpers invoked as `bash`).
|
|
#
|
|
# Usage:
|
|
# fetch-models.sh -> compact JSON array (newest first). No pipeline
|
|
# consumes this; it is a human/debugging affordance.
|
|
# fetch-models.sh --table -> the same data as a tab-separated table, one
|
|
# model per line: the single format every
|
|
# selector consumes (id is column 1).
|
|
#
|
|
# JSON fields / table columns (objective signals a selector can weigh):
|
|
# id, name, released=YYYY-MM-DD, context=N, max_output=N,
|
|
# effort=bool, thinking=bool, code_exec=bool
|
|
#
|
|
# Diagnostics go to stderr so `$(...)` capture stays clean.
|
|
# Exit status: 0 with non-empty output on success; non-zero on any failure
|
|
# (missing token, HTTP error, empty/garbage body) — the caller decides how
|
|
# to fall back.
|
|
set -eu
|
|
|
|
MODE="${1:-json}" # json (default) | --table
|
|
|
|
log() { echo "fetch-models: $*" >&2; }
|
|
|
|
BODY="$(mktemp)"
|
|
trap 'rm -f "$BODY"' EXIT
|
|
|
|
if [ -n "${FETCH_MODELS_BODY_FILE:-}" ]; then
|
|
# Test seam: run the exact parsing/jq below over a recorded /v1/models body,
|
|
# no token and no network. Used by scripts/ci/test-model-selection.sh to pin
|
|
# the response shape; never set in CI.
|
|
cp "$FETCH_MODELS_BODY_FILE" "$BODY"
|
|
else
|
|
TOKEN="${CLAUDE_CODE_OAUTH_TOKEN:-}"
|
|
if [ -z "$TOKEN" ]; then
|
|
log "CLAUDE_CODE_OAUTH_TOKEN is not set"
|
|
exit 1
|
|
fi
|
|
# limit=1000 (the API max): the default page size is 20 and the order is
|
|
# newest-first, so without this the OLDEST/cheapest models — the ones trivial
|
|
# work should route to — would be silently dropped once the account lists >20.
|
|
# --retry: a single transient blip should not drop the job to the CLI default;
|
|
# a hard failure still lands on the fallback. oauth-2025-04-20 is the beta
|
|
# header the subscription OAuth token is minted for; without it the API rejects
|
|
# a Bearer (as opposed to x-api-key) caller.
|
|
HTTP="$(curl -sS -o "$BODY" -w '%{http_code}' \
|
|
--max-time 20 --retry 2 --retry-connrefused \
|
|
'https://api.anthropic.com/v1/models?limit=1000' \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "anthropic-version: 2023-06-01" \
|
|
-H "anthropic-beta: oauth-2025-04-20")" || HTTP=000
|
|
if [ "$HTTP" != "200" ]; then
|
|
log "GET /v1/models returned HTTP $HTTP"
|
|
exit 1
|
|
fi
|
|
fi
|
|
# A truncated first page would mean the pipelines silently ignore older models;
|
|
# surface it rather than fail (limit=1000 makes it practically impossible).
|
|
if [ "$(jq -r '.has_more' "$BODY" 2>/dev/null)" = "true" ]; then
|
|
log "WARNING: /v1/models reported has_more=true — catalog is larger than one page"
|
|
fi
|
|
|
|
# Keep only objective signals a selector can reason from; preserve API order
|
|
# (newest first). No id patterns, no family names — just the facts the API
|
|
# reports about each model. jq stderr is surfaced so a response-schema change
|
|
# shows up as a real error, not just the generic "no usable model list".
|
|
OUT="$(jq -c '[.data[] | {
|
|
id,
|
|
name: .display_name,
|
|
released: (.created_at[0:10]),
|
|
context: .max_input_tokens,
|
|
max_output: .max_tokens,
|
|
effort: (.capabilities.effort.supported // false),
|
|
thinking: (.capabilities.thinking.supported // false),
|
|
code_exec: (.capabilities.code_execution.supported // false)
|
|
}]' "$BODY")" || { log "could not parse /v1/models response"; exit 1; }
|
|
|
|
if [ -z "$OUT" ] || [ "$OUT" = "[]" ]; then
|
|
log "response held no usable model list"
|
|
exit 1
|
|
fi
|
|
|
|
# jq does not error when a capability field simply goes missing — .max_input_tokens
|
|
# becomes null and the `// false` defaults swallow the rest. If EVERY row lost its
|
|
# context window the API shape has changed and both selectors are left with only
|
|
# name + date to reason from. Degrade rather than fail, but say so loudly.
|
|
if [ "$(printf '%s' "$OUT" | jq -r 'all(.[]; .context == null)')" = "true" ]; then
|
|
log "WARNING: no model reports a context window — /v1/models may have changed shape; capability signals are unavailable this run"
|
|
fi
|
|
|
|
case "$MODE" in
|
|
--table)
|
|
printf '%s' "$OUT" | jq -r '.[]
|
|
| "\(.id)\t\(.name)\treleased=\(.released)\tcontext=\(.context)\tmax_output=\(.max_output)\teffort=\(.effort)\tthinking=\(.thinking)\tcode_exec=\(.code_exec)"'
|
|
;;
|
|
json)
|
|
printf '%s\n' "$OUT"
|
|
;;
|
|
*)
|
|
log "unknown mode '$MODE' (use --table or nothing)"; exit 2
|
|
;;
|
|
esac
|