// SPDX-FileCopyrightText: Amolith // // SPDX-License-Identifier: AGPL-3.0-or-later import envoy import filepath import gleam/dict.{type Dict} import gleam/result import gleam/string import simplifile import tom.{type Toml} /// Reasoning effort level for thinking models. pub type ReasoningEffort { ReasoningLow ReasoningMedium ReasoningHigh } /// User's reasoning preference: not specified, explicitly disabled, or a specific level. pub type ReasoningSetting { ReasoningNotSet ReasoningDisabled ReasoningEnabled(ReasoningEffort) } /// Parse a reasoning effort string into a ReasoningSetting. pub fn parse_reasoning(value: String) -> ReasoningSetting { case string.lowercase(value) { "" -> ReasoningNotSet "none" | "off" | "disabled" -> ReasoningDisabled "low" -> ReasoningEnabled(ReasoningLow) "medium" -> ReasoningEnabled(ReasoningMedium) "high" -> ReasoningEnabled(ReasoningHigh) _ -> ReasoningNotSet } } /// Convert ReasoningEffort to the API string representation. pub fn reasoning_to_string(effort: ReasoningEffort) -> String { case effort { ReasoningLow -> "low" ReasoningMedium -> "medium" ReasoningHigh -> "high" } } pub type Config { Config( provider: String, model: String, api_key: String, api_key_cmd: String, directions: String, reasoning: ReasoningSetting, endpoint: String, ) } pub fn default() -> Config { Config( provider: "", model: "", api_key: "", api_key_cmd: "", directions: "", reasoning: ReasoningNotSet, endpoint: "", ) } /// Load config from XDG_CONFIG_HOME/garble/config.toml or ~/.config/garble/config.toml pub fn load() -> Config { case config_path() { Error(_) -> default() Ok(path) -> case simplifile.read(path) { Error(_) -> default() Ok(content) -> case tom.parse(content) { Error(_) -> default() Ok(parsed) -> parse_config(parsed) } } } } fn config_path() -> Result(String, Nil) { let config_dir = case envoy.get("XDG_CONFIG_HOME") { Ok(xdg) -> xdg Error(_) -> case envoy.get("HOME") { Ok(home) -> filepath.join(home, ".config") Error(_) -> "" } } case config_dir { "" -> Error(Nil) dir -> { let path = filepath.join(dir, "garble/config.toml") case simplifile.is_file(path) { Ok(True) -> Ok(path) _ -> Error(Nil) } } } } fn parse_config(parsed: Dict(String, Toml)) -> Config { Config( provider: get_string(parsed, "provider"), model: get_string(parsed, "model"), api_key: get_string(parsed, "api_key"), api_key_cmd: get_string(parsed, "api_key_cmd"), directions: get_string(parsed, "directions"), reasoning: parse_reasoning(get_string(parsed, "reasoning")), endpoint: get_string(parsed, "endpoint"), ) } fn get_string(parsed: Dict(String, Toml), key: String) -> String { tom.get_string(parsed, [key]) |> result.unwrap("") } /// Merge CLI flags over config values. CLI takes precedence when non-empty. pub fn merge( cfg: Config, cli_provider cli_provider: String, cli_model cli_model: String, cli_directions cli_directions: String, cli_reasoning cli_reasoning: String, cli_endpoint cli_endpoint: String, ) -> Config { Config( provider: prefer_nonempty(cli_provider, cfg.provider), model: prefer_nonempty(cli_model, cfg.model), api_key: cfg.api_key, api_key_cmd: cfg.api_key_cmd, directions: prefer_nonempty(cli_directions, cfg.directions), reasoning: prefer_reasoning(cli_reasoning, cfg.reasoning), endpoint: prefer_nonempty(cli_endpoint, cfg.endpoint), ) } fn prefer_reasoning(cli: String, fallback: ReasoningSetting) -> ReasoningSetting { case parse_reasoning(cli) { ReasoningNotSet -> fallback setting -> setting } } fn prefer_nonempty(cli: String, fallback: String) -> String { case cli { "" -> fallback val -> val } }