module

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

CONNECT_TIMEOUT_ENV = "NOIR_AI_CONNECT_TIMEOUT"
DEFAULT_CONNECT_TIMEOUT = 10.seconds

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.

DEFAULT_TIMEOUT = 300.seconds

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.

MAX_ATTEMPTS = 3
MAX_ERROR_SNIPPET_SIZE = 1024
MAX_RETRY_AFTER = 30.seconds

Cap on how long a provider's Retry-After may park the scan.

RETRYABLE_STATUS = Set {408, 425, 429, 500, 502, 503, 504, 529}

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.

TIMEOUT_ENV = "NOIR_AI_TIMEOUT"

Class methods

backoff(attempt : Int32) : Time::Span

1s, then 2s — bounded, and short enough that exhausting all attempts still returns while the caller is waiting.

Source
connect_timeout
Source
duration_from_env(name : String) : Time::Span | Nil

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.

Source
post_json(url : String, body : String, headers : HTTP::Headers) : String | Nil

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).

Source
retry_after(response : HTTP::Client::Response | Nil) : Time::Span | Nil

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.

Source
retry_delay(response : HTTP::Client::Response | Nil, attempt : Int32) : Time::Span
Source
retryable_status?(code : Int32) : Bool
Source
timeout
Source
truncate_error_snippet(body : String) : String
Source