openwebui-cli/openwebui_cli/commands/config_cmd.py
2025-12-01 04:24:51 +01:00

364 lines
11 KiB
Python

"""CLI configuration commands."""
from urllib.parse import urlparse
import typer
import yaml
from rich.console import Console
from rich.prompt import Prompt
from rich.table import Table
from ..config import (
Config,
DefaultsConfig,
OutputConfig,
ProfileConfig,
get_config_path,
load_config,
save_config,
)
app = typer.Typer(no_args_is_help=True)
console = Console()
@app.command()
def init(
ctx: typer.Context,
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
) -> None:
"""Initialize configuration file interactively."""
config_path = get_config_path()
if config_path.exists() and not force:
console.print(f"[yellow]Config already exists at: {config_path}[/yellow]")
console.print("Use --force to overwrite")
raise typer.Exit(1)
console.print("[bold]OpenWebUI CLI Configuration Setup[/bold]\n")
# Get server URI
uri = Prompt.ask(
"Server URI",
default="http://localhost:8080",
)
# Get default model
default_model = Prompt.ask(
"Default model (optional)",
default="",
)
# Get output format
default_format = Prompt.ask(
"Default output format",
choices=["text", "json", "yaml"],
default="text",
)
# Build config
config = Config(
default_profile="default",
profiles={
"default": ProfileConfig(uri=uri),
},
)
if default_model:
config.defaults.model = default_model
config.defaults.format = default_format
# Save config
save_config(config)
console.print(f"\n[green]Configuration saved to: {config_path}[/green]")
console.print("\nNext steps:")
console.print(" 1. Run 'openwebui auth login' to authenticate")
console.print(" 2. Run 'openwebui models list' to see available models")
console.print(" 3. Run 'openwebui chat send -m <model> -p \"Hello\"' to chat")
@app.command()
def show(ctx: typer.Context) -> None:
"""Show current configuration."""
config_path = get_config_path()
if not config_path.exists():
console.print("[yellow]No config file found. Run 'openwebui config init' first.[/yellow]")
raise typer.Exit(1)
config = load_config()
console.print(f"[bold]Config file:[/bold] {config_path}\n")
# Show profiles
table = Table(title="Profiles")
table.add_column("Name", style="cyan")
table.add_column("URI", style="green")
table.add_column("Default", style="yellow")
for name, profile in config.profiles.items():
is_default = "" if name == config.default_profile else ""
table.add_row(name, profile.uri, is_default)
console.print(table)
# Show defaults
console.print("\n[bold]Defaults:[/bold]")
console.print(f" Model: {config.defaults.model or '(not set)'}")
console.print(f" Format: {config.defaults.format}")
console.print(f" Stream: {config.defaults.stream}")
console.print(f" Timeout: {config.defaults.timeout}s")
def _validate_uri(uri: str) -> None:
"""Validate URI format."""
parsed = urlparse(uri)
if not parsed.scheme:
raise ValueError("URI must have a scheme (e.g., http://, https://)")
if parsed.scheme not in ("http", "https"):
raise ValueError(f"URI scheme must be 'http' or 'https', got '{parsed.scheme}'")
def _set_config_value(config: Config, key: str, value: str) -> None:
"""Set a configuration value using dot notation.
Supports:
- defaults.model
- defaults.format
- defaults.stream
- defaults.timeout
- output.colors
- output.progress_bars
- output.timestamps
- profiles.<name>.uri
"""
parts = key.split(".")
try:
if len(parts) == 2:
section, field = parts
if section == "defaults":
_set_defaults_field(config.defaults, field, value)
elif section == "output":
_set_output_field(config.output, field, value)
else:
raise ValueError(f"Unknown section: {section}")
elif len(parts) == 3:
section, name, field = parts
if section == "profiles":
_set_profile_field(config, name, field, value)
else:
raise ValueError(f"Unknown section: {section}")
else:
msg = "Key format: section.field or profiles.<name>.uri (e.g., 'defaults.model')"
raise ValueError(msg)
except (ValueError, TypeError) as e:
console.print(f"[red]Error setting {key}: {e}[/red]")
raise typer.Exit(1)
def _set_defaults_field(defaults: DefaultsConfig, field: str, value: str) -> None:
"""Set a field in the defaults configuration."""
if field == "model":
defaults.model = value if value else None
elif field == "format":
if value not in ("text", "json", "yaml"):
raise ValueError("format must be 'text', 'json', or 'yaml'")
defaults.format = value
elif field == "stream":
defaults.stream = value.lower() in ("true", "1", "yes")
elif field == "timeout":
try:
timeout = int(value)
if timeout <= 0:
raise ValueError("timeout must be positive")
defaults.timeout = timeout
except ValueError as e:
raise ValueError(f"timeout must be a positive integer: {e}")
else:
raise ValueError(f"Unknown defaults field: {field}")
def _set_output_field(output: OutputConfig, field: str, value: str) -> None:
"""Set a field in the output configuration."""
if field == "colors":
output.colors = value.lower() in ("true", "1", "yes")
elif field == "progress_bars":
output.progress_bars = value.lower() in ("true", "1", "yes")
elif field == "timestamps":
output.timestamps = value.lower() in ("true", "1", "yes")
else:
raise ValueError(f"Unknown output field: {field}")
def _set_profile_field(config: Config, profile_name: str, field: str, value: str) -> None:
"""Set a field in a profile configuration."""
if field != "uri":
raise ValueError(f"Profile field must be 'uri', got '{field}'")
_validate_uri(value)
if profile_name not in config.profiles:
config.profiles[profile_name] = ProfileConfig(uri=value)
else:
config.profiles[profile_name].uri = value
@app.command("set")
def set_value(
ctx: typer.Context,
key: str = typer.Argument(
...,
help="Config key (e.g., 'defaults.model' or 'profiles.prod.uri')",
),
value: str = typer.Argument(..., help="Value to set"),
) -> None:
"""Set a configuration value.
Examples:
openwebui config set defaults.model mistral
openwebui config set defaults.timeout 60
openwebui config set profiles.prod.uri https://prod.example.com
openwebui config set output.colors false
"""
try:
config = load_config()
except yaml.YAMLError as e:
console.print(f"[red]Error loading config: {e}[/red]")
raise typer.Exit(1)
except Exception as e:
console.print(f"[red]Error loading config: {e}[/red]")
raise typer.Exit(1)
_set_config_value(config, key, value)
try:
save_config(config)
console.print(f"[green]Set {key} = {value}[/green]")
except OSError as e:
console.print(f"[red]Error saving config: {e}[/red]")
raise typer.Exit(1)
def _get_config_value(config: Config, key: str) -> str:
"""Get a configuration value using dot notation.
Supports:
- defaults.model
- defaults.format
- defaults.stream
- defaults.timeout
- output.colors
- output.progress_bars
- output.timestamps
- profiles.<name> (returns URI)
- profiles.<name>.uri (returns URI)
Returns the value as a string suitable for scripting.
"""
parts = key.split(".")
if len(parts) == 2:
section, field = parts
if section == "defaults":
return _get_defaults_field(config.defaults, field)
elif section == "output":
return _get_output_field(config.output, field)
elif section == "profiles":
# profiles.<name> returns the URI
return _get_profile_uri(config, field)
else:
raise KeyError(f"Unknown section: {section}")
elif len(parts) == 3:
section, name, field = parts
if section == "profiles":
return _get_profile_field(config, name, field)
else:
raise KeyError(f"Unknown section: {section}")
else:
raise KeyError("Key format: section.field or profiles.<name>.uri (e.g., 'defaults.model')")
def _get_defaults_field(defaults: DefaultsConfig, field: str) -> str:
"""Get a field from the defaults configuration."""
if field == "model":
return defaults.model or ""
elif field == "format":
return defaults.format
elif field == "stream":
return str(defaults.stream)
elif field == "timeout":
return str(defaults.timeout)
else:
raise KeyError(f"Unknown field: {field}")
def _get_output_field(output: OutputConfig, field: str) -> str:
"""Get a field from the output configuration."""
if field == "colors":
return str(output.colors)
elif field == "progress_bars":
return str(output.progress_bars)
elif field == "timestamps":
return str(output.timestamps)
else:
raise KeyError(f"Unknown field: {field}")
def _get_profile_uri(config: Config, profile_name: str) -> str:
"""Get the URI from a profile configuration."""
profile = config.profiles.get(profile_name)
if not profile:
raise KeyError(f"Unknown profile: {profile_name}")
return profile.uri
def _get_profile_field(config: Config, profile_name: str, field: str) -> str:
"""Get a field from a profile configuration."""
profile = config.profiles.get(profile_name)
if not profile:
raise KeyError(f"Unknown profile: {profile_name}")
if field == "uri":
return profile.uri
else:
raise KeyError(f"Unknown field: {field}")
@app.command("get")
def get_value(
ctx: typer.Context,
key: str = typer.Argument(
...,
help="Config key to get (e.g., 'defaults.model' or 'profiles.prod.uri')",
),
) -> None:
"""Get a configuration value.
Returns just the value (no decorations) suitable for scripting.
Examples:
openwebui config get defaults.model
openwebui config get defaults.timeout
openwebui config get profiles.prod.uri
openwebui config get output.colors
"""
try:
config = load_config()
except yaml.YAMLError as e:
console.print(f"[red]Error loading config: {e}[/red]")
raise typer.Exit(1)
except Exception as e:
console.print(f"[red]Error loading config: {e}[/red]")
raise typer.Exit(1)
try:
value = _get_config_value(config, key)
console.print(value)
except KeyError as e:
console.print(f"[red]Error getting {key}: {e}[/red]")
raise typer.Exit(1)