From e0c9ba384a1fedccd9346a1dbd5281bdf483dd8c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 26 Aug 2026 02:58:45 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20dynamic=20provider=20config=20=E2=80=94?= =?UTF-8?q?=20pos=20config=20ai=20auto-discovers=20provider=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/ai-providers/*.sh declare # PROVIDER_CONFIG: headers - lib/config-ui.sh: _cfg_provider_keys() scans providers at runtime - bin/pos-ai: POS_CONFIG uses *providers marker (no hardcoded keys) - Adding a new provider auto-populates config UI — no main tool edits needed --- DOC/AGENT_Context_Project.md | 2 +- bin/pos-ai | 8 +++----- lib/ai-providers/gemini.sh | 4 ++++ lib/ai-providers/openrouter.sh | 4 ++++ lib/config-ui.sh | 30 +++++++++++++++++++++++++++++- 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 534ea24..280f4f0 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -633,7 +633,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | | `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently | | `bin/pos-system-uninstall` | 415 | Remove pos toolkit binaries, services, shell integration, config, and data | -| `bin/pos-ai` | 645 | AI assistant: ask, chat, sessions, capture, models, providers | +| `bin/pos-ai` | 632 | AI assistant: ask, chat, sessions, capture, models, providers | | `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | | `bin/pos-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands | | `completions/pos.bash` | 306 | Dynamic bash completion | diff --git a/bin/pos-ai b/bin/pos-ai index 882b9f6..0716aba 100755 --- a/bin/pos-ai +++ b/bin/pos-ai @@ -3,7 +3,7 @@ set -euo pipefail # POS: ai ask — AI assistant: ask, chat, sessions, capture, models, providers # POS_SUBCMDS: ask chat sessions capture models providers # POS_FLAGS: --provider --model --session --system --full --last -# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini or openrouter, default gemini) | AI_GEMINI_API_KEY=secret:Gemini API key from aistudio.google.com | OPENROUTER_API_KEY=secret:OpenRouter API key from openrouter.ai | AI_MODEL=:Model id (default per provider) | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset) +# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini or openrouter, default gemini) | *providers | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset) source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" @@ -76,11 +76,9 @@ Options: Config: $CONFIG_FILE (edit with 'pos config ai') AI_PROVIDER Provider to use (gemini|openrouter, default gemini) - AI_GEMINI_API_KEY Gemini API key from aistudio.google.com - OPENROUTER_API_KEY OpenRouter API key from openrouter.ai - AI_MODEL Model id (default depends on provider) AI_SYSTEM_PROMPT Custom system prompt (overrides built-in; empty to reset) - OPENROUTER_MODEL Legacy: OpenRouter model fallback + Provider keys: auto-discovered from lib/ai-providers/*.sh + (AI_GEMINI_API_KEY, OPENROUTER_API_KEY, etc.) Notes: ask is terse by default: a built-in system instruction tells the model to diff --git a/lib/ai-providers/gemini.sh b/lib/ai-providers/gemini.sh index d950c3c..7051a22 100755 --- a/lib/ai-providers/gemini.sh +++ b/lib/ai-providers/gemini.sh @@ -3,6 +3,10 @@ # Provider-specific: API call, auth, response parsing, models list # Part of the R8 provider-agnostic architecture (lib/ai-providers/). +# Provider-specific config variables (auto-discovered by pos config ai): +# PROVIDER_CONFIG: AI_GEMINI_API_KEY=secret:Gemini API key from aistudio.google.com +# PROVIDER_CONFIG: AI_GEMINI_MODEL=:Gemini model id (default: gemini-2.5-flash) + provider_name() { printf 'Google Gemini'; } provider_default_model() { printf 'gemini-2.5-flash'; } diff --git a/lib/ai-providers/openrouter.sh b/lib/ai-providers/openrouter.sh index f20bc81..7fa6712 100755 --- a/lib/ai-providers/openrouter.sh +++ b/lib/ai-providers/openrouter.sh @@ -3,6 +3,10 @@ # Provider-specific: API call, auth, response parsing, models list # Part of the R8 provider-agnostic architecture (lib/ai-providers/). +# Provider-specific config variables (auto-discovered by pos config ai): +# PROVIDER_CONFIG: OPENROUTER_API_KEY=secret:OpenRouter API key from openrouter.ai +# PROVIDER_CONFIG: OPENROUTER_MODEL=:OpenRouter model id (default: openrouter/auto) + provider_name() { printf 'OpenRouter'; } provider_default_model() { printf 'openrouter/auto'; } diff --git a/lib/config-ui.sh b/lib/config-ui.sh index 2203b7d..ac981c3 100644 --- a/lib/config-ui.sh +++ b/lib/config-ui.sh @@ -136,6 +136,33 @@ _cfg_plugin_keys() { return 0 } +# "*providers" expansion: keys declared by the installed AI provider +# adapters' "# PROVIDER_CONFIG:" headers (lib/ai-providers/*.sh). +_cfg_provider_keys() { + local pdir line key desc flags + # Find lib/ai-providers/ relative to config-ui.sh + pdir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib/ai-providers" 2>/dev/null && pwd)" + [ -d "$pdir" ] || return 0 + while IFS= read -r line; do + [ -n "$line" ] || continue + # Format: KEY=flags:description (same as POS_CONFIG key fields) + key="${line%%=*}" + [ -n "$key" ] || continue + [ -n "${_cfg_seen[$key]:-}" ] && continue + _cfg_seen[$key]=1 + # Parse flags and description from the rest + local rest="${line#*=}" flags="" desc="" + if [[ "$rest" == *":"* ]]; then + flags="${rest%%:*}" + desc="${rest#*:}" + else + flags="$rest" + fi + printf '%s|%s|%s|\n' "$key" "$flags" "$desc" + done < <(grep '^# PROVIDER_CONFIG:' "$pdir"/*.sh 2>/dev/null | sed 's/^.*# PROVIDER_CONFIG:[[:space:]]*//' || true) + return 0 +} + # Declared keys for a scope: "KEY|flags|description" lines, deduped. cfg_scope_keys() { local scope="$1" dir line s keystring field @@ -155,7 +182,8 @@ cfg_scope_keys() { if [ -n "$field" ]; then if [[ "$field" == "*"* ]]; then case "$field" in - *plugins*) _cfg_plugin_keys ;; + *plugins*) _cfg_plugin_keys ;; + *providers*) _cfg_provider_keys ;; esac else _cfg_key_line "$field"