AGENTS.md

AGENTS.md - Developer Guide for synu

This document helps AI agents work effectively with the synu codebase.

Project Overview

synu is a universal wrapper for AI agents that tracks quota usage for Synthetic API calls. It's written in Fish shell and provides:

  • Transparent quota tracking before/after agent execution
  • Agent-specific configuration system (model routing, API endpoints)
  • Persistent model preferences via cache file
  • Interactive model selection with gum
  • Passthrough mode for agents without special configuration

Architecture

synu (main wrapper)
  ├─ _synu_get_quota (API quota fetching)
  ├─ _synu_cache (model preference persistence)
  └─ _synu_agents/ (agent-specific configuration)
      ├─ claude.fish  (Claude Code: env vars, multi-model)
      ├─ opencode.fish (OpenCode: CLI args)
      ├─ aider.fish    (Aider: env vars + CLI args)
      ├─ llxprt.fish   (llxprt: CLI args only)
      └─ qwen.fish     (Qwen Code: env vars)

Control Flow:

  1. Parse arguments (check for interactive mode, agent name)
  2. Load agent definition from functions/_synu_agents/<agent>.fish if it exists
  3. Parse agent-specific flags using argparse with --ignore-unknown for passthrough
  4. Configure agent environment (if agent has _configure function)
  5. Fetch initial quota from Synthetic API
  6. Execute agent with remaining arguments (plus any _args output)
  7. Clean up environment variables (if agent has _env_vars function)
  8. Fetch final quota and calculate/display session usage

Data Flow:

  • Quota API → _synu_get_quota → space-separated "requests limit" string
  • Agent flags → argparse_flag_* variables → agent _configure function
  • Cache file → _synu_cache_get → default model values
  • Interactive mode → gum + Synthetic API → model IDs → recursive synu call with flags

File Structure

├── functions/
│   ├── synu.fish              # Main wrapper function
│   ├── _synu_get_quota.fish   # Private: Fetch quota from API
│   ├── _synu_cache.fish       # Private: Model preference cache
│   └── _synu_agents/
│       ├── claude.fish        # Claude Code agent configuration
│       ├── opencode.fish      # OpenCode agent configuration
│       ├── aider.fish         # Aider agent configuration
│       ├── llxprt.fish        # llxprt agent configuration
│       └── qwen.fish          # Qwen Code agent configuration
├── completions/
│   └── synu.fish              # Shell completions
├── crush.json                 # LSP configuration (fish-lsp)
├── README.md                  # User documentation
└── .gitignore                 # Ignore agent working directories

Essential Commands

This is a Fish shell library with no build system. Key commands:

Testing Manually

# Source the function (from repo root)
source functions/synu.fish
source functions/_synu_get_quota.fish
source functions/_synu_cache.fish

# Check quota only
synu

# Test with an agent
synu claude "What does this function do?"

# Test interactive mode (requires gum)
synu i claude "prompt"
synu i opencode "prompt"
synu i aider "prompt"

# Test with model override flags
synu claude --opus hf:some/model "prompt"
synu claude --large hf:some/model "prompt"
synu opencode --model hf:some/model "prompt"
synu aider --model hf:some/model --editor-model hf:other/model "prompt"

Installation Testing

# Install via fundle (in a clean fish instance)
fundle plugin 'synu' --url 'https://git.secluded.site/amolith/llm-projects'
fundle install
fundle init

Checking for Syntax Errors

# Validate Fish syntax
fish -n functions/synu.fish
fish -n functions/_synu_get_quota.fish
fish -n functions/_synu_cache.fish
fish -n functions/_synu_agents/*.fish
fish -n completions/synu.fish

LSP Integration

The project includes crush.json configuring fish-lsp for Fish language support. The LSP should automatically provide diagnostics when editing .fish files.

Code Conventions

Fish Shell Style

  • Indentation: 4 spaces (no tabs)
  • Variable naming: snake_case with descriptive names
    • Private/local vars: set -l var_name
    • Global vars: set -g var_name (agent defaults, selected models)
    • Exported vars: set -gx VAR_NAME (environment)
  • Function naming:
    • Public: synu
    • Private/helper: _synu_*
    • Agent-specific: _synu_agent_<name>_<action>
    • Agent internal helpers: _synu_<agent>_<helper> (e.g., _synu_claude_default)
  • Comments: Use # for explanatory comments focusing on why not what
  • Error handling: Check $status after critical operations, return non-zero on errors
  • Error output: Use >&2 for all error messages
  • SPDX headers: All files must include SPDX copyright and license headers

Agent Configuration Pattern

Each agent definition in functions/_synu_agents/<agent>.fish can provide five functions:

  1. _synu_agent_<name>_flags - Returns argparse flag specification (one per line)

    function _synu_agent_myagent_flags
        echo "m/model="
        echo "e/extra="
    end
    
  2. _synu_agent_<name>_configure - Sets environment variables and/or global state

    function _synu_agent_myagent_configure
        argparse 'm/model=' -- $argv
        or return 1
    
        # Configure based on flags (use cache or fallback for defaults)
        set -g _synu_myagent_selected_model (_synu_myagent_default)
        if set -q _flag_model
            set -g _synu_myagent_selected_model $_flag_model
        end
    
        # Export env vars if agent uses them
        set -gx AGENT_MODEL $_synu_myagent_selected_model
    end
    
  3. _synu_agent_<name>_args - Returns CLI arguments to inject (for agents that don't use env vars)

    function _synu_agent_myagent_args
        echo --model
        echo $_synu_myagent_selected_model
    end
    
  4. _synu_agent_<name>_env_vars - Returns list of environment variables to clean up after execution

    function _synu_agent_myagent_env_vars
        echo AGENT_MODEL
        echo AGENT_CONFIG
    end
    
  5. _synu_agent_<name>_interactive - Implements interactive model selection

    function _synu_agent_myagent_interactive
        # Use gum to get user input
        set -l selection (gum choose "option1" "option2")
        or return 1
    
        # Optionally save to cache
        if gum confirm "Save as default?"
            _synu_cache_set myagent model $selection
        end
    
        # Return flags to pass to main synu call
        echo --model=$selection
    end
    

Agent Configuration Patterns:

Agent Configuration Method Notes
claude Environment variables Multi-model (opus, sonnet, haiku, subagent)
qwen Environment variables Single model
aider Env vars + CLI args Env for API, CLI for models
opencode CLI args only Uses synthetic/ prefix in model
llxprt CLI args only Needs --provider, --baseurl flags

Important patterns:

  • Agents without definitions work as passthrough (no special config)
  • Use argparse --ignore-unknown in main wrapper so agent-native flags pass through
  • Interactive functions return flags that trigger recursive synu call
  • Configure functions receive flags already parsed from _flag_* variables
  • Environment variables listed in _env_vars are automatically unset after agent exits
  • Use _synu_cache_get/_synu_cache_set for persistent model preferences

Cache System

The cache stores model preferences in $XDG_CONFIG_HOME/synu/models.conf (or ~/.config/synu/models.conf).

Format:

# synu model preferences
# Format: agent.slot = model_id

claude.opus = hf:moonshotai/Kimi-K2-Thinking
claude.sonnet = hf:MiniMaxAI/MiniMax-M2
opencode.model = hf:MiniMaxAI/MiniMax-M2

Functions:

  • _synu_cache_get agent slot - Returns cached value or fails
  • _synu_cache_set agent slot value - Creates/updates cache entry

Pattern for using cache with fallbacks:

function _synu_myagent_default --description "Get default model"
    set -l cached (_synu_cache_get myagent model)
    if test $status -eq 0
        echo $cached
    else
        echo $_synu_myagent_fallback_model  # Hardcoded fallback
    end
end

API Integration

Synthetic API endpoints:

  • Quota: GET https://api.synthetic.new/v2/quotas
  • Models: GET https://api.synthetic.new/openai/v1/models
  • Claude routing: https://api.synthetic.new/anthropic
  • OpenAI-compatible: https://api.synthetic.new/openai/v1

Authentication: Bearer token via Authorization: Bearer $SYNTHETIC_API_KEY

Response parsing: Use jq with fallbacks (// 0) for missing fields

Error handling: Check curl exit status and response emptiness before parsing

Important Gotchas

Argument Passing and argparse

  1. Passthrough requires --ignore-unknown: When parsing agent flags, use argparse --ignore-unknown so agent-native flags aren't consumed by synu's parser.

  2. Flag variables after argparse: After argparse, the $argv variable contains only non-flag arguments. Rebuild flag arguments from _flag_* variables to pass to configure function.

  3. Preserve argument order: When executing the agent with command $agent $extra_args $agent_args, all original arguments (except synu-specific flags) must pass through unchanged.

  4. The _args function output is inserted before user args: This allows agent-specific flags to be set while still allowing user to pass additional flags.

Quota Tracking Edge Cases

  1. API failures are non-fatal: If quota fetch fails before/after execution, warn but continue. The agent should still run.

  2. Default to zeros on failure: If pre-execution quota fetch fails, use "0 0" to avoid math errors in post-execution display.

  3. Exit status preservation: Always return the agent's exit status ($exit_status), not the quota fetch status.

Environment Variable Scoping

  • Use set -gx for environment variables that need to be visible to child processes (the actual agent binary)
  • Agent defaults and selected models use set -g (global but not exported)
  • Local calculations use set -l
  • Environment variables are cleaned up after execution via _env_vars

Interactive Mode Flow

Interactive mode (synu i <agent> [args...]) works by:

  1. Calling agent's _interactive function to get flags
  2. Optionally saving selections to cache (user prompted)
  3. Recursively calling synu <agent> <flags> [args...] (non-interactive)
  4. The recursive call then follows normal path with pre-parsed flags

This means the _configure function must handle both direct flag input and flags from interactive mode identically.

Model ID Prefixes

Different agents expect different model ID formats:

  • opencode: Prefix with synthetic/ (e.g., synthetic/hf:moonshotai/Kimi-K2)
  • aider: Prefix with openai/ (e.g., openai/hf:moonshotai/Kimi-K2)
  • claude, qwen, llxprt: Use raw model ID (e.g., hf:moonshotai/Kimi-K2)

Fish-specific Considerations

  • Status checks: Always use test $status -ne 0 immediately after command, as any subsequent command overwrites $status
  • Array slicing: Use $argv[2..-1] syntax (1-indexed, inclusive)
  • Command substitution: Use (command) not $(command)
  • String operations: Use string builtin (string split, string match)
  • No local keyword: Use set -l for local scope
  • Variable indirection: Use $$var_name to get value of variable named by $var_name

Testing Approach

This project has no automated test suite. Testing is manual:

  1. Syntax validation: Run fish -n on all .fish files
  2. Quota tracking: Run synu alone, check quota display with color
  3. Passthrough: Test with unknown agent (synu echo "hello")
  4. Agent config: Test each agent with/without flags
  5. Interactive mode: Test synu i <agent> with gum installed
  6. Cache persistence: Test that interactive selections save correctly
  7. Error cases: Test without SYNTHETIC_API_KEY, with invalid API key

When making changes:

  • Validate syntax with fish -n
  • Source the function and test manually with different argument combinations
  • Check that exit status is preserved
  • Verify quota display shows correct before/after values
  • Test cache file creation and updates

Common Tasks

Adding a New Agent Configuration

  1. Create functions/_synu_agents/<agent>.fish
  2. Add SPDX header
  3. Source cache functions: source (status dirname)/../_synu_cache.fish
  4. Define fallback default: set -g _synu_<agent>_fallback_model "hf:..."
  5. Define _synu_<agent>_default helper (uses cache with fallback)
  6. Define _synu_agent_<agent>_flags (return argparse flag specs)
  7. Define _synu_agent_<agent>_configure (set state/env vars)
  8. Define _synu_agent_<agent>_args (if agent uses CLI args for model)
  9. Define _synu_agent_<agent>_env_vars (if agent uses env vars)
  10. Optionally define _synu_agent_<agent>_interactive (for synu i <agent>)
  11. Update completions/synu.fish to add the new agent to suggestions
  12. Test manually

Modifying Quota Display

Quota display logic is in two places:

  • No args case: synu.fish lines ~7-29
  • Post-execution: synu.fish lines ~162-180

Both use the same color thresholds:

  • Green: < 33% used
  • Yellow: 33-66% used
  • Red: > 66% used

Debugging API Issues

# Test quota fetch directly
source functions/_synu_get_quota.fish
set -gx SYNTHETIC_API_KEY your_key_here
_synu_get_quota
# Should output: "requests_used limit"

# Test with curl directly
curl -s -f -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
    "https://api.synthetic.new/v2/quotas" | jq .

# Test model listing
curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
    "https://api.synthetic.new/openai/v1/models" | jq '.data[].name'

Debugging Cache Issues

# Check cache file location
echo $XDG_CONFIG_HOME/synu/models.conf
# or
echo ~/.config/synu/models.conf

# View cache contents
cat ~/.config/synu/models.conf

# Test cache functions
source functions/_synu_cache.fish
_synu_cache_set test model hf:test/model
_synu_cache_get test model

Understanding Model Configuration Flow

Example: Claude with flag override

  1. User runs: synu claude --large hf:some/model "prompt"
  2. synu.fish loads functions/_synu_agents/claude.fish
  3. Calls _synu_agent_claude_flags to get flag spec
  4. Parses with argparse --ignore-unknown → sets _flag_large
  5. Rebuilds flags for configure: --large=hf:some/model
  6. Calls _synu_agent_claude_configure --large=hf:some/model
  7. Configure sets opus/sonnet/subagent models from flag
  8. Exports ANTHROPIC_* environment variables
  9. Executes command claude "prompt" with those environment variables
  10. Claude Code sees Synthetic models via env vars and uses them
  11. After exit, env vars are cleaned up via _env_vars

Example: OpenCode with CLI args

  1. User runs: synu opencode "prompt"
  2. synu.fish loads functions/_synu_agents/opencode.fish
  3. _configure sets _synu_opencode_selected_model from cache/fallback
  4. _args returns: --model synthetic/hf:MiniMaxAI/MiniMax-M2
  5. Executes: command opencode -m "synthetic/hf:..." "prompt"

Commit Conventions

Use Conventional Commits with type and scope:

Types: feat, fix, refactor, docs, chore, test

Scopes:

  • synu - main wrapper function
  • quota - quota fetching/display
  • cache - model preference caching
  • agents/<name> - specific agent configuration (e.g., agents/claude)
  • completions - shell completions
  • docs - documentation

Examples:

feat(synu): add support for model override flags
fix(quota): handle API errors gracefully
feat(agents/claude): add interactive model selection
feat(cache): implement persistent model preferences
docs(readme): document interactive mode

Trailers:

  • Use References: HASH for related commits/bugs
  • Always include Assisted-by: [Model Name] via [Tool Name] when AI-assisted

Project-Specific Context

Why Fish Shell?

This is Amolith's personal project and Fish is their shell of choice. Fish provides:

  • Clean syntax without bashisms
  • Better error handling than POSIX shells
  • Native arrays and proper scoping
  • Built-in string manipulation

Why Synthetic API?

Synthetic provides a unified API for multiple LLM providers with quota tracking. This wrapper makes quota visibility automatic and consistent across different agent tools.

Design Philosophy

  • Minimal interference: Pass through as much as possible to the underlying agent
  • Graceful degradation: If quota tracking fails, still run the agent
  • Extensible: Easy to add new agent configurations without modifying core wrapper
  • Interactive when helpful: Support both CLI flags and TUI selection
  • Persistent preferences: Save model selections for future sessions

Configured Agents

Agents with specific configurations in functions/_synu_agents/:

Agent Models Configuration Method Interactive
claude opus, sonnet, haiku, subagent Environment variables Yes
opencode single CLI args Yes
aider main, editor Env + CLI args Yes
llxprt single CLI args Yes
qwen single Environment variables Yes

Passthrough agents (no config, just quota tracking): crush, amp, octo, codex

Any command can be wrapped; the above are just agents with explicit configuration.

Future Considerations

Potential enhancements (not currently implemented):

  • Support for other quota tracking APIs besides Synthetic
  • Agent-specific usage tracking/history
  • Budget warnings/limits
  • More agent configurations

When implementing features, maintain the core principles:

  • Don't break passthrough mode
  • Keep it simple and transparent
  • Graceful degradation
  • Follow Fish idioms