feat(agents): add aider, llxprt, and qwen agents

Amolith created

- Add aider agent with dual model support (architect + editor)
- Add llxprt agent with openai-compatible config
- Add qwen agent with environment variable config
- Update shell completions to include all new agents
- Follow established agent configuration patterns

Assisted-by: GLM 4.6 via Crush

Change summary

completions/synu.fish              |  28 +++++-
functions/_synu_agents/aider.fish  | 147 ++++++++++++++++++++++++++++++++
functions/_synu_agents/llxprt.fish |  92 ++++++++++++++++++++
functions/_synu_agents/qwen.fish   |  93 ++++++++++++++++++++
4 files changed, 355 insertions(+), 5 deletions(-)

Detailed changes

completions/synu.fish 🔗

@@ -4,14 +4,14 @@
 
 # Provide basic usage information for the wrapper itself
 # Suggest known agents and interactive mode for the first argument
-complete -c synu -n "not __fish_seen_subcommand_from i claude crush amp octo codex" \
+complete -c synu -n "not __fish_seen_subcommand_from i claude opencode aider llxprt qwen crush amp octo codex" \
     -a "i" -d "Interactive model selection"
-complete -c synu -n "not __fish_seen_subcommand_from i claude crush amp octo codex" \
-    -a "claude crush amp octo codex" -d "AI agent to wrap"
+complete -c synu -n "not __fish_seen_subcommand_from i claude opencode aider llxprt qwen crush amp octo codex" \
+    -a "claude opencode aider llxprt qwen crush amp octo codex" -d "AI agent to wrap"
 
 # After "i" subcommand, suggest agents
-complete -c synu -n "__fish_seen_subcommand_from i; and not __fish_seen_subcommand_from claude crush amp octo codex" \
-    -a "claude crush amp octo codex" -d "AI agent"
+complete -c synu -n "__fish_seen_subcommand_from i; and not __fish_seen_subcommand_from claude opencode aider llxprt qwen crush amp octo codex" \
+    -a "claude opencode aider llxprt qwen crush amp octo codex" -d "AI agent"
 
 # Claude-specific flags (when claude is the agent)
 complete -c synu -n "__fish_seen_subcommand_from claude" \
@@ -29,3 +29,21 @@ complete -c synu -n "__fish_seen_subcommand_from claude" \
 
 # Inherit claude completions for claude subcommand
 complete -c synu -n "__fish_seen_subcommand_from claude" -w claude
+
+# OpenCode-specific flags (when opencode is the agent)
+complete -c synu -n "__fish_seen_subcommand_from opencode" \
+    -s m -l model -r -d "Override model"
+
+# Aider-specific flags (when aider is the agent)
+complete -c synu -n "__fish_seen_subcommand_from aider" \
+    -s m -l model -r -d "Main model"
+complete -c synu -n "__fish_seen_subcommand_from aider" \
+    -s e -l editor-model -r -d "Editor model (enables architect + editor mode)"
+
+# llxprt-specific flags (when llxprt is the agent)
+complete -c synu -n "__fish_seen_subcommand_from llxprt" \
+    -s m -l model -r -d "Override model"
+
+# Qwen Code-specific flags (when qwen is the agent)
+complete -c synu -n "__fish_seen_subcommand_from qwen" \
+    -s m -l model -r -d "Override model"

functions/_synu_agents/aider.fish 🔗

@@ -0,0 +1,147 @@
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: Unlicense
+
+# Aider agent definition for synu
+# Provides model configuration for routing through Synthetic API
+# Aider accepts model via CLI flags and API config via environment variables
+
+# Source cache functions
+source (status dirname)/../_synu_cache.fish
+
+# Fallback defaults (used when no cache entry exists)
+set -g _synu_aider_fallback_model "hf:MiniMaxAI/MiniMax-M2"
+set -g _synu_aider_fallback_editor_model ""
+
+function _synu_aider_default --description "Get default model: _synu_aider_default slot"
+    set -l slot $argv[1]
+    set -l cached (_synu_cache_get aider $slot)
+    if test $status -eq 0
+        echo $cached
+    else
+        set -l var_name _synu_aider_fallback_$slot
+        echo $$var_name
+    end
+end
+
+function _synu_agent_aider_flags --description "Return argparse-compatible flag specification"
+    echo "m/model="
+    echo "e/editor-model="
+end
+
+function _synu_agent_aider_env_vars --description "Return list of environment variables set by configure"
+    echo OPENAI_API_BASE
+    echo OPENAI_API_KEY
+end
+
+function _synu_agent_aider_configure --description "Configure Aider environment variables and model selection"
+    # Parse flags passed from main synu
+    argparse 'm/model=' 'e/editor-model=' -- $argv
+    or return 1
+
+    # Start with defaults (from cache or fallback)
+    set -g _synu_aider_selected_model (_synu_aider_default model)
+    set -g _synu_aider_selected_editor_model (_synu_aider_default editor_model)
+
+    # Apply overrides if provided
+    if set -q _flag_model
+        set -g _synu_aider_selected_model $_flag_model
+    end
+    if set -q _flag_editor_model
+        set -g _synu_aider_selected_editor_model $_flag_editor_model
+    end
+
+    # Export environment variables for Aider
+    set -gx OPENAI_API_BASE "https://api.synthetic.new/openai/v1"
+    set -gx OPENAI_API_KEY $SYNTHETIC_API_KEY
+end
+
+function _synu_agent_aider_args --description "Return CLI arguments to pass to aider"
+    # Always return --model
+    echo --model
+    echo "openai/$_synu_aider_selected_model"
+    
+    # Return --editor-model if set
+    if test -n "$_synu_aider_selected_editor_model"
+        echo --editor-model
+        echo "openai/$_synu_aider_selected_editor_model"
+    end
+end
+
+function _synu_agent_aider_interactive --description "Interactive model selection using gum"
+    # Check for gum
+    if not command -q gum
+        echo "Error: gum is required for interactive mode. Install: https://github.com/charmbracelet/gum" >&2
+        return 1
+    end
+
+    # Fetch available models
+    set -l models_json (gum spin --spinner dot --title "Fetching models..." -- \
+        curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
+        "https://api.synthetic.new/openai/v1/models")
+    or return 1
+
+    set -l model_names (echo $models_json | jq -r '.data[].name')
+    or return 1
+
+    # Ask if editor model should be set
+    set -l use_editor_model (gum choose --limit 1 \
+        --header "Aider has two modes. Set editor model?" \
+        "No (single model mode)" "Yes (architect + editor mode)")
+    or return 1
+
+    # Build flags array
+    set -l flags
+
+    # Select main model
+    set -l model_name (printf "%s\n" $model_names | \
+        gum filter --limit 1 --header "Select main model for Aider" \
+        --placeholder "Filter models...")
+    or return 1
+
+    set -l model_id (echo $models_json | \
+        jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+
+    if test -z "$model_id"
+        echo "Error: Could not find model ID" >&2
+        return 1
+    end
+
+    set flags $flags --model=$model_id
+
+    # Select editor model if requested
+    if test "$use_editor_model" = "Yes (architect + editor mode)"
+        set -l editor_model_name (printf "%s\n" $model_names | \
+            gum filter --limit 1 --header "Select editor model for Aider" \
+            --placeholder "Filter models...")
+        or return 1
+
+        set -l editor_model_id (echo $models_json | \
+            jq -r --arg name "$editor_model_name" '.data[] | select(.name == $name) | .id')
+
+        if test -z "$editor_model_id"
+            echo "Error: Could not find editor model ID" >&2
+            return 1
+        end
+
+        set flags $flags --editor-model=$editor_model_id
+    end
+
+    # Offer to save as defaults
+    if gum confirm "Save as default for 'aider'?"
+        for flag in $flags
+            # Parse --key=value format
+            set -l parts (string match -r -- '^--([^=]+)=(.+)$' $flag)
+            if test -n "$parts[2]"
+                set -l key $parts[2]
+                set -l value $parts[3]
+                # Replace hyphens with underscores for cache keys
+                set key (string replace -a '-' '_' $key)
+                _synu_cache_set aider $key $value
+            end
+        end
+    end
+
+    # Output flags for caller to use
+    echo $flags
+end

functions/_synu_agents/llxprt.fish 🔗

@@ -0,0 +1,92 @@
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: Unlicense
+
+# llxprt agent definition for synu
+# Provides model configuration for routing through Synthetic API
+# llxprt only accepts configuration via CLI flags
+
+# Source cache functions
+source (status dirname)/../_synu_cache.fish
+
+# Fallback default (used when no cache entry exists)
+set -g _synu_llxprt_fallback_model "hf:MiniMaxAI/MiniMax-M2"
+
+function _synu_llxprt_default --description "Get default model"
+    set -l cached (_synu_cache_get llxprt model)
+    if test $status -eq 0
+        echo $cached
+    else
+        echo $_synu_llxprt_fallback_model
+    end
+end
+
+function _synu_agent_llxprt_flags --description "Return argparse-compatible flag specification"
+    echo "m/model="
+end
+
+function _synu_agent_llxprt_configure --description "Configure llxprt model selection"
+    # Parse flags passed from main synu
+    argparse 'm/model=' -- $argv
+    or return 1
+
+    # Start with default (from cache or fallback)
+    set -g _synu_llxprt_selected_model (_synu_llxprt_default)
+
+    # Apply override if provided
+    if set -q _flag_model
+        set -g _synu_llxprt_selected_model $_flag_model
+    end
+end
+
+function _synu_agent_llxprt_args --description "Return CLI arguments to pass to llxprt"
+    # Return --provider, --baseurl, and --model flags
+    echo --provider
+    echo openai
+    echo --baseurl
+    echo "https://api.synthetic.new/openai/v1"
+    echo --model
+    echo $_synu_llxprt_selected_model
+end
+
+function _synu_agent_llxprt_interactive --description "Interactive model selection using gum"
+    # Check for gum
+    if not command -q gum
+        echo "Error: gum is required for interactive mode. Install: https://github.com/charmbracelet/gum" >&2
+        return 1
+    end
+
+    # Fetch available models
+    set -l models_json (gum spin --spinner dot --title "Fetching models..." -- \
+        curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
+        "https://api.synthetic.new/openai/v1/models")
+    or return 1
+
+    set -l model_names (echo $models_json | jq -r '.data[].name')
+    or return 1
+
+    # Select model
+    set -l model_name (printf "%s\n" $model_names | \
+        gum filter --limit 1 --header "Select model for llxprt" \
+        --placeholder "Filter models...")
+    or return 1
+
+    set -l model_id (echo $models_json | \
+        jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+
+    if test -z "$model_id"
+        echo "Error: Could not find model ID" >&2
+        return 1
+    end
+
+    # Build flags
+    set -l flags --model=$model_id
+
+    # Offer to save as default
+    if gum confirm "Save as default for 'llxprt'?"
+        _synu_cache_set llxprt model $model_id
+    end
+
+    # Output flags for caller to use
+    echo $flags
+end

functions/_synu_agents/qwen.fish 🔗

@@ -0,0 +1,93 @@
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: Unlicense
+
+# Qwen Code agent definition for synu
+# Provides model configuration for routing through Synthetic API
+# Qwen Code only accepts configuration via environment variables
+
+# Source cache functions
+source (status dirname)/../_synu_cache.fish
+
+# Fallback default (used when no cache entry exists)
+set -g _synu_qwen_fallback_model "hf:MiniMaxAI/MiniMax-M2"
+
+function _synu_qwen_default --description "Get default model"
+    set -l cached (_synu_cache_get qwen model)
+    if test $status -eq 0
+        echo $cached
+    else
+        echo $_synu_qwen_fallback_model
+    end
+end
+
+function _synu_agent_qwen_flags --description "Return argparse-compatible flag specification"
+    echo "m/model="
+end
+
+function _synu_agent_qwen_env_vars --description "Return list of environment variables set by configure"
+    echo OPENAI_API_KEY
+    echo OPENAI_BASE_URL
+    echo OPENAI_MODEL
+end
+
+function _synu_agent_qwen_configure --description "Configure Qwen Code environment variables"
+    # Parse flags passed from main synu
+    argparse 'm/model=' -- $argv
+    or return 1
+
+    # Start with default (from cache or fallback)
+    set -l model (_synu_qwen_default)
+
+    # Apply override if provided
+    if set -q _flag_model
+        set model $_flag_model
+    end
+
+    # Export environment variables for Qwen Code
+    set -gx OPENAI_API_KEY $SYNTHETIC_API_KEY
+    set -gx OPENAI_BASE_URL "https://api.synthetic.new/openai/v1"
+    set -gx OPENAI_MODEL $model
+end
+
+function _synu_agent_qwen_interactive --description "Interactive model selection using gum"
+    # Check for gum
+    if not command -q gum
+        echo "Error: gum is required for interactive mode. Install: https://github.com/charmbracelet/gum" >&2
+        return 1
+    end
+
+    # Fetch available models
+    set -l models_json (gum spin --spinner dot --title "Fetching models..." -- \
+        curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
+        "https://api.synthetic.new/openai/v1/models")
+    or return 1
+
+    set -l model_names (echo $models_json | jq -r '.data[].name')
+    or return 1
+
+    # Select model
+    set -l model_name (printf "%s\n" $model_names | \
+        gum filter --limit 1 --header "Select model for Qwen Code" \
+        --placeholder "Filter models...")
+    or return 1
+
+    set -l model_id (echo $models_json | \
+        jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+
+    if test -z "$model_id"
+        echo "Error: Could not find model ID" >&2
+        return 1
+    end
+
+    # Build flags
+    set -l flags --model=$model_id
+
+    # Offer to save as default
+    if gum confirm "Save as default for 'qwen'?"
+        _synu_cache_set qwen model $model_id
+    end
+
+    # Output flags for caller to use
+    echo $flags
+end