sqlite_graphrag/constants/network.rs
1//! OpenRouter endpoints and probe budgets.
2//!
3//! Split out of the former single-file `constants.rs` in v1.2.5;
4//! every item is re-exported by the parent module, so `crate::constants::X`
5//! resolves exactly as before.
6
7/// Default OpenRouter chat completions endpoint (override via XDG
8/// `network.openrouter.chat_url` or alias `network.chat_url`).
9pub const DEFAULT_OPENROUTER_CHAT_URL: &str = "https://openrouter.ai/api/v1/chat/completions";
10
11/// Default OpenRouter embeddings endpoint (override via XDG
12/// `network.openrouter.embeddings_url` or alias `network.embed_url`).
13pub const DEFAULT_OPENROUTER_EMBEDDINGS_URL: &str = "https://openrouter.ai/api/v1/embeddings";
14
15/// Fail-fast probe budget for LLM backends before spawning (ms).
16/// Override via XDG `llm.probe_timeout_ms`.
17pub const DEFAULT_LLM_PROBE_TIMEOUT_MS: u64 = 800;
18
19/// Ceiling, in seconds, on a single `Retry-After` sleep inside the HTTP retry
20/// loops of [`crate::chat_api`] and [`crate::embedding_api`].
21///
22/// The header is server-controlled and was honoured verbatim: a provider (or
23/// anything answering in its place) replying `Retry-After: 86400` put a CLI
24/// that is supposed to be born, run and die to sleep for a full day, with no
25/// output and no way to tell the stall apart from a hang.
26///
27/// The value is anchored on the budget that already governs the TOTAL wait:
28/// `enrich.rate_limit_deadline_secs` defaults to 3600s
29/// ([`crate::constants::DEFAULT_RATE_LIMIT_DEADLINE_SECS`]). One step must stay
30/// well under that or the deadline stops meaning anything — at 60s the worst
31/// case of `openrouter_http::MAX_RETRIES` rate-limited attempts is
32/// 240s, under 7% of the deadline, so the operator's budget still decides when
33/// the run gives up. It is also far above any wait a healthy provider advises,
34/// so the cap only fires on values that were never actionable anyway.
35///
36/// Distinct from [`crate::constants::ENRICH_BACKOFF_CEILING_SECS`] (900s),
37/// which bounds the drain's own backoff BETWEEN items, not one HTTP attempt.
38/// Coordination wait against a remote limit, so it takes no XDG key.
39pub const MAX_RETRY_AFTER_SECS: u64 = 60;
40
41/// Default per-item budget, in seconds, for an OpenRouter chat-completion when
42/// `--openrouter-timeout` is omitted.
43///
44/// GAP-SG-17: raised from 300 to 600 because dense bodies (close to the ~32K
45/// token context ceiling of the configured model) routinely take longer than
46/// five minutes to generate via `deepseek-v4-flash:nitro`.
47pub const DEFAULT_OPENROUTER_CHAT_TIMEOUT_SECS: u64 = 600;
48
49/// Clamps a server-advised `Retry-After` to [`MAX_RETRY_AFTER_SECS`], warning
50/// when the cap actually bites.
51///
52/// Lives beside the constant rather than in either transport because both HTTP
53/// retry loops apply the same policy; a second copy of the expression is one
54/// edit away from the chat and embedding paths disagreeing about how long a
55/// remote limit may stall a one-shot process.
56///
57/// The warning is not decoration: a cap that trims in silence is
58/// indistinguishable from a provider that asked for the shorter wait, so the
59/// operator would read the retries as normal pacing instead of as a provider
60/// demanding a delay this CLI refuses to grant.
61pub fn clamp_retry_after_secs(requested: u64) -> u64 {
62 let applied = requested.min(MAX_RETRY_AFTER_SECS);
63 if applied < requested {
64 tracing::warn!(
65 requested_secs = requested,
66 applied_secs = applied,
67 "Retry-After exceeds the local ceiling and was capped; \
68 a one-shot process must not sleep on a server's word alone"
69 );
70 }
71 applied
72}
73
74#[cfg(test)]
75mod retry_after_ceiling_tests {
76 use super::{clamp_retry_after_secs, MAX_RETRY_AFTER_SECS};
77
78 #[test]
79 fn an_absurd_retry_after_is_capped() {
80 // 86400 is the measured shape of the defect: one header value put the
81 // process to sleep for a day inside a born-run-die CLI.
82 assert_eq!(clamp_retry_after_secs(86_400), MAX_RETRY_AFTER_SECS);
83 assert_eq!(
84 clamp_retry_after_secs(u64::MAX),
85 MAX_RETRY_AFTER_SECS,
86 "the cap must hold for any value the header can carry"
87 );
88 }
89
90 #[test]
91 fn a_reasonable_retry_after_passes_through_untouched() {
92 for requested in [0, 1, 2, 30, MAX_RETRY_AFTER_SECS] {
93 assert_eq!(
94 clamp_retry_after_secs(requested),
95 requested,
96 "a wait a healthy provider advises must be honoured verbatim"
97 );
98 }
99 }
100}