1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
//! Regression tests for `handshake_timeout_for`.
//!
//! Locks in the 2026-04-25 fix where NVIDIA's wedged
//! `integrate.api.nvidia.com` ate ~3 minutes (3 × 90s) of retry budget
//! before falling back. Cloud HTTP now bails after 60s (bumped from 30s
//! on 2026-05-07 because routing proxies like dialagram legitimately
//! need 20-45s when upstream is slow), while local HTTP keeps the longer
//! 90s window for cold-loading models.
use crate::brain::agent::service::helpers::{
cli_startup_timeout_for, handshake_timeout_for, send_handshake_timeout_for,
};
use std::time::Duration;
#[test]
fn cli_providers_get_ten_minutes() {
// Subprocess startup + auth refresh dominate; the URL — if any —
// doesn't matter.
assert_eq!(handshake_timeout_for(true, None), Duration::from_secs(600));
assert_eq!(
handshake_timeout_for(true, Some("http://localhost:1234/v1/chat/completions")),
Duration::from_secs(600),
);
assert_eq!(
handshake_timeout_for(true, Some("https://api.openai.com/v1/chat/completions")),
Duration::from_secs(600),
);
}
#[test]
fn local_http_gets_ninety_seconds() {
// Cold-loading a large gguf or MLX checkpoint can take 30+ seconds.
for url in [
"http://localhost:1234/v1/chat/completions",
"http://127.0.0.1:8080/v1/chat/completions",
"http://[::1]:11434/api/chat",
"http://0.0.0.0:8000/v1/chat/completions",
"http://192.168.1.42:11434/api/chat",
"http://10.0.0.5:8080/v1",
"http://172.20.0.10:8000/v1",
"http://my-rig.local:1234/v1/chat/completions",
] {
assert_eq!(
handshake_timeout_for(false, Some(url)),
Duration::from_secs(90),
"expected 90s for local URL: {}",
url,
);
}
}
#[test]
fn cloud_http_gets_sixty_seconds() {
// Routing proxies (dialagram, openrouter) can take 20-45s when
// upstream is slow. Wedged servers are >120s, so 60s still catches
// real hangs in under 3 min via the retry chain. Previous 30s killed
// legitimate requests to slower-but-healthy providers.
for url in [
"https://integrate.api.nvidia.com/v1/chat/completions",
"https://api.openai.com/v1/chat/completions",
"https://api.anthropic.com/v1/messages",
"https://api.z.ai/api/coding/paas/v4/chat/completions",
"https://opencode.ai/zen/go/v1/chat/completions",
"https://openrouter.ai/api/v1/chat/completions",
"https://api.minimax.io/v1/chat/completions",
"https://api.moonshot.ai/v1/chat/completions",
] {
assert_eq!(
handshake_timeout_for(false, Some(url)),
Duration::from_secs(60),
"expected 60s for cloud URL: {}",
url,
);
}
}
#[test]
fn missing_base_url_defaults_to_cloud_timeout() {
// Providers without a base_url (built-in Anthropic/Gemini that
// hardcode their endpoints internally) are always cloud — they
// can't be a local LM server.
assert_eq!(handshake_timeout_for(false, None), Duration::from_secs(60));
}
// ---------------------------------------------------------------------------
// The PER-SEND budget (#680/#682).
//
// `handshake_timeout_for` is the class table. `send_handshake_timeout_for` is
// what each HTTP provider applies at its own `.send()`, and it must NOT return
// a budget for a CLI provider: those spawn a subprocess rather than sending
// HTTP, so a send budget is meaningless.
// ---------------------------------------------------------------------------
#[test]
fn cli_providers_get_no_per_send_budget() {
assert_eq!(send_handshake_timeout_for(true, None), None);
assert_eq!(
send_handshake_timeout_for(true, Some("https://api.openai.com/v1/chat/completions")),
None,
);
}
#[test]
fn http_providers_get_the_class_budget_per_send() {
assert_eq!(
send_handshake_timeout_for(false, Some("https://api.openai.com/v1/chat/completions")),
Some(Duration::from_secs(60)),
);
assert_eq!(
send_handshake_timeout_for(false, Some("http://127.0.0.1:8080/v1/chat/completions")),
Some(Duration::from_secs(90)),
);
// No base_url: the built-in Anthropic/Gemini providers hardcode cloud
// endpoints, so they take the cloud budget.
assert_eq!(
send_handshake_timeout_for(false, None),
Some(Duration::from_secs(60)),
);
}
// ---------------------------------------------------------------------------
// The CALLER-LEVEL wall (#680). This is the leg that keeps the two helpers from
// being swapped: an HTTP provider must get NO wall around `provider.stream()`
// (its budget lives at the send), while a CLI provider — a subprocess, with no
// send to bound — must keep its process-startup wall, and this level is the
// only place that can carry it.
// ---------------------------------------------------------------------------
#[test]
fn caller_wall_is_cli_only() {
assert_eq!(
cli_startup_timeout_for(true, None),
Some(Duration::from_secs(600)),
);
assert_eq!(
cli_startup_timeout_for(true, Some("https://api.openai.com/v1/chat/completions")),
Some(Duration::from_secs(600)),
);
assert_eq!(cli_startup_timeout_for(false, None), None);
assert_eq!(
cli_startup_timeout_for(false, Some("https://api.openai.com/v1/chat/completions")),
None,
);
assert_eq!(
cli_startup_timeout_for(false, Some("http://127.0.0.1:8080/v1/chat/completions")),
None,
);
}
/// The two helpers partition the provider space: exactly one of them yields a
/// wall for any given provider, never both and never neither.
#[test]
fn send_and_caller_walls_never_both_apply() {
let bases = [
None,
Some("https://api.openai.com/v1/chat/completions"),
Some("http://127.0.0.1:8080/v1/chat/completions"),
];
for cli in [true, false] {
for base in bases {
let send = send_handshake_timeout_for(cli, base);
let caller = cli_startup_timeout_for(cli, base);
assert!(
send.is_some() ^ caller.is_some(),
"exactly one wall must apply (cli={}, send={:?}, caller={:?})",
cli,
send,
caller
);
}
}
}