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:
- Parse arguments (check for interactive mode, agent name)
- Load agent definition from
functions/_synu_agents/<agent>.fishif it exists - Parse agent-specific flags using
argparsewith--ignore-unknownfor passthrough - Configure agent environment (if agent has
_configurefunction) - Fetch initial quota from Synthetic API
- Execute agent with remaining arguments (plus any
_argsoutput) - Clean up environment variables (if agent has
_env_varsfunction) - 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_configurefunction - Cache file →
_synu_cache_get→ default model values - Interactive mode →
gum+ Synthetic API → model IDs → recursivesynucall 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_casewith 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)
- Private/local vars:
- Function naming:
- Public:
synu - Private/helper:
_synu_* - Agent-specific:
_synu_agent_<name>_<action> - Agent internal helpers:
_synu_<agent>_<helper>(e.g.,_synu_claude_default)
- Public:
- Comments: Use
#for explanatory comments focusing on why not what - Error handling: Check
$statusafter critical operations, return non-zero on errors - Error output: Use
>&2for 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:
-
_synu_agent_<name>_flags- Returns argparse flag specification (one per line)function _synu_agent_myagent_flags echo "m/model=" echo "e/extra=" end -
_synu_agent_<name>_configure- Sets environment variables and/or global statefunction _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 -
_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 -
_synu_agent_<name>_env_vars- Returns list of environment variables to clean up after executionfunction _synu_agent_myagent_env_vars echo AGENT_MODEL echo AGENT_CONFIG end -
_synu_agent_<name>_interactive- Implements interactive model selectionfunction _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-unknownin main wrapper so agent-native flags pass through - Interactive functions return flags that trigger recursive
synucall - Configure functions receive flags already parsed from
_flag_*variables - Environment variables listed in
_env_varsare automatically unset after agent exits - Use
_synu_cache_get/_synu_cache_setfor 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
-
Passthrough requires
--ignore-unknown: When parsing agent flags, useargparse --ignore-unknownso agent-native flags aren't consumed by synu's parser. -
Flag variables after argparse: After
argparse, the$argvvariable contains only non-flag arguments. Rebuild flag arguments from_flag_*variables to pass to configure function. -
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. -
The
_argsfunction 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
-
API failures are non-fatal: If quota fetch fails before/after execution, warn but continue. The agent should still run.
-
Default to zeros on failure: If pre-execution quota fetch fails, use
"0 0"to avoid math errors in post-execution display. -
Exit status preservation: Always return the agent's exit status (
$exit_status), not the quota fetch status.
Environment Variable Scoping
- Use
set -gxfor 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:
- Calling agent's
_interactivefunction to get flags - Optionally saving selections to cache (user prompted)
- Recursively calling
synu <agent> <flags> [args...](non-interactive) - 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 0immediately 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
stringbuiltin (string split,string match) - No
localkeyword: Useset -lfor local scope - Variable indirection: Use
$$var_nameto get value of variable named by$var_name
Testing Approach
This project has no automated test suite. Testing is manual:
- Syntax validation: Run
fish -non all.fishfiles - Quota tracking: Run
synualone, check quota display with color - Passthrough: Test with unknown agent (
synu echo "hello") - Agent config: Test each agent with/without flags
- Interactive mode: Test
synu i <agent>with gum installed - Cache persistence: Test that interactive selections save correctly
- 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
- Create
functions/_synu_agents/<agent>.fish - Add SPDX header
- Source cache functions:
source (status dirname)/../_synu_cache.fish - Define fallback default:
set -g _synu_<agent>_fallback_model "hf:..." - Define
_synu_<agent>_defaulthelper (uses cache with fallback) - Define
_synu_agent_<agent>_flags(return argparse flag specs) - Define
_synu_agent_<agent>_configure(set state/env vars) - Define
_synu_agent_<agent>_args(if agent uses CLI args for model) - Define
_synu_agent_<agent>_env_vars(if agent uses env vars) - Optionally define
_synu_agent_<agent>_interactive(forsynu i <agent>) - Update
completions/synu.fishto add the new agent to suggestions - Test manually
Modifying Quota Display
Quota display logic is in two places:
- No args case:
synu.fishlines ~7-29 - Post-execution:
synu.fishlines ~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
- User runs:
synu claude --large hf:some/model "prompt" synu.fishloadsfunctions/_synu_agents/claude.fish- Calls
_synu_agent_claude_flagsto get flag spec - Parses with
argparse --ignore-unknown→ sets_flag_large - Rebuilds flags for configure:
--large=hf:some/model - Calls
_synu_agent_claude_configure --large=hf:some/model - Configure sets opus/sonnet/subagent models from flag
- Exports
ANTHROPIC_*environment variables - Executes
command claude "prompt"with those environment variables - Claude Code sees Synthetic models via env vars and uses them
- After exit, env vars are cleaned up via
_env_vars
Example: OpenCode with CLI args
- User runs:
synu opencode "prompt" synu.fishloadsfunctions/_synu_agents/opencode.fish_configuresets_synu_opencode_selected_modelfrom cache/fallback_argsreturns:--model synthetic/hf:MiniMaxAI/MiniMax-M2- 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 functionquota- quota fetching/displaycache- model preference cachingagents/<name>- specific agent configuration (e.g.,agents/claude)completions- shell completionsdocs- 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: HASHfor 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