inferd_daemon/config.rs
1//! Daemon CLI configuration.
2//!
3//! M1 keeps the CLI surface deliberately small: one transport choice
4//! (`--tcp` or `--uds`), a lock path, a backend selector, and a queue
5//! depth. The operator-flag matrix expands in M4 along with packaging.
6
7use clap::{Parser, ValueEnum};
8use std::path::PathBuf;
9
10/// Backend adapters the daemon can register at startup.
11///
12/// `LlamaCpp` is gated behind the `llamacpp` cargo feature — default
13/// daemon builds only ship the mock adapter (per ADR 0006: lean core,
14/// extensions are separate concerns). `OpenAiCompat` is gated behind
15/// the `openai` cargo feature — pulled in only when the operator
16/// wants the outbound HTTPS adapter (ADR 0006 cloud carve-out).
17#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
18pub enum BackendKind {
19 /// Deterministic test double — used by integration tests and the
20 /// M1 echo daemon.
21 Mock,
22 /// Local llama.cpp backend via FFI (M2). Requires `--model-path`.
23 #[cfg(feature = "llamacpp")]
24 Llamacpp,
25 /// OpenAI-compatible outbound HTTPS adapter (Phase 5A). Reaches
26 /// any provider speaking the `/v1/chat/completions` wire (OpenAI,
27 /// vLLM, LM Studio, LocalAI, OpenRouter, llama.cpp's HTTP server).
28 /// Requires `--openai-base-url` + `--openai-model`. The API key
29 /// is read from `--openai-api-key` or env (`INFERD_OPENAI_API_KEY`
30 /// then `OPENAI_API_KEY`); pass an empty string to skip the
31 /// `Authorization` header for self-hosted endpoints.
32 #[cfg(feature = "openai")]
33 OpenaiCompat,
34 /// AWS Bedrock-runtime `InvokeModelWithResponseStream` adapter
35 /// (Phase 6B-5). v0.2.0 ships only the Anthropic-on-Bedrock body
36 /// shape — Claude models invoked via Bedrock's pinned
37 /// `anthropic_version: "bedrock-2023-05-31"` payload. Requires
38 /// `--bedrock-region` + `--bedrock-model-id`. Auth resolves from
39 /// `--bedrock-bearer-token` / `AWS_BEARER_TOKEN_BEDROCK` first,
40 /// then the standard `AWS_ACCESS_KEY_ID` /
41 /// `AWS_SECRET_ACCESS_KEY` (+ optional `AWS_SESSION_TOKEN`)
42 /// chain.
43 #[cfg(feature = "bedrock")]
44 BedrockInvoke,
45}
46
47/// Top-level CLI for `inferd-daemon`.
48#[derive(Debug, Parser)]
49#[command(name = "inferd-daemon", version, about = "Local inference daemon")]
50pub struct Cli {
51 /// Backend to load at startup.
52 ///
53 /// When omitted: defer to the config file's `backends:` (or legacy
54 /// `model:` block) if one is present; otherwise fall back to the
55 /// in-memory `mock` backend so `--lock + --tcp/--uds/--pipe` alone
56 /// still boots a dev-mode echo daemon.
57 ///
58 /// When explicit: honour the CLI choice. Passing `--backend mock`
59 /// short-circuits config loading (useful for forcing mock in test
60 /// rigs even when a config file is on disk); any other explicit
61 /// kind is built from CLI flags only — config-file `backends:` are
62 /// ignored in that case so operators get exactly what they asked
63 /// for.
64 #[arg(long, value_enum, env = "INFERD_BACKEND")]
65 pub backend: Option<BackendKind>,
66
67 /// Path to the single-instance lock file. The lock is held for the
68 /// lifetime of the daemon process.
69 #[arg(long, env = "INFERD_LOCK")]
70 pub lock: PathBuf,
71
72 /// Loopback TCP bind address. Mutually exclusive with `--uds` and `--pipe`.
73 #[arg(long, env = "INFERD_TCP", conflicts_with_all = ["uds", "pipe"])]
74 pub tcp: Option<String>,
75
76 /// Unix domain socket path. Mutually exclusive with `--tcp` and `--pipe`. Unix only.
77 #[arg(long, env = "INFERD_UDS", conflicts_with_all = ["tcp", "pipe"])]
78 pub uds: Option<PathBuf>,
79
80 /// Windows named pipe path (e.g. `\\.\pipe\inferd-infer`).
81 /// Mutually exclusive with `--tcp` and `--uds`. Windows only.
82 #[arg(long, env = "INFERD_PIPE", conflicts_with_all = ["tcp", "uds"])]
83 pub pipe: Option<String>,
84
85 /// Group name for the UDS (Unix only). Ignored on other transports.
86 #[arg(long, env = "INFERD_GROUP")]
87 pub group: Option<String>,
88
89 /// Active generations served concurrently. v0.1 invariant is 1; values
90 /// above 1 are reserved for v0.2 continuous-batching backends.
91 #[arg(long, default_value_t = 1, env = "INFERD_ACTIVE_PERMITS")]
92 pub active_permits: usize,
93
94 /// Maximum waiting queue depth. Submits beyond this return
95 /// `code: queue_full` immediately.
96 #[arg(long, default_value_t = 10, env = "INFERD_QUEUE_DEPTH")]
97 pub queue_depth: usize,
98
99 /// Seconds to wait for the backend to report ready before failing
100 /// startup.
101 #[arg(long, default_value_t = 30, env = "INFERD_READY_TIMEOUT_SECS")]
102 pub ready_timeout_secs: u64,
103
104 /// Path to the GGUF model file. Required when `--backend llamacpp`.
105 #[arg(long, env = "INFERD_MODEL_PATH")]
106 pub model_path: Option<PathBuf>,
107
108 /// Optional expected SHA-256 of the model file as a hex string
109 /// (64 chars). When present, the daemon verifies the file before
110 /// loading via `subtle::ConstantTimeEq` (THREAT_MODEL F-5).
111 #[arg(long, env = "INFERD_MODEL_SHA256")]
112 pub model_sha256: Option<String>,
113
114 /// Llama.cpp context window in tokens. Default 8192.
115 #[arg(long, default_value_t = 8192, env = "INFERD_N_CTX")]
116 pub n_ctx: u32,
117
118 /// Llama.cpp GPU layer offload count. 0 = CPU-only. GPU support
119 /// requires the `cuda`/`metal`/`vulkan`/`rocm` cargo feature at
120 /// build time.
121 #[arg(long, default_value_t = 0, env = "INFERD_N_GPU_LAYERS")]
122 pub n_gpu_layers: i32,
123
124 /// Enable embed capability on the active llamacpp backend. Same
125 /// flag plumbing as `--n-ctx` / `--n-gpu-layers`: when set, this
126 /// CLI value flips `LlamacppEntry::embed = true` for both the
127 /// legacy single-model promotion path (config has `model:`) AND
128 /// the dev-mode path (no config file at all). When unset, the
129 /// effective embed flag comes from the multi-backend
130 /// `backends[].embed` field; legacy single-model and dev-mode
131 /// stay generation-only.
132 ///
133 /// Mirrors the `--embed` flag's posture: this enables the
134 /// *capability*; `--embed` separately decides whether to bind
135 /// the embed socket. Both flags are needed to actually serve
136 /// embed requests.
137 #[arg(long, env = "INFERD_LLAMACPP_EMBED")]
138 pub llamacpp_embed: bool,
139
140 /// Pooling strategy for llamacpp embeddings. Maps to llama.cpp's
141 /// `LLAMA_POOLING_TYPE_*`: 0 = NONE (per-token vectors), 1 = MEAN,
142 /// 2 = CLS, 3 = LAST. When omitted, the model's metadata pooling
143 /// default applies. Has no effect unless `--llamacpp-embed` is
144 /// also set.
145 #[arg(long, env = "INFERD_LLAMACPP_EMBED_POOLING")]
146 pub llamacpp_embed_pooling: Option<i32>,
147
148 /// Llama.cpp embed-side context window in tokens. Default 2048.
149 /// Embed requests are bounded by this; generation is unaffected.
150 /// Has no effect unless `--llamacpp-embed` is also set.
151 #[arg(long, default_value_t = 2048, env = "INFERD_LLAMACPP_EMBED_N_CTX")]
152 pub llamacpp_embed_n_ctx: u32,
153
154 /// Base URL of the upstream OpenAI-compat endpoint, no trailing
155 /// slash and no path (the adapter appends `/v1/chat/completions`).
156 /// Required when `--backend openai-compat`. Examples:
157 /// `https://api.openai.com`, `http://localhost:11434`,
158 /// `https://openrouter.ai`.
159 #[arg(long, env = "INFERD_OPENAI_BASE_URL")]
160 pub openai_base_url: Option<String>,
161
162 /// Bearer token for the OpenAI-compat upstream. Sent as
163 /// `Authorization: Bearer <value>`. Pass an empty string to skip
164 /// the header entirely for self-hosted endpoints. Resolves from
165 /// `--openai-api-key`, then `INFERD_OPENAI_API_KEY`, then
166 /// `OPENAI_API_KEY` (the de-facto env name most providers' SDKs
167 /// already use).
168 #[arg(long, env = "INFERD_OPENAI_API_KEY", hide_env_values = true)]
169 pub openai_api_key: Option<String>,
170
171 /// Upstream model identifier echoed in the request `model` field
172 /// — provider-specific (e.g. `gpt-4o-mini`, `llama3.1:8b`,
173 /// `meta-llama/Meta-Llama-3-70B-Instruct`). Required when
174 /// `--backend openai-compat`.
175 #[arg(long, env = "INFERD_OPENAI_MODEL")]
176 pub openai_model: Option<String>,
177
178 /// Total request timeout for OpenAI-compat calls, in seconds.
179 /// Default 300 (5 minutes) — long enough for a slow first-token
180 /// from a cold cloud model, short enough to surface stuck
181 /// requests rather than hang forever.
182 #[arg(long, default_value_t = 300, env = "INFERD_OPENAI_TIMEOUT_SECS")]
183 pub openai_timeout_secs: u64,
184
185 /// AWS region the Bedrock endpoint lives in, e.g. `us-east-1`,
186 /// `eu-central-1`. Required when `--backend bedrock-invoke`.
187 /// Used for both the endpoint host and SigV4 signing scope.
188 #[arg(long, env = "INFERD_BEDROCK_REGION")]
189 pub bedrock_region: Option<String>,
190
191 /// Bedrock model id (URL-encoded by the adapter), e.g.
192 /// `anthropic.claude-3-5-sonnet-20241022-v2:0`. Required when
193 /// `--backend bedrock-invoke`.
194 #[arg(long, env = "INFERD_BEDROCK_MODEL_ID")]
195 pub bedrock_model_id: Option<String>,
196
197 /// Pre-issued Bedrock bearer token (`AWS_BEARER_TOKEN_BEDROCK`
198 /// shape, AWS rolled this out in 2025-06). When set, the adapter
199 /// sends `Authorization: Bearer <value>` and skips SigV4. When
200 /// unset, the adapter falls back to the standard
201 /// `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (+ optional
202 /// `AWS_SESSION_TOKEN`) chain via SigV4 signing.
203 #[arg(long, env = "AWS_BEARER_TOKEN_BEDROCK", hide_env_values = true)]
204 pub bedrock_bearer_token: Option<String>,
205
206 /// Override the Bedrock endpoint host. Empty/absent → default
207 /// `bedrock-runtime.<region>.amazonaws.com`. Useful for VPC
208 /// endpoints / integration tests.
209 #[arg(long, env = "INFERD_BEDROCK_ENDPOINT")]
210 pub bedrock_endpoint: Option<String>,
211
212 /// Total request timeout for Bedrock calls, in seconds. Default
213 /// 300 (5 minutes).
214 #[arg(long, default_value_t = 300, env = "INFERD_BEDROCK_TIMEOUT_SECS")]
215 pub bedrock_timeout_secs: u64,
216
217 /// Optional pre-shared API key. When set, TCP clients MUST send
218 /// `{"type":"auth","key":"<this value>"}` as their first NDJSON
219 /// frame on the connection or the daemon closes the connection.
220 /// UDS and named-pipe transports ignore this — kernel-attested
221 /// peer credentials (F-7) do the work there.
222 ///
223 /// Comparison is constant-time. THREAT_MODEL F-8.
224 #[arg(long, env = "INFERD_API_KEY", hide_env_values = true)]
225 pub api_key: Option<String>,
226
227 /// Path to the operator JSON config file. Default
228 /// `~/.inferd/config.json`. When present, fetch + auto-pull are
229 /// driven from it; CLI flags (`--model-path`, `--model-sha256`,
230 /// `--n-ctx`, `--n-gpu-layers`) override config-file values when
231 /// both are supplied. When absent, the daemon falls back to
232 /// CLI-flag-only operation (dev mode).
233 #[arg(long, env = "INFERD_CONFIG")]
234 pub config: Option<PathBuf>,
235
236 /// Admin endpoint path. Defaults per-platform to the path
237 /// documented in `docs/protocol-v1.md` §"Admin endpoint" — e.g.
238 /// `/run/inferd/admin.sock` on Linux, `\\.\pipe\inferd-admin` on
239 /// Windows. Override for tests / non-default deployments.
240 #[arg(long, env = "INFERD_ADMIN_ADDR")]
241 pub admin_addr: Option<PathBuf>,
242
243 /// Enable the v2 inference endpoint per ADR 0015. v2 binds on a
244 /// *separate* socket from v1: `infer.v2.sock` on Unix /
245 /// `\\.\pipe\inferd-infer-v2` on Windows. v1 stays on its own
246 /// socket and is unaffected.
247 ///
248 /// Phase 1B: the v2 endpoint accepts and validates v2 requests
249 /// but returns `Error{code:internal, message:"v2 generation not
250 /// implemented"}` because the Backend trait does not yet expose
251 /// `generate_v2`. Use this to integration-test middleware that
252 /// will speak v2 once Phase 2A lands.
253 #[arg(long, env = "INFERD_V2")]
254 pub v2: bool,
255
256 /// Override the default v2 inference endpoint path.
257 /// Mirrors `--uds` / `--pipe` for v2; on Linux/macOS this is a
258 /// UDS path, on Windows a named-pipe path. Has no effect unless
259 /// `--v2` is also set.
260 #[arg(long, env = "INFERD_V2_ADDR")]
261 pub v2_addr: Option<PathBuf>,
262
263 /// Loopback TCP bind address for the v2 endpoint. Mutually
264 /// exclusive with `--v2-addr`. Useful for tests that don't want
265 /// the platform default (UDS / named pipe). Has no effect
266 /// unless `--v2` is also set.
267 #[arg(long, env = "INFERD_V2_TCP", conflicts_with = "v2_addr")]
268 pub v2_tcp: Option<String>,
269
270 /// Enable the embed inference endpoint per ADR 0017. The embed
271 /// endpoint binds on a *separate* socket from v1/v2:
272 /// `infer.embed.sock` on Unix / `\\.\pipe\inferd-infer-embed`
273 /// on Windows. Has no effect unless the active backend's
274 /// `capabilities().embed` is true (capability-driven binding).
275 #[arg(long, env = "INFERD_EMBED")]
276 pub embed: bool,
277
278 /// Override the default embed inference endpoint path.
279 /// Mirrors `--uds` / `--pipe` for embed; on Linux/macOS this is
280 /// a UDS path, on Windows a named-pipe path. Has no effect
281 /// unless `--embed` is also set.
282 #[arg(long, env = "INFERD_EMBED_ADDR")]
283 pub embed_addr: Option<PathBuf>,
284
285 /// Loopback TCP bind address for the embed endpoint. Mutually
286 /// exclusive with `--embed-addr`. Has no effect unless `--embed`
287 /// is also set.
288 #[arg(long, env = "INFERD_EMBED_TCP", conflicts_with = "embed_addr")]
289 pub embed_tcp: Option<String>,
290}
291
292impl Cli {
293 /// Validate that exactly one transport is selected. clap enforces
294 /// mutual exclusion; this checks the at-least-one part.
295 pub fn require_one_transport(&self) -> Result<(), &'static str> {
296 let count = [self.tcp.is_some(), self.uds.is_some(), self.pipe.is_some()]
297 .iter()
298 .filter(|b| **b)
299 .count();
300 match count {
301 1 => Ok(()),
302 0 => Err("must specify one of --tcp, --uds, --pipe"),
303 _ => Err("--tcp, --uds, --pipe are mutually exclusive"),
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use clap::CommandFactory;
312
313 #[test]
314 fn cli_parses_minimum_required() {
315 let cli = Cli::parse_from([
316 "inferd-daemon",
317 "--lock",
318 "/tmp/inferd.lock",
319 "--tcp",
320 "127.0.0.1:0",
321 ]);
322 assert!(cli.tcp.is_some());
323 assert!(cli.uds.is_none());
324 assert_eq!(cli.queue_depth, 10);
325 assert_eq!(cli.active_permits, 1);
326 cli.require_one_transport().unwrap();
327 }
328
329 #[test]
330 fn cli_rejects_no_transport() {
331 let cli = Cli::parse_from(["inferd-daemon", "--lock", "/tmp/inferd.lock"]);
332 assert!(cli.require_one_transport().is_err());
333 }
334
335 #[test]
336 fn cli_rejects_both_transports_via_clap() {
337 // clap-level mutual exclusion: this should fail to parse, not
338 // require_one_transport's runtime check.
339 let result = Cli::try_parse_from([
340 "inferd-daemon",
341 "--lock",
342 "/tmp/inferd.lock",
343 "--tcp",
344 "127.0.0.1:0",
345 "--uds",
346 "/tmp/inferd.sock",
347 ]);
348 assert!(result.is_err());
349 }
350
351 #[test]
352 fn cli_accepts_pipe_transport() {
353 let cli = Cli::parse_from([
354 "inferd-daemon",
355 "--lock",
356 "C:/tmp/inferd.lock",
357 "--pipe",
358 r"\\.\pipe\inferd-test",
359 ]);
360 assert!(cli.pipe.is_some());
361 assert!(cli.uds.is_none());
362 assert!(cli.tcp.is_none());
363 cli.require_one_transport().unwrap();
364 }
365
366 #[test]
367 fn cli_rejects_pipe_with_tcp_via_clap() {
368 let result = Cli::try_parse_from([
369 "inferd-daemon",
370 "--lock",
371 "/tmp/inferd.lock",
372 "--tcp",
373 "127.0.0.1:0",
374 "--pipe",
375 r"\\.\pipe\inferd-test",
376 ]);
377 assert!(result.is_err());
378 }
379
380 #[test]
381 fn cli_command_factory_is_well_formed() {
382 // Ensures clap's `#[command]` derives don't conflict; cheap smoke
383 // test that catches lots of misconfigurations.
384 Cli::command().debug_assert();
385 }
386
387 #[test]
388 fn cli_accepts_v2_flag() {
389 let cli = Cli::parse_from([
390 "inferd-daemon",
391 "--lock",
392 "/tmp/inferd.lock",
393 "--tcp",
394 "127.0.0.1:0",
395 "--v2",
396 "--v2-tcp",
397 "127.0.0.1:0",
398 ]);
399 assert!(cli.v2);
400 assert!(cli.v2_tcp.is_some());
401 assert!(cli.v2_addr.is_none());
402 }
403
404 #[test]
405 fn cli_rejects_v2_addr_with_v2_tcp() {
406 let result = Cli::try_parse_from([
407 "inferd-daemon",
408 "--lock",
409 "/tmp/inferd.lock",
410 "--tcp",
411 "127.0.0.1:0",
412 "--v2",
413 "--v2-tcp",
414 "127.0.0.1:0",
415 "--v2-addr",
416 "/tmp/inferd-v2.sock",
417 ]);
418 assert!(result.is_err());
419 }
420
421 #[test]
422 fn cli_v2_disabled_by_default() {
423 let cli = Cli::parse_from([
424 "inferd-daemon",
425 "--lock",
426 "/tmp/inferd.lock",
427 "--tcp",
428 "127.0.0.1:0",
429 ]);
430 assert!(!cli.v2);
431 }
432
433 #[test]
434 fn cli_accepts_embed_flag() {
435 let cli = Cli::parse_from([
436 "inferd-daemon",
437 "--lock",
438 "/tmp/inferd.lock",
439 "--tcp",
440 "127.0.0.1:0",
441 "--embed",
442 "--embed-tcp",
443 "127.0.0.1:0",
444 ]);
445 assert!(cli.embed);
446 assert!(cli.embed_tcp.is_some());
447 assert!(cli.embed_addr.is_none());
448 }
449
450 #[test]
451 fn cli_rejects_embed_addr_with_embed_tcp() {
452 let result = Cli::try_parse_from([
453 "inferd-daemon",
454 "--lock",
455 "/tmp/inferd.lock",
456 "--tcp",
457 "127.0.0.1:0",
458 "--embed",
459 "--embed-tcp",
460 "127.0.0.1:0",
461 "--embed-addr",
462 "/tmp/inferd-embed.sock",
463 ]);
464 assert!(result.is_err());
465 }
466
467 #[test]
468 fn cli_embed_disabled_by_default() {
469 let cli = Cli::parse_from([
470 "inferd-daemon",
471 "--lock",
472 "/tmp/inferd.lock",
473 "--tcp",
474 "127.0.0.1:0",
475 ]);
476 assert!(!cli.embed);
477 }
478
479 #[test]
480 fn cli_llamacpp_embed_flags_default_off() {
481 let cli = Cli::parse_from([
482 "inferd-daemon",
483 "--lock",
484 "/tmp/inferd.lock",
485 "--tcp",
486 "127.0.0.1:0",
487 ]);
488 assert!(!cli.llamacpp_embed);
489 assert!(cli.llamacpp_embed_pooling.is_none());
490 assert_eq!(cli.llamacpp_embed_n_ctx, 2048);
491 }
492
493 #[test]
494 fn cli_accepts_llamacpp_embed_flags() {
495 // Issue #16: dev-mode + legacy single-model configs need a
496 // CLI route to flip embed on without rewriting the config.
497 let cli = Cli::parse_from([
498 "inferd-daemon",
499 "--lock",
500 "/tmp/inferd.lock",
501 "--tcp",
502 "127.0.0.1:0",
503 "--llamacpp-embed",
504 "--llamacpp-embed-pooling",
505 "1",
506 "--llamacpp-embed-n-ctx",
507 "1024",
508 ]);
509 assert!(cli.llamacpp_embed);
510 assert_eq!(cli.llamacpp_embed_pooling, Some(1));
511 assert_eq!(cli.llamacpp_embed_n_ctx, 1024);
512 }
513
514 #[cfg(feature = "openai")]
515 #[test]
516 fn cli_accepts_openai_compat_backend() {
517 let cli = Cli::parse_from([
518 "inferd-daemon",
519 "--lock",
520 "/tmp/inferd.lock",
521 "--tcp",
522 "127.0.0.1:0",
523 "--backend",
524 "openai-compat",
525 "--openai-base-url",
526 "http://localhost:11434",
527 "--openai-model",
528 "llama3.1:8b",
529 "--openai-api-key",
530 "sk-x",
531 "--openai-timeout-secs",
532 "30",
533 ]);
534 assert_eq!(cli.backend, Some(BackendKind::OpenaiCompat));
535 assert_eq!(
536 cli.openai_base_url.as_deref(),
537 Some("http://localhost:11434")
538 );
539 assert_eq!(cli.openai_model.as_deref(), Some("llama3.1:8b"));
540 assert_eq!(cli.openai_api_key.as_deref(), Some("sk-x"));
541 assert_eq!(cli.openai_timeout_secs, 30);
542 }
543
544 #[cfg(feature = "bedrock")]
545 #[test]
546 fn cli_accepts_bedrock_invoke_backend() {
547 let cli = Cli::parse_from([
548 "inferd-daemon",
549 "--lock",
550 "/tmp/inferd.lock",
551 "--tcp",
552 "127.0.0.1:0",
553 "--backend",
554 "bedrock-invoke",
555 "--bedrock-region",
556 "us-east-1",
557 "--bedrock-model-id",
558 "anthropic.claude-3-5-sonnet-20241022-v2:0",
559 "--bedrock-bearer-token",
560 "abc123",
561 "--bedrock-timeout-secs",
562 "60",
563 ]);
564 assert_eq!(cli.backend, Some(BackendKind::BedrockInvoke));
565 assert_eq!(cli.bedrock_region.as_deref(), Some("us-east-1"));
566 assert_eq!(
567 cli.bedrock_model_id.as_deref(),
568 Some("anthropic.claude-3-5-sonnet-20241022-v2:0")
569 );
570 assert_eq!(cli.bedrock_bearer_token.as_deref(), Some("abc123"));
571 assert_eq!(cli.bedrock_timeout_secs, 60);
572 }
573
574 #[cfg(feature = "bedrock")]
575 #[test]
576 fn cli_bedrock_timeout_defaults_to_300() {
577 let cli = Cli::parse_from([
578 "inferd-daemon",
579 "--lock",
580 "/tmp/inferd.lock",
581 "--tcp",
582 "127.0.0.1:0",
583 "--backend",
584 "bedrock-invoke",
585 "--bedrock-region",
586 "us-east-1",
587 "--bedrock-model-id",
588 "anthropic.claude-3-5-haiku-20241022-v1:0",
589 ]);
590 assert_eq!(cli.bedrock_timeout_secs, 300);
591 assert!(cli.bedrock_bearer_token.is_none());
592 assert!(cli.bedrock_endpoint.is_none());
593 }
594
595 #[cfg(feature = "openai")]
596 #[test]
597 fn cli_openai_timeout_defaults_to_300() {
598 let cli = Cli::parse_from([
599 "inferd-daemon",
600 "--lock",
601 "/tmp/inferd.lock",
602 "--tcp",
603 "127.0.0.1:0",
604 "--backend",
605 "openai-compat",
606 "--openai-base-url",
607 "https://api.openai.com",
608 "--openai-model",
609 "gpt-4o-mini",
610 ]);
611 assert_eq!(cli.openai_timeout_secs, 300);
612 assert!(cli.openai_api_key.is_none());
613 }
614}