sqlite_graphrag/chat_api/mod.rs
1//! HTTP client for the OpenRouter chat-completions API.
2//!
3//! Sends structured-output chat requests to the OpenAI-compatible endpoint
4//! at `openrouter.ai/api/v1/chat/completions` and returns the parsed JSON
5//! object the model produced under a strict `json_schema` `response_format`.
6//!
7//! This mirrors [`crate::embedding_api`] for the embeddings endpoint: same
8//! retry/backoff policy (immediate abort on 401/400/404, `retry-after` on
9//! 429, exponential backoff + jitter on 5xx) and the same minimal headers
10//! (only `Authorization: Bearer`, no `HTTP-Referer`/`X-Title`). The shared
11//! error envelope and backoff helper live in [`crate::openrouter_http`]
12//! (GAP-SG-74). The submodule layout mirrors it too: `wire` holds the serde
13//! shapes, `error` the failure type, `transport` the retry loop, `client`
14//! the call surface and `completion` the response finalisation.
15//!
16//! v1.0.95 (ADR-0054): adds an OpenRouter REST transport for the `enrich`
17//! JUDGE so structured extraction no longer requires a locally installed
18//! `claude` / `codex` / `opencode` CLI subprocess.
19//!
20//! v1.1.00 (GAP-SG-70/72-chat): the OpenAI-compatible contract surfaces
21//! `choices[].finish_reason` and `usage.{prompt_tokens,completion_tokens}`.
22//! `finish_reason == "length"` means the response was truncated because
23//! `max_tokens` was too small — not a malformed generation.
24//! [`OpenRouterChatClient::complete`](crate::chat_api::OpenRouterChatClient::complete)
25//! now detects this BEFORE attempting JSON repair, grows `max_tokens` and
26//! re-issues the request (bounded by
27//! [`crate::constants::ENRICH_MAX_LENGTH_RETRIES`]), and always reports the
28//! diagnostics (`finish_reason`, token counts) to the caller via
29//! [`ChatCompletion`](crate::chat_api::ChatCompletion) on success or
30//! [`ChatError`](crate::chat_api::ChatError) on failure.
31
32use secrecy::SecretBox;
33
34// GAP-SG-17: raised from 300 to 600 — the per-request fallback budget when a
35// caller passes `0`. Dense bodies near the model's ~32K-token context ceiling
36// regularly need more than five minutes to generate.
37const DEFAULT_TIMEOUT_SECS: u64 = 600;
38const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 10;
39
40/// Fixed `json_schema` name sent in the `response_format`. OpenRouter only
41/// requires a short identifier; the actual contract is carried by `schema`.
42const SCHEMA_NAME: &str = "enrich_output";
43
44/// Sampling temperature for every request this client makes (G-PR-7).
45///
46/// All of them are extraction and classification over evidence the caller
47/// already holds, so the useful output is the one the evidence determines.
48/// Before this constant the field was absent from the request entirely and
49/// each provider applied its own default, which for most is 1.0.
50const EXTRACTION_TEMPERATURE: f64 = 0.0;
51
52mod client;
53mod completion;
54mod error;
55mod transport;
56mod wire;
57
58// Split by responsibility. Every public item is re-exported here, so
59// `crate::chat_api::OpenRouterChatClient`, `ChatCompletion` and `ChatError`
60// keep resolving exactly as before for every caller inside and outside this
61// module.
62pub use completion::ChatCompletion;
63pub use error::ChatError;
64
65/// Process-wide OpenRouter chat client. Holds the model name so that callers
66/// only thread the per-item prompt/schema/input through [`Self::complete`].
67pub struct OpenRouterChatClient {
68 client: reqwest::Client,
69 api_key: SecretBox<String>,
70 model: String,
71 /// Endpoint each request is POSTed to. Resolved from XDG/config at
72 /// construction (default: [`DEFAULT_OPENROUTER_CHAT_URL`](crate::constants::DEFAULT_OPENROUTER_CHAT_URL)).
73 base_url: String,
74}
75
76#[cfg(test)]
77#[path = "../chat_api_tests.rs"]
78mod tests;