Commit 7398a9223c6e
Changed files (1)
dots
.config
aichat
dots/.config/aichat/genconf.py
@@ -3,9 +3,13 @@
# dependencies = [
# "requests",
# "PyYAML",
-# "google-generativeai",
# ]
# ///
+#
+# Generate aichat config.yaml from config.yaml.in template.
+# - Resolves passage:: secrets
+# - Fetches available models from running services (Ollama, Gemini, etc.)
+# - Uses REST API for Gemini (no grpc/native extensions needed)
import os
import os.path
@@ -15,17 +19,16 @@ import sys
from typing import Any, Dict, List, Optional
import requests
-import yaml # pip install pyyaml types-pyyaml
+import yaml
import urllib.parse as urlparse
-import google.generativeai as genai
def debug(msg: str):
print(f"[DEBUG] {msg}", file=sys.stderr)
-def check_running(api_base, timeout=0.5) -> bool:
- """Quickly check if Ollama is accessible at the given host and port."""
+def check_running(api_base: str, timeout: float = 0.5) -> bool:
+ """Quickly check if a service is accessible at the given host and port."""
url = urlparse.urlparse(api_base)
port = url.port or (80 if url.scheme == "http" else 443)
debug(f"Checking if {url.hostname}:{port} is running (api_base={api_base})")
@@ -38,7 +41,7 @@ def check_running(api_base, timeout=0.5) -> bool:
return result == 0
except Exception as e:
debug(f"Exception in check_running: {e}")
- raise e
+ return False
def load_config(config_path: str) -> Dict[str, Any]:
@@ -49,105 +52,15 @@ def load_config(config_path: str) -> Dict[str, Any]:
return config
-def get_models(api_base: str, api_key_config: str | None) -> List[Dict[str, str]]:
- """Query the models endpoint and return a list of model data"""
- debug(
- f"get_models called with api_base={api_base}, api_key_config={api_key_config}"
- )
- actual_api_key = None
- if api_key_config:
- if api_key_config.startswith("passage::"):
- passage_path = api_key_config.split("::", 1)[1]
- debug(f"Retrieving API key from passage at {passage_path}")
- actual_api_key = get_passageword(passage_path)
- if not actual_api_key:
- print(
- f"Could not retrieve API key from passage for path: {passage_path}",
- file=sys.stderr,
- )
- debug(f"Failed to retrieve API key from passage for {passage_path}")
- # Decide how to handle failure: skip, return empty, etc.
- # Here we'll proceed without a key, which might fail later.
- else:
- actual_api_key = api_key_config # Use the key directly if not a passage path
- debug("Using API key directly from config")
-
- headers = {}
- if actual_api_key:
- headers["Authorization"] = f"Bearer {actual_api_key}"
- debug("Authorization header set")
-
- # Ensure the URL is properly formatted
- if not api_base.endswith("/"):
- api_base = api_base + "/"
- debug(f"api_base adjusted to {api_base}")
-
- models_url = f"{api_base}models"
- debug(f"Querying models endpoint: {models_url}")
-
- try:
- response = requests.get(models_url, headers=headers, timeout=10)
- debug(f"HTTP GET {models_url} status_code={response.status_code}")
- response.raise_for_status()
- data = response.json()
- debug(f"Response JSON: {data}")
-
- # Extract models from response
- models = data.get("data", [])
- debug(f"Extracted models: {models}")
- return [
- {"name": model.get("id"), "description": model.get("id")}
- for model in models
- ]
- except Exception as e:
- print(f"Error querying {api_base}: {str(e)}", file=sys.stderr)
- debug(f"Exception in get_models: {e}")
- return []
-
-
-def get_gemini_models(api_key: Optional[str]) -> List[Dict[str, str]]:
- """Query Google's Gemini API and return a list of available models"""
- debug(f"get_gemini_models called with api_key={'***' if api_key else None}")
- if not api_key:
- print("Error: API key is required for Google Gemini API", file=sys.stderr)
- debug("No API key provided to get_gemini_models")
- return []
-
- try:
- # Configure the Gemini API with the provided key
- debug("Configuring genai with provided API key")
- genai.configure(api_key=api_key)
-
- # Get list of available models
- debug("Listing models from genai")
- models_list = genai.list_models()
- debug(f"Models list: {models_list}")
-
- # Filter for Gemini models
- gemini_models = [
- {"name": model.name.split("/")[-1], "description": model.description}
- for model in models_list
- if "gemini" in model.name.lower()
- ]
- debug(f"Filtered Gemini models: {gemini_models}")
-
- return gemini_models
- except Exception as e:
- print(f"Error querying Google Gemini API: {str(e)}", file=sys.stderr)
- debug(f"Exception in get_gemini_models: {e}")
- return []
-
-
-def get_passageword(passage_path: str) -> str | None:
- """Retrieve passageword from passage using the given path."""
+def get_passageword(passage_path: str) -> Optional[str]:
+ """Retrieve password from passage using the given path."""
debug(f"get_passageword called for passage_path={passage_path}")
try:
result = subprocess.run(
["passage", "show", passage_path], capture_output=True, text=True, check=True
)
- # Return the first line of the output, stripping newline
passageword = result.stdout.splitlines()[0]
- debug(f"Passageword retrieved from passage for {passage_path}")
+ debug(f"Password retrieved from passage for {passage_path}")
return passageword
except FileNotFoundError:
print(
@@ -166,6 +79,128 @@ def get_passageword(passage_path: str) -> str | None:
return None
+def resolve_api_key(api_key_config: Optional[str]) -> Optional[str]:
+ """Resolve API key, fetching from passage if needed."""
+ if not api_key_config:
+ return None
+ if api_key_config.startswith("passage::"):
+ passage_path = api_key_config.split("::", 1)[1]
+ debug(f"Retrieving API key from passage at {passage_path}")
+ return get_passageword(passage_path)
+ return api_key_config
+
+
+def get_models_openai(api_base: str, api_key: Optional[str]) -> List[Dict[str, str]]:
+ """Query OpenAI-compatible /models endpoint."""
+ debug(f"get_models_openai: api_base={api_base}")
+
+ headers = {}
+ if api_key:
+ headers["Authorization"] = f"Bearer {api_key}"
+
+ if not api_base.endswith("/"):
+ api_base = api_base + "/"
+
+ models_url = f"{api_base}models"
+ debug(f"Querying models endpoint: {models_url}")
+
+ try:
+ response = requests.get(models_url, headers=headers, timeout=10)
+ debug(f"HTTP GET {models_url} status_code={response.status_code}")
+ response.raise_for_status()
+ data = response.json()
+
+ models = data.get("data", [])
+ debug(f"Found {len(models)} models")
+ return [
+ {"name": model.get("id"), "description": model.get("id")}
+ for model in models
+ ]
+ except Exception as e:
+ print(f"Error querying {api_base}: {str(e)}", file=sys.stderr)
+ debug(f"Exception in get_models_openai: {e}")
+ return []
+
+
+def get_gemini_models(api_key: Optional[str]) -> List[Dict[str, str]]:
+ """Query Google's Gemini API via REST and return available models.
+
+ Uses the REST API directly instead of the google-generativeai SDK
+ to avoid grpc/native extension dependencies.
+ """
+ debug(f"get_gemini_models called with api_key={'***' if api_key else None}")
+ if not api_key:
+ print("Error: API key is required for Google Gemini API", file=sys.stderr)
+ return []
+
+ # Gemini REST API endpoint for listing models
+ url = "https://generativelanguage.googleapis.com/v1beta/models"
+ headers = {"x-goog-api-key": api_key}
+
+ try:
+ debug(f"Querying Gemini REST API: {url}")
+ response = requests.get(url, headers=headers, timeout=10)
+ debug(f"HTTP GET {url} status_code={response.status_code}")
+ response.raise_for_status()
+ data = response.json()
+
+ # Filter for gemini models and extract name/description
+ models = data.get("models", [])
+ gemini_models = [
+ {
+ "name": model.get("name", "").split("/")[-1], # "models/gemini-pro" -> "gemini-pro"
+ "description": model.get("description", model.get("displayName", "")),
+ }
+ for model in models
+ if "gemini" in model.get("name", "").lower()
+ ]
+ debug(f"Found {len(gemini_models)} Gemini models")
+ return gemini_models
+
+ except requests.exceptions.HTTPError as e:
+ print(f"HTTP error querying Gemini API: {e}", file=sys.stderr)
+ debug(f"HTTPError in get_gemini_models: {e}")
+ return []
+ except Exception as e:
+ print(f"Error querying Google Gemini API: {str(e)}", file=sys.stderr)
+ debug(f"Exception in get_gemini_models: {e}")
+ return []
+
+
+def process_client(client: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+ """Process a single client configuration."""
+ updated_client = client.copy()
+ debug(f"Processing client: {updated_client.get('name', 'unnamed')}")
+
+ # Resolve API key from passage if needed
+ api_key_config = updated_client.get("api_key")
+ actual_api_key = resolve_api_key(api_key_config)
+ updated_client["api_key"] = actual_api_key
+
+ client_type = updated_client.get("type")
+
+ # OpenAI-compatible clients (Ollama, Groq, etc.)
+ if client_type == "openai-compatible":
+ if not updated_client.get("models"):
+ api_base = updated_client.get("api_base")
+ if api_base:
+ if not check_running(api_base):
+ debug(f"{api_base} not running, skipping client")
+ return None
+ fetched_models = get_models_openai(api_base, actual_api_key)
+ updated_client["models"] = fetched_models if fetched_models else []
+ else:
+ updated_client["models"] = []
+
+ # Google Gemini clients
+ elif client_type == "gemini":
+ if not updated_client.get("models"):
+ fetched_models = get_gemini_models(actual_api_key)
+ updated_client["models"] = fetched_models if fetched_models else []
+
+ return updated_client
+
+
def main():
# Support reading from script directory or ~/.config/aichat/
script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -180,75 +215,16 @@ def main():
if "clients" in config_data:
updated_clients = []
debug(f"main: found {len(config_data.get('clients', []))} clients")
+
for client in config_data.get("clients", []):
- # Make a copy to avoid modifying the original dict during iteration if needed elsewhere
- updated_client = client.copy()
- debug(f"Processing client: {updated_client}")
+ processed = process_client(client)
+ if processed is not None:
+ updated_clients.append(processed)
+ debug(f"main: client {client.get('name', 'unnamed')} processed")
- actual_api_key = None
- api_key_config = updated_client.get("api_key")
- if api_key_config and api_key_config.startswith("passage::"):
- passage_path = api_key_config.split("::", 1)[1]
- debug(f"main: retrieving api_key from passage for {passage_path}")
- actual_api_key = get_passageword(passage_path)
- else:
- actual_api_key = api_key_config
- updated_client["api_key"] = actual_api_key
- debug("main: actual_api_key set for client")
-
- # For OpenAI-compatible clients, query and potentially add models
- if updated_client.get("type") == "openai-compatible":
- # Check if models are NOT already defined in config or are empty
- if not updated_client.get("models"):
- api_base = updated_client.get("api_base")
- api_key = updated_client.get("api_key")
- debug(
- f"main: openai-compatible client, api_base={api_base}, api_key={'***' if api_key else None}"
- )
-
- # Skip ollama explicitly if needed, or handle based on your logic
- if api_base:
- if not check_running(api_base):
- debug(f"main: {api_base} not running, skipping client")
- continue
- # Try to fetch models from API
- fetched_models = get_models(api_base, api_key)
- if fetched_models:
- updated_client["models"] = fetched_models
- debug("main: models fetched and set for client")
- else:
- # Keep models empty/undefined or add an empty list
- updated_client["models"] = []
- debug("main: no models fetched, set empty list")
- else:
- # Handle cases where type is openai-compatible but no api_base
- updated_client["models"] = []
- debug(
- "main: openai-compatible client with no api_base, set empty models"
- )
-
- # For Google Gemini clients, query and potentially add models
- elif updated_client.get("type") == "gemini":
- # Check if models are NOT already defined in config or are empty
- if not updated_client.get("models"):
- api_key = updated_client.get("api_key")
- debug(f"main: gemini client, api_key={'***' if api_key else None}")
- fetched_models = get_gemini_models(api_key)
- if fetched_models:
- updated_client["models"] = fetched_models
- debug("main: gemini models fetched and set for client")
- else:
- updated_client["models"] = []
- debug("main: no gemini models fetched, set empty list")
-
- updated_clients.append(updated_client)
- debug("main: client processed and added to updated_clients")
-
- # Replace the original clients list with the updated one
config_data["clients"] = updated_clients
- debug("main: updated_clients set in config_data")
- # Print the entire (potentially updated) configuration as YAML
+ # Output final configuration as YAML
debug("main: dumping config_data as YAML")
print(yaml.dump(config_data, default_flow_style=False, sort_keys=False))