Retry HTTP 429 with backoff; keep models beyond first 3 as fallbacks; degrade agreement when some models fail; force-high risk only if zero usable responses. Clearer zh-CN errors/docs for dogfood uses: ./ rebase + workflow_dispatch re-run.
162 lines
5.2 KiB
Bash
Executable File
162 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Call one model via provider protocol.
|
|
# Protocols: openai | openai-completions | openai-responses | anthropic
|
|
# (openai / openai-compatible → chat completions)
|
|
# Retries HTTP 429 with exponential backoff (GATEKEEP_HTTP_RETRIES, default 3).
|
|
# Args: <provider> <model> <protocol> <base_url> <prompt.json> <out.json> <secrets.json>
|
|
set -euo pipefail
|
|
|
|
PROVIDER="${1:?provider}"
|
|
MODEL="${2:?model}"
|
|
PROTOCOL="${3:?protocol}"
|
|
BASE_URL="${4:?base_url}"
|
|
PROMPT_FILE="${5:?prompt}"
|
|
OUT_FILE="${6:?out}"
|
|
SECRETS_FILE="${7:?secrets}"
|
|
|
|
KEY=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2],""))' "$SECRETS_FILE" "$PROVIDER")
|
|
if [[ -z "$KEY" ]]; then
|
|
echo "missing key for $PROVIDER" >&2
|
|
echo "{\"error\":\"missing_key\",\"model\":\"$MODEL\",\"provider\":\"$PROVIDER\"}" > "$OUT_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
MAX_TOKENS="${GATEKEEP_MAX_TOKENS:-4096}"
|
|
RETRIES="${GATEKEEP_HTTP_RETRIES:-3}"
|
|
SYSTEM=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["system"])' "$PROMPT_FILE")
|
|
USER=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["user"])' "$PROMPT_FILE")
|
|
|
|
LABEL="${PROVIDER}:${MODEL}"
|
|
|
|
# Normalize protocol aliases
|
|
WIRE=$(python3 -c 'import sys
|
|
p=sys.argv[1].lower()
|
|
m={"openai":"openai-completions","openai-compatible":"openai-completions",
|
|
"openai-completions":"openai-completions","openai-responses":"openai-responses",
|
|
"anthropic":"anthropic"}
|
|
print(m.get(p,""))' "$PROTOCOL")
|
|
|
|
if [[ -z "$WIRE" ]]; then
|
|
echo "{\"error\":\"bad_protocol\",\"protocol\":\"$PROTOCOL\",\"model\":\"$MODEL\"}" > "$OUT_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$WIRE" == "openai-completions" ]]; then
|
|
BODY=$(python3 - "$MODEL" "$SYSTEM" "$USER" "$MAX_TOKENS" <<'PY'
|
|
import json,sys
|
|
model, system, user, max_tokens = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
|
|
print(json.dumps({
|
|
"model": model,
|
|
"temperature": 0,
|
|
"max_tokens": max_tokens,
|
|
"messages": [
|
|
{"role": "system", "content": system},
|
|
{"role": "user", "content": user},
|
|
],
|
|
}))
|
|
PY
|
|
)
|
|
URL="${BASE_URL%/}/chat/completions"
|
|
EXTRA_HDR=(-H "HTTP-Referer: https://github.com/${GITHUB_REPOSITORY:-gatekeep}" -H "X-Title: Gatekeep")
|
|
AUTH=(-H "Authorization: Bearer $KEY")
|
|
EXTRACT=openai
|
|
elif [[ "$WIRE" == "openai-responses" ]]; then
|
|
BODY=$(python3 - "$MODEL" "$SYSTEM" "$USER" "$MAX_TOKENS" <<'PY'
|
|
import json,sys
|
|
model, system, user, max_tokens = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
|
|
print(json.dumps({
|
|
"model": model,
|
|
"temperature": 0,
|
|
"max_output_tokens": max_tokens,
|
|
"input": [
|
|
{"role": "system", "content": system},
|
|
{"role": "user", "content": user},
|
|
],
|
|
}))
|
|
PY
|
|
)
|
|
URL="${BASE_URL%/}/responses"
|
|
EXTRA_HDR=()
|
|
AUTH=(-H "Authorization: Bearer $KEY")
|
|
EXTRACT=openai-responses
|
|
elif [[ "$WIRE" == "anthropic" ]]; then
|
|
BODY=$(python3 - "$MODEL" "$SYSTEM" "$USER" "$MAX_TOKENS" <<'PY'
|
|
import json,sys
|
|
model, system, user, max_tokens = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
|
|
print(json.dumps({
|
|
"model": model,
|
|
"max_tokens": max_tokens,
|
|
"temperature": 0,
|
|
"system": system,
|
|
"messages": [{"role": "user", "content": user}],
|
|
}))
|
|
PY
|
|
)
|
|
URL="${BASE_URL%/}/v1/messages"
|
|
EXTRA_HDR=(-H "anthropic-version: 2023-06-01")
|
|
AUTH=(-H "x-api-key: $KEY")
|
|
EXTRACT=anthropic
|
|
fi
|
|
|
|
attempt=0
|
|
HTTP_CODE="000"
|
|
while true; do
|
|
HTTP_CODE=$(curl -sS -o "$OUT_FILE.raw" -w "%{http_code}" \
|
|
"$URL" \
|
|
"${AUTH[@]}" \
|
|
-H "Content-Type: application/json" \
|
|
"${EXTRA_HDR[@]}" \
|
|
-d "$BODY" || echo "000")
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
break
|
|
fi
|
|
if [[ "$HTTP_CODE" == "429" && "$attempt" -lt "$RETRIES" ]]; then
|
|
sleep_s=$((2 ** attempt))
|
|
echo "$LABEL HTTP 429 — retry $((attempt + 1))/${RETRIES} after ${sleep_s}s" >&2
|
|
sleep "$sleep_s"
|
|
attempt=$((attempt + 1))
|
|
continue
|
|
fi
|
|
echo "$LABEL failed HTTP $HTTP_CODE" >&2
|
|
head -c 800 "$OUT_FILE.raw" >&2 || true
|
|
echo "{\"error\":\"http\",\"model\":\"$MODEL\",\"provider\":\"$PROVIDER\",\"http\":$HTTP_CODE}" > "$OUT_FILE"
|
|
exit 0
|
|
done
|
|
|
|
python3 - "$OUT_FILE.raw" "$OUT_FILE" "$MODEL" "$PROVIDER" "$EXTRACT" <<'PY'
|
|
import json,sys
|
|
raw=json.load(open(sys.argv[1]))
|
|
out, model, provider, kind = sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]
|
|
if kind == "anthropic":
|
|
text = "".join(
|
|
(c.get("text") or "")
|
|
for c in (raw.get("content") or [])
|
|
if c.get("type") == "text"
|
|
)
|
|
elif kind == "openai-responses":
|
|
text = raw.get("output_text") or ""
|
|
if not text:
|
|
chunks = []
|
|
for item in raw.get("output") or []:
|
|
for c in item.get("content") or []:
|
|
if c.get("type") in ("output_text", "text"):
|
|
chunks.append(c.get("text") or "")
|
|
text = "".join(chunks)
|
|
else:
|
|
text = (((raw.get("choices") or [{}])[0].get("message") or {}).get("content")) or ""
|
|
start, end = text.find("{"), text.rfind("}")
|
|
payload = {}
|
|
if start >= 0 and end > start:
|
|
try:
|
|
payload = json.loads(text[start:end+1])
|
|
except Exception as e:
|
|
payload = {"parse_error": str(e), "raw_text": text[:2000]}
|
|
else:
|
|
payload = {"parse_error": "no_json", "raw_text": text[:2000]}
|
|
payload["_model"] = f"{provider}:{model}"
|
|
payload["_provider"] = provider
|
|
json.dump(payload, open(out,"w"), indent=2)
|
|
PY
|
|
|
|
echo "$LABEL → $OUT_FILE" >&2
|