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] verified:
1b05507d70
resource: repo://src/ai.rs2a737474d8
resource: repo://src/config.rs
generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" }
---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:26-44): System, User, Assistant, serialised as
"system", "user", "assistant".ChatTurn { role, content } (src/ai.rs:48-51): one message of the
conversation.ImageInput { mime, bytes } (src/ai.rs:80-83): an image for vision
requests.AiClient (src/ai.rs:55-61): reqwest::Client, endpoint, API key,
model and optional vision model.Both AiClient and ImageInput have hand-written Debug impls that omit the
API key and the image bytes (src/ai.rs:63-97).
AiClient::new(&Config) (src/ai.rs:106-128):
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.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:137-139) sends
{ "model": "<model>", "messages": [{ "role": "...", "content": "..." }] }
built by build_request_body / build_messages (src/ai.rs:213-227).
has_vision() (src/ai.rs:145-147) is true when a vision model is
configured. complete_with_image(&turns, &image) (src/ai.rs:157-170):
modello vision non configurato without a vision model;src/ai.rs:273-285): 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:236-252).The image is embedded as a data:<mime>;base64,<...> URL
(src/ai.rs:259-266).
send_body (src/ai.rs:179-205) 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:316-318); the API key is never
included.
parse_reply (src/ai.rs:293-310) 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:320-527 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 and truncation. See
Unit tests.