use anyhow::Result;
use clap::Parser;
use std::io::{self, IsTerminal, Read};
use tracing::Level;
fn default_log_level() -> String {
if cfg!(debug_assertions) {
"debug".to_string()
} else {
"info".to_string()
}
}
fn piped_stdin() -> &'static str {
static PIPED: std::sync::OnceLock<String> = std::sync::OnceLock::new();
PIPED
.get_or_init(|| {
if io::stdin().is_terminal() {
return String::new();
}
let mut buffer = String::new();
if let Err(e) = io::stdin().read_to_string(&mut buffer) {
tracing::warn!("Failed to read stdin: {}", e);
return String::new();
}
buffer.trim().to_string()
})
.as_str()
}
fn load_actions_file(path: &str) -> Result<Vec<serde_json::Value>> {
use anyhow::Context;
let trimmed = path.trim();
let candidate = std::path::Path::new(trimmed);
let resolved: std::path::PathBuf = if candidate.exists() {
candidate.to_path_buf()
} else {
let with_ext = std::path::PathBuf::from(format!(
"{}{}",
trimmed,
crate::utils::save_load::NETGET_EXTENSION
));
if with_ext.exists() {
with_ext
} else {
candidate.to_path_buf() }
};
let content =
crate::utils::file_io::read_text(&resolved, crate::utils::save_load::MAX_SESSION_BYTES)
.with_context(|| format!("Failed to read --load file: {}", resolved.display()))?;
let parsed: serde_json::Value = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse JSON from {}", resolved.display()))?;
let actions = parsed
.get("actions")
.and_then(|v| v.as_array())
.ok_or_else(|| {
anyhow::anyhow!(
"{} must contain {{\"actions\": [...]}} (as written by the /save command)",
resolved.display()
)
})?
.clone();
Ok(actions)
}
#[derive(Parser, Debug)]
#[clap(
author,
version,
about,
long_about = "NetGet - LLM-Controlled Network Application\n\n\
NetGet is an AI-powered network tool where an LLM controls network protocols.\n\
It can operate in interactive mode (with TUI) or non-interactive mode.",
after_help = "EXAMPLES:\n\
\n\
Interactive mode (TUI):\n\
netget\n\
\n\
Non-interactive with prompt (no quotes needed):\n\
netget listen on port 80 via http\n\
netget \"listen on port 80 via http\" # quoted version\n\
cat prompt.txt | netget\n\
\n\
Specify model with prompt after --:\n\
netget -m llama3.2:latest -- listen on port 80\n\
netget --model deepseek-coder:latest show version\n\
\n\
Specify scripting environment:\n\
netget -e python listen on port 80\n\
netget --env javascript -- start http server\n\
netget --env llm show version\n\
\n\
MCP over HTTP (loopback by default; any other bind address needs a token):\n\
netget --mcp-http 8080\n\
NETGET_MCP_TOKEN=$(openssl rand -hex 32) netget --mcp-http 8080 --listen-addr 0.0.0.0\n\
\n\
Connect a client without a model round-trip:\n\
netget --client redis --connect 127.0.0.1:6379\n\
netget --client tcp --connect 10.0.0.5:9000 log every banner you receive\n\
netget --client-list",
trailing_var_arg = true,
allow_hyphen_values = true
)]
pub struct Args {
#[clap(short = 'm', long = "model", value_name = "MODEL")]
pub model: Option<String>,
#[clap(
short = 'l',
long = "log-level",
value_name = "LEVEL",
default_value_t = default_log_level()
)]
pub log_level: String,
#[clap(
short = 'e',
long = "env",
value_name = "ENVIRONMENT",
help = "Scripting environment: on (LLM chooses runtime), off (LLM only mode), python (Python scripting), javascript (JavaScript scripting), go (Go scripting), perl (Perl scripting)"
)]
pub scripting_env: Option<String>,
#[clap(
long = "handler",
value_name = "MODE",
help = "Event handler mode: any (LLM chooses handler types), script (force script handlers), static (force static responses), llm (force LLM handlers)"
)]
pub event_handler_mode: Option<String>,
#[clap(
long = "listen-addr",
value_name = "ADDRESS",
help = "IP address the --mcp-http control endpoint binds (default 127.0.0.1). Protocol servers are NOT affected; they bind per start_server 'host'. A non-loopback address requires --mcp-token or NETGET_MCP_TOKEN."
)]
pub listen_addr: Option<String>,
#[clap(
long = "include-disabled-protocols",
help = "Includes experimental protocols for testing purposes"
)]
pub include_disabled_protocols: bool,
#[clap(
long = "min-stability",
value_name = "LEVEL",
help = "Refuse to start any protocol whose declared development state is below LEVEL (incomplete, experimental, beta, stable; case-insensitive). Also filters lower-maturity protocols out of the LLM's base-stack menu so the model is not offered a protocol the operator forbade. Default: no minimum (only Incomplete protocols are hidden from the model)."
)]
pub min_stability: Option<String>,
#[clap(
long = "ollama-lock",
help = "DEPRECATED AND IGNORED. Accepted for compatibility only: nothing is locked, nothing is written to disk, and NetGet instances are not serialized against each other. It has never done any of those things. To bound LLM concurrency use --llm-max-concurrent (with --llm-queue-timeout and --llm-max-queued), which are enforced."
)]
pub ollama_lock: bool,
#[clap(
long = "ollama-url",
value_name = "URL",
help = "Base URL for Ollama API (default: http://localhost:11434). Use this to point to a custom Ollama instance or mock server for testing.",
conflicts_with = "openai_url",
hide = true // Hidden from help output - primarily for testing
)]
pub ollama_url: Option<String>,
#[clap(
long = "openai-url",
value_name = "URL",
help = "Base URL for an OpenAI-compatible API endpoint (e.g., https://api.openai.com, http://localhost:1234 for vLLM/LM Studio). Requires --model and an API key in NETGET_API_KEY or OPENAI_API_KEY (or, less safely, --api-key).",
conflicts_with = "ollama_url"
)]
pub openai_url: Option<String>,
#[clap(
long = "api-key",
value_name = "KEY",
help = "API key for the OpenAI-compatible endpoint. PREFER the NETGET_API_KEY (or OPENAI_API_KEY) environment variable: an argument is visible to every local user in the process table (`ps`), an environment variable is not. Passing this flag prints a warning."
)]
pub api_key: Option<String>,
#[clap(
long = "llm-agent",
help = "Do not call any model. Queue every LLM request for the calling MCP agent (e.g. Claude Code) to answer via the get_next_llm_request / answer_llm_request tools. Only meaningful with --mcp / --mcp-http; mutually exclusive with --ollama-url / --openai-url.",
conflicts_with_all = ["ollama_url", "openai_url"]
)]
pub llm_agent: bool,
#[clap(
long = "llm-agent-pipe",
value_name = "PATH",
help = "Named pipe (FIFO) that receives the id of each newly queued LLM request, so the agent can block-read it to get woken instead of polling. Created if absent. Requires --llm-agent; the long-poll tool works without it.",
requires = "llm_agent"
)]
pub llm_agent_pipe: Option<std::path::PathBuf>,
#[clap(
long = "llm-agent-timeout",
value_name = "SECONDS",
default_value = "300",
help = "How long (seconds) a queued LLM request waits for the agent to answer before the call errors and the connection resets to Idle. Requires --llm-agent.",
requires = "llm_agent"
)]
pub llm_agent_timeout: u64,
#[clap(
long = "llm-request-timeout",
value_name = "SECONDS",
help = "Wall-clock bound (seconds) on a single LLM backend call before it is treated as a transport failure and retried. Default: 300. A locally served 31B reasoning model answering a full prompt with the default tool list was measured at ~79s idle, so the old 120s default failed whenever anything else shared the GPU. Lower it only if you would rather fail fast than wait."
)]
pub llm_request_timeout: Option<u64>,
#[clap(
long = "llm-max-tokens",
value_name = "TOKENS",
help = "Completion tokens a single LLM call may produce before it is cut off. This is a runaway guard, not a working budget: the default (32768) sits far above any legitimate answer so it never truncates one, and --llm-request-timeout is the primary backstop. Lower it only to bound a model you expect to ramble."
)]
pub llm_max_tokens: Option<u32>,
#[clap(
long = "llm-seed",
value_name = "SEED",
help = "Sampler seed sent with every LLM request (Ollama `options.seed`, OpenAI `seed`). With a fixed seed an identical prompt draws the same tokens, which is what makes a real-model run repeatable; a prompt that differs (a new port, a new query id) still diverges. Unset, nothing is sent and the model's own default applies."
)]
pub llm_seed: Option<u64>,
#[clap(
long = "llm-temperature",
value_name = "TEMP",
value_parser = parse_temperature,
help = "Sampling temperature sent with every LLM request (Ollama `options.temperature`, OpenAI `temperature`). 0 is greedy decoding. Unset, nothing is sent and the model's own default applies (0.8 for most Ollama Modelfiles)."
)]
pub llm_temperature: Option<f32>,
#[cfg(feature = "embedded-llm")]
#[clap(
long = "embedded-model",
value_name = "PATH",
help = "Path to GGUF model file for embedded inference (e.g., mistral-7b.Q4_K_M.gguf). When specified, NetGet will use embedded llama.cpp instead of or alongside Ollama."
)]
pub embedded_model: Option<std::path::PathBuf>,
#[cfg(feature = "embedded-llm")]
#[clap(
long = "use-embedded",
help = "Use embedded LLM backend exclusively, skipping Ollama health check. Requires --embedded-model to be set or configured in ~/.netget/config.toml"
)]
pub use_embedded: bool,
#[clap(
long = "theme",
value_name = "THEME",
default_value = "auto",
help = "Color theme for TUI: auto (detect background), light (dark colors on light background), dark (bright colors on dark background), neutral (medium contrast for both)"
)]
pub theme: String,
#[clap(
long = "show-art",
help = "Show the Ollama-generated ASCII art banner on startup (costs one model round trip)"
)]
pub show_art: bool,
#[clap(long = "suppress-art", hide = true)]
pub suppress_art: bool,
#[clap(
long = "llm-max-concurrent",
value_name = "NUM",
help = "Maximum number of LLM requests in flight at once. Default: 1, i.e. sequential processing - additional requests QUEUE and are served in turn (bounded by --llm-queue-timeout and --llm-max-queued), they are not dropped. Increase for higher throughput with local Ollama."
)]
pub llm_max_concurrent: Option<usize>,
#[clap(
long = "llm-queue-timeout",
value_name = "SECONDS",
help = "How long a network-triggered LLM request waits for a free slot under --llm-max-concurrent before the protocol answers the peer with an overload error instead. Default: 120 (one backend request timeout). 0 waits forever."
)]
pub llm_queue_timeout: Option<u64>,
#[clap(
long = "llm-max-queued",
value_name = "NUM",
help = "How many network-triggered LLM requests may wait for a free slot at once. Beyond this the protocol answers the peer with an overload error immediately rather than growing the queue. Default: 128. 0 is unbounded."
)]
pub llm_max_queued: Option<usize>,
#[clap(
long = "llm-token-limit",
value_name = "TOKENS",
help = "Maximum tokens (input + output) per time window. Use for cloud API rate limiting. No limit by default (suitable for local Ollama)."
)]
pub llm_token_limit: Option<u64>,
#[clap(
long = "llm-token-window",
value_name = "SECONDS",
default_value = "60",
help = "Time window in seconds for token limit enforcement."
)]
pub llm_token_window: u64,
#[clap(
long = "no-scripts",
help = "Disable script generation for tests that need action-only responses",
hide = true // Hidden from help output - primarily for testing
)]
pub no_scripts: bool,
#[clap(
long = "load",
value_name = "FILE",
help = "Load and execute server/client configurations from a .netget file"
)]
pub load_file: Option<String>,
#[clap(
long = "exit-after-events",
value_name = "N",
value_parser = clap::value_parser!(u64).range(1..),
help = "Non-interactive mode: exit with status 0 once N network events have been handled across all servers and clients (each entry in the access log counts once, whatever answered it - handler, model or operator)"
)]
pub exit_after_events: Option<u64>,
#[clap(
long = "run-for",
value_name = "SECS",
value_parser = clap::value_parser!(u64).range(1..),
help = "Non-interactive mode: run for SECS seconds, then stop everything and exit with status 0"
)]
pub run_for: Option<u64>,
#[clap(
long = "mock-config-file",
value_name = "FILE",
help = "Path to JSON file containing mock LLM responses (used by tests)",
hide = true // Hidden from help output - internal testing flag
)]
pub mock_config_file: Option<std::path::PathBuf>,
#[clap(
long = "simple",
value_name = "PROTOCOL",
help = "Start a simple protocol server (e.g., http). Use --simple-list to see available protocols."
)]
pub simple_protocol: Option<String>,
#[clap(
long = "simple-list",
help = "List available simple protocols that can be started with --simple"
)]
pub simple_list: bool,
#[clap(
long = "client",
value_name = "PROTOCOL",
help = "Connect a client of PROTOCOL to the address given by --connect, without asking the model to do it. Any trailing text is used as the client's instruction. Use --client-list to see available protocols."
)]
pub client_protocol: Option<String>,
#[clap(
long = "connect",
value_name = "ADDRESS",
requires = "client_protocol",
help = "Remote address the --client connects to, e.g. 127.0.0.1:6379"
)]
pub client_addr: Option<String>,
#[clap(
long = "client-params",
value_name = "JSON",
requires = "client_protocol",
help = "JSON object of startup parameters for --client (same keys as the MCP start_client tool's startup_params)"
)]
pub client_params: Option<String>,
#[clap(
long = "client-handlers",
value_name = "JSON",
requires = "client_protocol",
help = "JSON array of event handlers for --client (script or static handlers run in-process with NO LLM call, which is what makes a scripted client deterministic)"
)]
pub client_handlers: Option<String>,
#[clap(
long = "client-list",
help = "List protocols that can be connected as clients with --client"
)]
pub client_list: bool,
#[clap(
long = "server",
value_name = "PROTOCOL",
conflicts_with = "client_protocol",
help = "Start a server of PROTOCOL directly, WITHOUT asking the model which protocol to run — the initial LLM call is skipped. Any trailing text becomes the server's per-request instruction (the general prompt used for every request). Use a base-stack name (e.g. http, dns, redis); run with --docs to see them. Combine with --port and --server-params."
)]
pub server_protocol: Option<String>,
#[clap(
long = "port",
value_name = "PORT",
requires = "server_protocol",
help = "Port the --server listens on (default is the protocol's own default)."
)]
pub server_port: Option<u16>,
#[clap(
long = "server-params",
value_name = "JSON",
requires = "server_protocol",
help = "JSON object of startup parameters for --server (same keys as the MCP start_server tool's startup_params, e.g. request_filter/default_response for http). Not every parameter has a dedicated flag — use this for the rest."
)]
pub server_params: Option<String>,
#[clap(
long = "docs",
help = "Print documentation for every compiled-in server and client protocol (base stacks, keywords, startup parameters, events, actions, examples) to stdout, then exit."
)]
pub docs: bool,
#[clap(
long = "mcp",
alias = "mcp-stdio",
help = "Run as an MCP server over stdin/stdout for integration with MCP clients like Claude Desktop"
)]
pub mcp_stdio: bool,
#[clap(
long = "mcp-http",
value_name = "PORT",
conflicts_with = "mcp_stdio",
help = "Run as an MCP server over HTTP/SSE on the given port (e.g. --mcp-http 8080). Bind address comes from --listen-addr (default 127.0.0.1)"
)]
pub mcp_http: Option<u16>,
#[clap(
long = "mcp-token",
value_name = "TOKEN",
help = "Require 'Authorization: Bearer <TOKEN>' on every --mcp-http request. PREFER the NETGET_MCP_TOKEN environment variable: an argument is visible to every local user in the process table. Required to bind --listen-addr to anything but a loopback address; without it the endpoint admits loopback Host/Origin only."
)]
pub mcp_token: Option<String>,
#[clap(value_name = "PROMPT", num_args = 0..)]
pub prompt: Vec<String>,
}
fn warn_api_key_on_command_line() {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
const MSG: &str = "--api-key puts the key in this process's command line, where any local user can read it (ps / /proc). Prefer the NETGET_API_KEY or OPENAI_API_KEY environment variable.";
tracing::warn!("{}", MSG);
eprintln!("⚠ {}", MSG);
});
}
pub const MCP_TOKEN_ENV: &str = "NETGET_MCP_TOKEN";
fn warn_mcp_token_on_command_line() {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
const MSG: &str = "--mcp-token puts the token in this process's command line, where any local user can read it (ps / /proc). Prefer the NETGET_MCP_TOKEN environment variable.";
tracing::warn!("{}", MSG);
eprintln!("⚠ {}", MSG);
});
}
impl Args {
pub fn resolve_api_key(&self) -> Option<String> {
if let Some(key) = &self.api_key {
warn_api_key_on_command_line();
return Some(key.clone());
}
std::env::var("NETGET_API_KEY")
.ok()
.or_else(|| std::env::var("OPENAI_API_KEY").ok())
}
pub fn resolve_mcp_token(&self) -> Option<String> {
if let Some(token) = &self.mcp_token {
warn_mcp_token_on_command_line();
return Some(token.clone()).filter(|t| !t.is_empty());
}
std::env::var(MCP_TOKEN_ENV).ok().filter(|t| !t.is_empty())
}
pub fn effective_log_level(&self) -> Level {
match self.log_level.to_lowercase().as_str() {
"off" | "none" => Level::ERROR, "error" => Level::ERROR,
"warn" | "warning" => Level::WARN,
"info" => Level::INFO,
"debug" => Level::DEBUG,
"trace" => Level::TRACE,
_ => Level::ERROR,
}
}
pub fn logging_disabled(&self) -> bool {
self.log_level == "off" || self.log_level == "none"
}
pub fn is_interactive(&self) -> bool {
if !self.prompt.is_empty() {
return false;
}
if !io::stdin().is_terminal() {
return false;
}
if !io::stdout().is_terminal() {
return false;
}
true
}
pub fn get_prompt(&self) -> Result<Option<String>> {
if self.load_file.is_some() {
return Ok(None);
}
if !self.prompt.is_empty() {
let joined = self.prompt.join(" ");
if crate::utils::save_load::is_actions_json(&joined) {
return Ok(None);
}
return Ok(Some(joined));
}
let piped = piped_stdin();
if !piped.is_empty() {
if crate::utils::save_load::is_actions_json(piped) {
return Ok(None);
}
return Ok(Some(piped.to_string()));
}
Ok(None)
}
pub fn get_actions_json(&self) -> Result<Option<Vec<serde_json::Value>>> {
use crate::utils::save_load;
if let Some(ref filename) = self.load_file {
return Ok(Some(load_actions_file(filename)?));
}
if !self.prompt.is_empty() {
let joined = self.prompt.join(" ");
if save_load::is_actions_json(&joined) {
let parsed: serde_json::Value = serde_json::from_str(&joined)?;
let actions = parsed["actions"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("Invalid actions format"))?
.clone();
return Ok(Some(actions));
}
}
if self.prompt.is_empty() {
let piped = piped_stdin();
if !piped.is_empty() && save_load::is_actions_json(piped) {
let parsed: serde_json::Value = serde_json::from_str(piped)?;
let actions = parsed["actions"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("Invalid actions format"))?
.clone();
return Ok(Some(actions));
}
}
Ok(None)
}
pub fn validate_mode(&self) -> Result<()> {
if self.is_interactive() {
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
anyhow::bail!(
"Cannot start in interactive mode without a terminal.\n\
Please provide a prompt via arguments, stdin, or use --non-interactive."
);
}
} else {
if self.get_prompt()?.is_none() {
anyhow::bail!(
"Non-interactive mode requires a prompt.\n\
Provide a prompt via arguments, stdin, or use interactive mode."
);
}
}
Ok(())
}
pub fn parse_scripting_mode(&self) -> Result<Option<crate::state::app_state::ScriptingMode>> {
if self.no_scripts {
return Ok(Some(crate::state::app_state::ScriptingMode::Off));
}
match &self.scripting_env {
None => Ok(None),
Some(env) => {
let mode = match env.to_lowercase().as_str() {
"on" | "auto" => crate::state::app_state::ScriptingMode::On,
"off" | "llm" => crate::state::app_state::ScriptingMode::Off,
"python" | "py" => crate::state::app_state::ScriptingMode::Python,
"javascript" | "js" | "node" => {
crate::state::app_state::ScriptingMode::JavaScript
}
"go" | "golang" => crate::state::app_state::ScriptingMode::Go,
"perl" => crate::state::app_state::ScriptingMode::Perl,
_ => {
anyhow::bail!(
"Invalid scripting environment: '{}'\n\
Valid options: on (auto), off (llm), python (py), javascript (js, node), go (golang), perl",
env
);
}
};
Ok(Some(mode))
}
}
}
pub fn parse_event_handler_mode(
&self,
) -> Result<Option<crate::state::app_state::EventHandlerMode>> {
match &self.event_handler_mode {
None => Ok(None),
Some(mode) => {
let parsed_mode = match mode.to_lowercase().as_str() {
"any" => crate::state::app_state::EventHandlerMode::Any,
"script" => crate::state::app_state::EventHandlerMode::Script,
"static" => crate::state::app_state::EventHandlerMode::Static,
"llm" => crate::state::app_state::EventHandlerMode::Llm,
_ => {
anyhow::bail!(
"Invalid event handler mode: '{}'\n\
Valid options: any, script, static, llm",
mode
);
}
};
Ok(Some(parsed_mode))
}
}
}
pub fn client_instruction(&self, protocol: &str, remote_addr: &str) -> String {
let trailing = self.prompt.join(" ").trim().to_string();
if trailing.is_empty() {
format!(
"You are a {} client connected to {}. Handle responses appropriately.",
protocol, remote_addr
)
} else {
trailing
}
}
pub fn server_instruction(&self, protocol: &str) -> String {
let trailing = self.prompt.join(" ").trim().to_string();
if trailing.is_empty() {
format!(
"You are a {} server. Respond to each request appropriately.",
protocol
)
} else {
trailing
}
}
pub fn parse_server_params(&self) -> Result<Option<serde_json::Value>> {
match &self.server_params {
None => Ok(None),
Some(raw) => {
let value: serde_json::Value = serde_json::from_str(raw)
.map_err(|e| anyhow::anyhow!("--server-params is not valid JSON: {}", e))?;
if !value.is_object() {
anyhow::bail!(
"--server-params must be a JSON object, e.g. '{{\"request_filter\": [...]}}'"
);
}
Ok(Some(value))
}
}
}
pub fn parse_client_params(&self) -> Result<Option<serde_json::Value>> {
match &self.client_params {
None => Ok(None),
Some(raw) => {
let value: serde_json::Value = serde_json::from_str(raw)
.map_err(|e| anyhow::anyhow!("--client-params is not valid JSON: {}", e))?;
if !value.is_object() {
anyhow::bail!("--client-params must be a JSON object, e.g. '{{\"db\": 0}}'");
}
Ok(Some(value))
}
}
}
pub fn parse_client_handlers(&self) -> Result<Option<Vec<serde_json::Value>>> {
match &self.client_handlers {
None => Ok(None),
Some(raw) => {
let value: serde_json::Value = serde_json::from_str(raw)
.map_err(|e| anyhow::anyhow!("--client-handlers is not valid JSON: {}", e))?;
match value {
serde_json::Value::Array(handlers) => Ok(Some(handlers)),
_ => anyhow::bail!(
"--client-handlers must be a JSON array of handler objects, e.g. '[{{\"event\": \"redis_response_received\", \"handler_type\": \"static\", \"response\": []}}]'"
),
}
}
}
}
pub fn parse_min_stability(
&self,
) -> Result<Option<crate::protocol::metadata::DevelopmentState>> {
match &self.min_stability {
None => Ok(None),
Some(raw) => match crate::protocol::metadata::DevelopmentState::parse_ci(raw) {
Some(state) => Ok(Some(state)),
None => anyhow::bail!(
"Invalid --min-stability value: '{}'\n\
Valid options (case-insensitive): incomplete, experimental, beta, stable",
raw
),
},
}
}
pub fn build_rate_limiter_config(&self) -> crate::llm::RateLimiterConfig {
crate::llm::RateLimiterConfig {
max_concurrent: self.llm_max_concurrent.unwrap_or(1),
token_limit: self.llm_token_limit,
token_window_secs: self.llm_token_window,
queue_timeout_secs: self
.llm_queue_timeout
.unwrap_or(crate::llm::DEFAULT_QUEUE_TIMEOUT_SECS),
max_queued: self
.llm_max_queued
.unwrap_or(crate::llm::DEFAULT_MAX_QUEUED),
}
}
}
fn parse_temperature(value: &str) -> Result<f32, String> {
let t: f32 = value
.parse()
.map_err(|_| format!("'{}' is not a number", value))?;
if !t.is_finite() || t < 0.0 {
return Err(format!(
"temperature must be a finite number >= 0, got {}",
value
));
}
Ok(t)
}