Skip to main content

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
44mod client;
45mod completion;
46mod error;
47mod transport;
48mod wire;
49
50// Split by responsibility. Every public item is re-exported here, so
51// `crate::chat_api::OpenRouterChatClient`, `ChatCompletion` and `ChatError`
52// keep resolving exactly as before for every caller inside and outside this
53// module.
54pub use completion::ChatCompletion;
55pub use error::ChatError;
56
57/// Process-wide OpenRouter chat client. Holds the model name so that callers
58/// only thread the per-item prompt/schema/input through [`Self::complete`].
59pub struct OpenRouterChatClient {
60    client: reqwest::Client,
61    api_key: SecretBox<String>,
62    model: String,
63    /// Endpoint each request is POSTed to. Resolved from XDG/config at
64    /// construction (default: [`DEFAULT_OPENROUTER_CHAT_URL`](crate::constants::DEFAULT_OPENROUTER_CHAT_URL)).
65    base_url: String,
66}
67
68#[cfg(test)]
69#[path = "../chat_api_tests.rs"]
70mod tests;