///|
/// Connection and generation settings for an OpenAI-compatible endpoint.
pub(all) struct Settings {
/// Bearer token. Empty means no `Authorization` header is sent.
mut api_key : String
/// API base URL, without a trailing slash. Defaults to the OpenAI endpoint.
mut base_url : String
/// Model id sent in the request body.
mut model : String
/// System prompt prepended to every request.
mut system : String
/// Sampling temperature, when set.
mut temperature : Double?
/// Cap on generated tokens, when set.
mut max_tokens : Int?
/// Per-request timeout in milliseconds.
mut timeout_ms : Int
/// 推理开关:勾上就往请求体里带 enable_thinking: true(Qwen3 / DashScope 那一套)。
/// 各家的开关名没有统一标准,所以两种都留着,由使用方按端点选。
mut enable_thinking : Bool
/// 另一个风格:reasoning_effort(OpenAI / DeepSeek 那一套)。None 表示不发。
mut reasoning_effort : String?
} derive(Eq)
///|
pub extend Settings with Eq::{equal, not_equal}
///|
pub let default_base_url : String = "https://api.openai.com/v1"
///|
pub let default_model : String = "gpt-4o-mini"
///|
pub let default_system : String = "You are a helpful assistant."
///|
pub let default_timeout_ms : Int = 60000
///|
/// Errors raised while resolving settings or parsing arguments.
pub(all) suberror ConfigError {
/// No API key was found and `--no-key` was not given.
MissingApiKey
/// A flag that needs a value was the last token on the command line.
MissingValue(flag~ : String)
/// A numeric flag could not be parsed.
BadNumber(flag~ : String, value~ : String)
/// An unrecognized `-`/`--` flag.
UnknownFlag(flag~ : String)
}
///|
pub fn ConfigError::to_string(self : ConfigError) -> String {
match self {
MissingApiKey =>
"missing API key: set MOONLLM_API_KEY or OPENAI_API_KEY, pass --api-key, or use --no-key for local endpoints"
MissingValue(flag~) => "missing value for \{flag}"
BadNumber(flag~, value~) => "invalid number for \{flag}: \{value}"
UnknownFlag(flag~) => "unknown flag: \{flag}"
}
}
///|
/// The first non-empty value among `keys`, in order.
fn first_env(env : Map[String, String], keys : Array[String]) -> String? {
for key in keys {
match env.get(key) {
Some(value) => if value != "" { return Some(value) }
None => ()
}
}
None
}
///|
/// Resolve settings from environment variables, applying defaults.
///
/// Per-field lookup order:
///
/// - API key: `MOONLLM_API_KEY`, `OPENAI_API_KEY`, `LLM_API_KEY`
/// - base URL: `MOONLLM_BASE_URL`, `OPENAI_BASE_URL`
/// - model: `MOONLLM_MODEL`, `OPENAI_MODEL`
/// - system: `MOONLLM_SYSTEM`
pub fn Settings::from_env(env : Map[String, String]) -> Settings {
{
api_key: first_env(env, ["MOONLLM_API_KEY", "OPENAI_API_KEY", "LLM_API_KEY"]).unwrap_or(
"",
),
base_url: first_env(env, ["MOONLLM_BASE_URL", "OPENAI_BASE_URL"]).unwrap_or(
default_base_url,
),
model: first_env(env, ["MOONLLM_MODEL", "OPENAI_MODEL"]).unwrap_or(
default_model,
),
system: first_env(env, ["MOONLLM_SYSTEM"]).unwrap_or(default_system),
temperature: None,
max_tokens: None,
timeout_ms: default_timeout_ms,
enable_thinking: false,
reasoning_effort: None,
}
}
///|
/// The `chat/completions` URL for this base URL.
pub fn Settings::chat_completions_url(self : Settings) -> String {
if self.base_url.has_suffix("/") {
self.base_url + "chat/completions"
} else {
self.base_url + "/chat/completions"
}
}