type: integration title: AI chat-completions client description: The OpenAI-compatible HTTP client in src/ai.rs — request building, vision image input and validation, timeouts, error handling and reply parsing. tags: [ai, openai, reqwest, vision, http] sources:
1b05507d70
resource: repo://src/ai.rs2a737474d8
resource: repo://src/config.rs
generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
verified:src/ai.rs implements AiClient, a small client for any
OpenAI-compatible POST <base_url>/chat/completions endpoint. It is used by
the AI autoreply workflow; the conversation
turns themselves are built elsewhere.
Role (src/ai.rs:38-58): System, User, Assistant, serialised as
"system", "user", "assistant".ChatTurn { role, content } (src/ai.rs:60-65): one message of the
conversation.ImageInput { mime, bytes } (src/ai.rs:95-100): an image for vision
requests.AiClient (src/ai.rs:67-77): reqwest::Client, endpoint, API key,
model, optional vision model and optional log path.Both AiClient and ImageInput have hand-written Debug impls that omit the
API key and the image bytes (src/ai.rs:79-114).
AiClient::new(&Config) (src/ai.rs:123-146):
openai_api_key; otherwise fails with
openai_api_key mancante in config.toml;REQUEST_TIMEOUT_SECONDS);openai_base_url (trailing / trimmed) +
/chat/completions;openai_model and openai_vision_model;Config::ai_log_path() only when ai_log is true.No network traffic happens until a completion is requested. Defaults and
config keys are described in
Configuration and data
(default base URL https://ollama.com/v1, default model
deepseek-v4.1-flash, src/config.rs:23-25).
complete(&turns) (src/ai.rs:155-157) sends
{ "model": "<model>", "messages": [{ "role": "...", "content": "..." }] }
built by build_request_body / build_messages (src/ai.rs:251-265).
has_vision() (src/ai.rs:163-165) is true when a vision model is
configured. complete_with_image(&turns, &image) (src/ai.rs:175-188):
modello vision non configurato without a vision model;src/ai.rs:311-323): empty → immagine vuota,
larger than MAX_IMAGE_BYTES (5 MiB) → immagine troppo grande: N byte
(errors mention the size only, never the data);[{"type":"text",...}, {"type":"image_url",...}]; with no user turn, a
user message holding only the image is appended
(src/ai.rs:274-290).The image is embedded as a data:<mime>;base64,<...> URL
(src/ai.rs:297-304).
send_body (src/ai.rs:200-209) delegates to request_reply
(src/ai.rs:217-243), which posts JSON with bearer auth and returns
Italian anyhow errors:
| Failure | Message |
|---|---|
| Transport error | richiesta al servizio AI fallita |
| Body read error | lettura della risposta del servizio AI fallita |
| Non-2xx status | servizio AI ha risposto <status>: <body> |
| Invalid JSON | risposta del servizio AI non valida: <body> |
Response bodies in errors are truncated to 300 characters on a character
boundary (truncate_chars, src/ai.rs:448-450); the API key is never
included.
When ai_log = true in config.toml (default false), send_body appends
one record per request to ai_requests.log in the data directory
(Config::ai_log_path, src/config.rs:283-285) after the request completes
(src/ai.rs:200-209). format_log_record (src/ai.rs:406-421) writes a
header ===== <local timestamp> | <endpoint> | modello: <model> =====, the
pretty-printed request body and the outcome labelled RISPOSTA or ERRORE.
data:<mime>;base64,<omessi N byte>
(redact_images / redact_data_url, src/ai.rs:356-397); text parts and
remote URLs are kept.append_to_file, src/ai.rs:436-442); write failures are ignored so
logging never makes a reply fail and never writes over the TUI.parse_reply (src/ai.rs:331-348) takes choices[0].message.content,
trims it and fails with risposta del servizio AI senza choices when there
are no choices or risposta del servizio AI vuota when the content is
missing or blank.
src/ai.rs:452-739 covers vision body shapes, missing vision model,
oversize/empty images (without leaking data), the exact size limit,
has_vision, Debug redaction, request body shape, reply parsing edge
cases, the missing-key error, endpoint construction, truncation, and the
request log (image redaction, no key or base64 in records, logging only when
enabled). See
Unit tests.