--- 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: - by: openwiki/0.7.2 at: 2026-10-19T18:58:46.219Z sources: - id: openwiki-source-1b05507d7085e8e99a9b64f1 resource: repo://src/ai.rs - id: openwiki-source-2a737474d86fc75cc9d9694f resource: repo://src/config.rs generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" } --- # AI chat-completions client `src/ai.rs` implements `AiClient`, a small client for any OpenAI-compatible `POST /chat/completions` endpoint. It is used by the [AI autoreply workflow](../workflows/ai-autoreply.md); the conversation turns themselves are built elsewhere. ## Types - `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`). ## Construction `AiClient::new(&Config)` (`src/ai.rs:106-128`): - requires `openai_api_key`; otherwise fails with `openai_api_key mancante in config.toml`; - builds the HTTP client with a 60 s request timeout (`REQUEST_TIMEOUT_SECONDS`); - builds the endpoint as `openai_base_url` (trailing `/` trimmed) + `/chat/completions`; - copies `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](../operations/configuration-and-data.md) (default base URL `https://ollama.com/v1`, default model `deepseek-v4.1-flash`, `src/config.rs:23-25`). ## Text completions `complete(&turns)` (`src/ai.rs:137-139`) sends ```json { "model": "", "messages": [{ "role": "...", "content": "..." }] } ``` built by `build_request_body` / `build_messages` (`src/ai.rs:213-227`). ## Vision completions `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`): 1. fails with `modello vision non configurato` without a vision model; 2. validates the image (`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); 3. sends a body using the vision model where the **last user turn** becomes `[{"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:;base64,<...>` URL (`src/ai.rs:259-266`). ## Sending and error handling `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 : ` | | Invalid JSON | `risposta del servizio AI non valida: ` | 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. ## Reply parsing `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. ## Tests `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](../testing/unit-tests.md).