ai-client.md 5.2 KB


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:

  • id: openwiki-source-1b05507d70 resource: repo://src/ai.rs
  • id: openwiki-source-2a737474d8 resource: repo://src/config.rs generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" } verified:
  • by: openwiki/0.7.2 at: 2026-10-19T19:53:56.480Z ---

AI chat-completions client

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.

Types

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

Construction

AiClient::new(&Config) (src/ai.rs:123-146):

  • 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;
  • sets the log path to 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).

Text completions

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

Vision completions

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

  1. fails with modello vision non configurato without a vision model;
  2. validates the image (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);
  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:274-290).

The image is embedded as a data:<mime>;base64,<...> URL (src/ai.rs:297-304).

Sending and error handling

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.

Optional request log

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.

  • Image data URLs are replaced by data:<mime>;base64,<omessi N byte> (redact_images / redact_data_url, src/ai.rs:356-397); text parts and remote URLs are kept.
  • The API key is never written (it is only in the bearer header, not in the body).
  • The file is opened in append mode and created if missing (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.

Reply parsing

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.

Tests

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.