class

LLM::General

Inherits Reference < Object

General OpenAI-compatible LLM client

Constants

CONTEXT_LIMIT = [/maximum context length is (\d+)/i, /> ?(\d+) maximum/i, /maximum number of tokens allowed \((\d+)\)/i, /context length of only (\d+)/i, /limit of (\d+) tokens/i]

How OpenAI, vLLM, Anthropic, Gemini and LM Studio state the window ...

CONTEXT_OVERFLOW = /context_length_exceeded|context length|context window|prompt is too long|maximum number of tokens|too many (?:input )?tokens|exceed[s]? the (?:configured )?limit/i
CONTEXT_USED = [/resulted in (\d+) tokens/i, /prompt is too long: (\d+) tokens/i, /input token count \((\d+)\)/i]

... and the prompt's real size, which corrects noir's chars/4 estimate.

FATAL_STATUS = Set {401, 403, 404}
FIXED_SAMPLING_MODEL = /\A(?:o\d+(?:-|\z)|gpt-5(?![\w.-]*-chat)|claude-(?:opus-4-[7-9]|(?:opus|sonnet|haiku)-[5-9]|fable|mythos))/

Reasoning models answer a non-default temperature with HTTP 400: OpenAI's o-series and GPT-5 family (its -chat models excepted), and Claude from Opus 4.7 / Sonnet 5 / Haiku 5.5 on (Fable and Mythos included). post recovers from that rejection for any model, so this list only saves the first wasted request for the models it names.

JSON_OBJECT_FORMAT = JSON.parse("{\"type\":\"json_object\"}")
MAX_FATAL_STREAK = 3

A bad key, a missing model, a wrong URL or an unreachable host fails every request the same way. After this many in a row, before the provider has answered once, the client stops sending, so a 1000-file scan does not repeat the same failure 1000 times; the files it skips are reported as unanalyzed by the caller. Once it has answered, a failure is an outage or a slow prompt (a read timeout looks the same as a refused connection here) and every request keeps its own chance.

PRESETS = {"openai" => "https://api.openai.com/v1/chat/completions", "ollama" => "http://localhost:11434/v1/chat/completions", "lmstudio" => "http://localhost:1234/v1/chat/completions", "xai" => "https://api.x.ai/v1/chat/completions", "vllm" => "http://localhost:8000/v1/chat/completions", "azure" => "https://models.inference.ai.azure.com/chat/completions", "github" => "https://models.github.ai/inference/chat/completions", "openrouter" => "https://openrouter.ai/api/v1/chat/completions", "gemini" => "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", "anthropic" => "https://api.anthropic.com/v1/messages"}

--ai-provider names that stand for a fixed endpoint. CLI validation rejects any other bare name, so a typo fails before the scan.

Constructors

new(url : String, model : String, api_key : String | Nil)
Source

Class methods

api_url(url : String) : String

The request URL for a provider given as a URL; LLM::Anthropic resolves it to the Messages endpoint instead.

Source
chat_completions_url(url : String) : String

Decided on the URI path, not the whole string: an Azure-style ...?api-version=2024-02-01 query used to get /chat/completions appended after it.

Source
collect_stream(raw : String) : JSON::Any

A streamed reply folded back into the non-streamed shape the callers read. A body that is not SSE (a server that ignored stream, or an HTML page) is parsed as-is so it fails loudly, and an error event is returned as the whole reply. A stream that ends without a finish reason was cut off, and says so.

Source
context_overflow(rejection : HttpTransport::Rejection) : ContextOverflow | Nil

nil when the rejection is not a context overflow. A 413 is one even with no wording: a reverse proxy's body-size cap answers in HTML.

Source
extract_agent_action(response_json : JSON::Any) : String

Parse provider response into normalized JSON action payload. If the model returns tool_calls, convert them to: {"action":"<function_name>","args":{...}} Otherwise, return cleaned textual content as-is.

Source
parse_tools_cached(tools : String) : JSON::Any
Source
response_format_rejected?(rejection : HttpTransport::Rejection) : Bool

A server that implements only json_object, or no structured output at all, answers every json_schema request with a 400 naming it.

Source
sampling_temperature?(model : String) : Bool
Source
seed_rejected?(rejection : HttpTransport::Rejection) : Bool
Source
stream=(stream : Bool)

--ai-stream: endpoint-extraction requests ask for SSE, so a proxy that drops idle connections sees bytes while the model generates.

Source
stream?

--ai-stream: endpoint-extraction requests ask for SSE, so a proxy that drops idle connections sees bytes while the model generates.

Source
temperature_rejected?(rejection : HttpTransport::Rejection) : Bool

The 400 for a rejected sampling parameter (temperature, or the seed from --ai-seed): OpenAI names it in param (unsupported_value), Anthropic in the message. Only the error itself is read; a gateway that echoes the request would otherwise match on every 400.

Source

Instance methods

request(prompt : String, format : String = "json")

Make a simple request with a single prompt

Source
request_messages(messages : Array(Hash(String, String)), format : String = "json", raise_overflow : Bool = false)

Make a request with chat-style messages raise_overflow turns a context-window rejection into ContextOverflow instead of "" (see post).

Source
request_messages_with_tools(messages : Array(Hash(String, String)), tools : String)

Request next action with provider-native tool-calling. tools must be a JSON array string compatible with OpenAI-style chat completions API.

Source

Nested types