#!/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: 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