LLM::HttpTransport
Shared HTTP transport for the provider clients (OpenAI-compatible and Ollama).
Both clients used to post through the bare one-shot helpers
(HTTP::Client.post / Crest.post), which set no timeouts at all: a
provider that accepts the connection and then never answers — a wedged
local ollama serve, a proxy that black-holes the request, a hosted
endpoint that stalls mid-generation — hung the scan forever, with no
output and no indication of what it was waiting on. Requests now fail
after a bounded wait.
It also retries the transient failures every hosted provider produces (429 rate limits, 502/503 from a load balancer, a dropped socket). Without that, one blip turned into a silent zero-endpoint AI result: the clients map any failure to "", which the analyzer treats as "this code defines no endpoints".
Constants
Connecting is fast even for remote providers, so a short budget here only shortens the "server isn't running" feedback loop that local provider users (ollama, vLLM, LM Studio) hit most often.
Generation is not fast: a large bundle against a CPU-bound local model legitimately takes minutes, so the read budget is generous and tunable rather than tight.
Token counts as the provider reports them: OpenAI-style usage
(prompt_tokens / completion_tokens, or Anthropic-style
input_tokens / output_tokens) and Ollama's eval counts. Read
with a regex rather than parsed: the model's own text is a JSON
string, where these keys would appear with escaped quotes.
Cap on how long a provider's Retry-After may park the scan.
Status codes worth another attempt: rate limits plus the transient gateway/overload family. A 400/401/404 is a configuration problem that retrying can only make slower.
Class methods
1s, then 2s — bounded, and short enough that exhausting all attempts still returns while the caller is waiting.
The key a request carries: a bearer token, or Anthropic's x-api-key.
Reads a timeout override, in seconds. Anything that isn't a positive
number (empty, 0, a word) falls back to the default instead of
disabling the timeout — the point of the override is to move the
bound, not to remove it.
--ai-max-requests: a hard bound on how many HTTP requests (retries
included) one run may send. 0 is unlimited. A request past the cap
fails like any other, so the files it was for are listed as coverage
gaps instead of silently dropped.
--ai-max-requests: a hard bound on how many HTTP requests (retries
included) one run may send. 0 is unlimited. A request past the cap
fails like any other, so the files it was for are listed as coverage
gaps instead of silently dropped.
ponytail: suffix and * entries only; CIDR ranges are not matched.
POSTs a JSON body and returns the response body, or nil when the request could not be completed. Failures are reported here so every provider path surfaces them the same way instead of each client inventing its own (or, in Ollama's case, staying silent).
post_json, except a rejection is returned unreported, for a caller
that can recover from some of them.
The proxy HTTPS_PROXY / HTTP_PROXY name for this request (lowercase
wins, as in curl), or nil to connect directly. Loopback and NO_PROXY
hosts always go direct: a local model behind a corporate proxy
variable must stay reachable.
A read/write timeout runs out the long budget: usually the model was still generating, and another attempt waits it out again and may pay for the same generation twice. Only a connect timeout is worth retrying. Matched on the message because the proxy shard re-wraps the error as a plain IO::Error ("... (Read timed out)"). ponytail: event loops word it "Connect timed out" / "connect timed out"; one that words it differently just loses the connect retry.
False once --ai-max-requests is used up: a skipped request, not a provider failure.
A provider that tells us when to come back (429s usually do) knows better than the fixed backoff, as long as it stays within our cap. Either form the RFC allows: delay-seconds or an HTTP-date.
Every provider error text that reaches stderr goes through here, so it is also where the key is masked — before the cut, so a key straddling it cannot leak its prefix.
One line for the end of a run, nil when it made no AI requests.
A key over plain http:// to another machine is readable by anything on the path. Local servers (ollama, vLLM, LM Studio) are the normal http case and stay quiet.