# M1.1 — `mem-llm` chat client | Field | Value | |---|---| | Phase | M1 — Gated loop at L1 | | Size | M — 1–3 days | | Status | ⬜ Not started | | Flags | — | | Spec | inlined below | | Blocks | M0.1 | ## Goal Talk to the homelab gateway, with the two non-obvious details that cost a day to find already baked in. ## Facts (inlined — no spec read needed) ``` base https://api.riotpiao.com/v1 route POST /v1/qwen/chat/completions qwen2.5:3b-instruct POST /v1/ornith/chat/completions ornith:35b POST /v1/reasoning/chat/completions DeepSeek-R1-Distill-32B (no tools) ``` **Auth is `apikey:`, not `Authorization: Bearer`.** Kong's `key-auth` compares the whole header value against the stored key, so the OpenAI SDK convention returns 401. Verified: ``` -H "apikey: $KEY" -> 200 -H "Authorization: $KEY" -> 200 -H "Authorization: Bearer $KEY" -> 401 ``` **One provider per route.** `baseUrl` is per-route and the model id is `ornith:35b` with the tag, not `ornith`. `/v1/models` advertises `/v1/score` which does not work — do not trust that list as a capability probe. Request bodies above ~10.6 KB used to fail with `{"error":{"message":"[] is too short - 'messages'"}}`; the Kong body buffer was raised to 16m and it is fixed. If that error ever reappears, it is the buffer, not the client. Send **no `tools` array**. The controller needs none, and the reasoning route rejects any request carrying one. ## Steps 1. `ChatClient::new(base_url, api_key, model)` in `mem-llm`. 2. Send the `apikey` header. Read the key from `MEM_API_KEY`, never from a committed file. 3. `complete(system, user, max_tokens) -> Completion { text, usage, latency }`. 4. Timeout default 300s — local models are slow to first token and a cold load can take minutes. 5. Retry on 5xx and timeout with exponential backoff, max 3. **Do not retry 4xx** — a 400 is a malformed request and retrying it just costs three times as much. 6. On any error, include the response body in the error. The useful information is always in the body, never the status. 7. `MEM_LLM_RECORD=