From 6a93e3db3c5ae9e2fa64ed7f5c4ded15378c2faa Mon Sep 17 00:00:00 2001 From: Daveswo <969dwi@gmail.com> Date: Tue, 18 Aug 2026 00:37:28 -0400 Subject: [PATCH] Add 80-remote-api.sh: opt-in remote LLM API fallback source (OpenAI-compatible) --- contrib/fx/sources.d/80-remote-api.sh | 92 +++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100755 contrib/fx/sources.d/80-remote-api.sh diff --git a/contrib/fx/sources.d/80-remote-api.sh b/contrib/fx/sources.d/80-remote-api.sh new file mode 100755 index 0000000..fc44d4d --- /dev/null +++ b/contrib/fx/sources.d/80-remote-api.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# Remote-API fallback source: queries any OpenAI-compatible chat +# completions endpoint (OpenAI itself, OpenRouter, Groq, Together, +# Anthropic's OpenAI-compatibility layer, a company-internal proxy, +# etc.) for a fix, only when nothing cheaper -- including the free +# local model in 70-local-llm.sh -- already matched. +# +# Off by default. Requires FX_REMOTE_API_URL, FX_REMOTE_API_KEY, and +# FX_REMOTE_API_MODEL to all be set explicitly; no default model is +# assumed, since guessing one could silently pick something you don't +# want to pay for. Silently no-ops if any of the three is missing. +# +# Numbered 80: after the free local-LLM source (70) and before +# 90-team-shared.sh, which is a human-curated source and gets the final +# word. This is the most expensive tier in latency and real dollar +# cost, so it should only ever be the last thing tried. +# +# TRUST WARNING, sharper than every other source here: this is the one +# tier where the text you'd be eval'ing was not authored by you, a tool +# you run, or a URL you personally chose to trust -- it's a third-party +# service's best guess. fx.sh already labels every candidate with its +# source filename in the picker ("[80-remote-api.sh] ..."), so this is +# never visually indistinguishable from a self-diagnosed or locally- +# verified fix. Read that label before accepting. + +cmd="$1" +[ -n "$cmd" ] || exit 0 +command -v python3 >/dev/null 2>&1 || exit 0 + +[ -n "$FX_REMOTE_API_URL" ] || exit 0 +[ -n "$FX_REMOTE_API_KEY" ] || exit 0 +[ -n "$FX_REMOTE_API_MODEL" ] || exit 0 + +# Some providers want "Authorization: Bearer " (OpenAI, OpenRouter, +# Groq, ...), others want a bare custom header (Anthropic's native API +# uses "x-api-key: " with no prefix). Both are configurable so this +# isn't hardcoded to one vendor's auth scheme. +AUTH_HEADER="${FX_REMOTE_API_AUTH_HEADER:-Authorization}" +AUTH_PREFIX="${FX_REMOTE_API_AUTH_PREFIX-Bearer }" + +python3 - "$cmd" "$FX_OUTPUT" "$FX_REMOTE_API_URL" "$FX_REMOTE_API_KEY" "$FX_REMOTE_API_MODEL" "$AUTH_HEADER" "$AUTH_PREFIX" <<'PYEOF' +import sys, json, urllib.request, urllib.error + +cmd, output, url, key, model, auth_header, auth_prefix = sys.argv[1:8] + +PROMPT = f"""You are suggesting a fix for a failed shell command, to be taught to a self-healing shell function called f(). +Rules: +- f() will eval your fix ONCE as a precondition, then re-run the ORIGINAL command verbatim. Your fix never replaces the command; it only sets up state so the original command then succeeds. +- If the fix should generalize beyond this one invocation, define a shell function with the same name as the failing program, wrapping the real binary via `command "$@"`, and only special-case the failure inside it. +- Respond with EXACTLY ONE LINE of POSIX shell and nothing else: no explanation, no markdown, no code fences, no commentary. + +Failing command: {cmd} + +Its output: +{output} +""" + +payload = { + "model": model, + "messages": [{"role": "user", "content": PROMPT}], + "temperature": 0, + "max_tokens": 200, +} + +req = urllib.request.Request( + url.rstrip("/") + "/chat/completions" if not url.rstrip("/").endswith("/chat/completions") else url, + data=json.dumps(payload).encode(), + headers={ + "Content-Type": "application/json", + auth_header: f"{auth_prefix}{key}", + }, + method="POST", +) + +try: + with urllib.request.urlopen(req, timeout=30) as r: + resp = json.loads(r.read().decode()) +except (urllib.error.URLError, TimeoutError, OSError, ValueError): + sys.exit(0) + +choices = resp.get("choices") or [] +if not choices: + sys.exit(0) +content = (choices[0].get("message", {}).get("content") or "").strip() +if not content: + sys.exit(0) + +# Enforce the one-line contract even if the model ignores instructions. +first_line = content.splitlines()[0].strip().strip("`") +if first_line: + print(first_line) +PYEOF