@@ -13,7 +13,8 @@ This document helps AI agents work effectively with the synu codebase.
**synu** is a universal wrapper for AI agents that tracks quota usage for [Synthetic](https://synthetic.new) API calls. It's written in Fish shell and provides:
- Transparent quota tracking before/after agent execution
-- Agent-specific configuration system (e.g., model routing for Claude Code)
+- 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
@@ -22,8 +23,13 @@ This document helps AI agents work effectively with the synu codebase.
```
synu (main wrapper)
├─ _synu_get_quota (API quota fetching)
+ ├─ _synu_cache (model preference persistence)
└─ _synu_agents/ (agent-specific configuration)
- └─ claude.fish (Claude Code model 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:**
@@ -32,12 +38,14 @@ synu (main wrapper)
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
-7. Fetch final quota and calculate/display session usage
+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
@@ -46,8 +54,13 @@ synu (main wrapper)
├── 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
+│ ├── 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)
@@ -65,6 +78,7 @@ This is a Fish shell library with no build system. Key commands:
# 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
@@ -74,10 +88,14 @@ 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
@@ -95,7 +113,8 @@ fundle init
# Validate Fish syntax
fish -n functions/synu.fish
fish -n functions/_synu_get_quota.fish
-fish -n functions/_synu_agents/claude.fish
+fish -n functions/_synu_cache.fish
+fish -n functions/_synu_agents/*.fish
fish -n completions/synu.fish
```
@@ -110,12 +129,13 @@ The project includes `crush.json` configuring `fish-lsp` for Fish language suppo
- **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)
+ - 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
@@ -123,30 +143,42 @@ The project includes `crush.json` configuring `fish-lsp` for Fish language suppo
### Agent Configuration Pattern
-Each agent definition in `functions/_synu_agents/<agent>.fish` can provide four functions:
+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)
```fish
function _synu_agent_myagent_flags
- echo "L/large="
- echo "o/option="
+ echo "m/model="
+ echo "e/extra="
end
```
-2. **`_synu_agent_<name>_configure`** - Sets environment variables before execution
+2. **`_synu_agent_<name>_configure`** - Sets environment variables and/or global state
```fish
function _synu_agent_myagent_configure
- argparse 'L/large=' 'o/option=' -- $argv
+ argparse 'm/model=' -- $argv
or return 1
- # Configure based on flags
- if set -q _flag_large
- set -gx AGENT_MODEL $_flag_large
+ # 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)
+ ```fish
+ function _synu_agent_myagent_args
+ echo --model
+ echo $_synu_myagent_selected_model
end
```
-3. **`_synu_agent_<name>_env_vars`** - Returns list of environment variables to clean up after execution
+4. **`_synu_agent_<name>_env_vars`** - Returns list of environment variables to clean up after execution
```fish
function _synu_agent_myagent_env_vars
echo AGENT_MODEL
@@ -154,24 +186,70 @@ Each agent definition in `functions/_synu_agents/<agent>.fish` can provide four
end
```
-4. **`_synu_agent_<name>_interactive`** - Implements interactive model selection
+5. **`_synu_agent_<name>_interactive`** - Implements interactive model selection
```fish
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 --option=$selection
+ 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:**
+```fish
+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
@@ -179,6 +257,7 @@ Each agent definition in `functions/_synu_agents/<agent>.fish` can provide four
- 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`
@@ -194,7 +273,9 @@ Each agent definition in `functions/_synu_agents/<agent>.fish` can provide four
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 $agent_args`, all original arguments (except synu-specific flags) must pass through unchanged.
+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
@@ -207,18 +288,27 @@ Each agent definition in `functions/_synu_agents/<agent>.fish` can provide four
### Environment Variable Scoping
- Use `set -gx` for environment variables that need to be visible to child processes (the actual agent binary)
-- Agent defaults use `set -g` (global but not exported)
+- 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. Recursively calling `synu <agent> <flags> [args...]` (non-interactive)
-3. The recursive call then follows normal path with pre-parsed 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`
@@ -226,6 +316,7 @@ This means the `_configure` function must handle both direct flag input and flag
- **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
@@ -234,15 +325,17 @@ 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 Claude with/without flags
-5. **Interactive mode**: Test `synu i claude` with gum installed
-6. **Error cases**: Test without `SYNTHETIC_API_KEY`, with invalid API key
+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
@@ -250,17 +343,22 @@ This project has no automated test suite. Testing is manual:
1. Create `functions/_synu_agents/<agent>.fish`
2. Add SPDX header
-3. Define `_synu_agent_<agent>_flags` (return argparse flag specs)
-4. Define `_synu_agent_<agent>_configure` (set environment variables)
-5. Optionally define `_synu_agent_<agent>_interactive` (for `synu i <agent>`)
-6. Update `completions/synu.fish` to add the new agent to suggestions
-7. Test manually
+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**: lines 6-28 of `synu.fish`
-- **Post-execution**: lines 128-166 of `synu.fish`
+- **No args case**: `synu.fish` lines ~7-29
+- **Post-execution**: `synu.fish` lines ~162-180
Both use the same color thresholds:
- Green: < 33% used
@@ -279,19 +377,50 @@ _synu_get_quota
# 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'
```
-### Understanding Model Configuration Flow (Claude)
+### Debugging Cache Issues
+
+```fish
+# 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: `L/large=`, `o/opus=`, etc.
+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 `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, etc.
-8. Executes `command claude "prompt"` with those environment variables
-9. Claude Code sees Synthetic models via env vars and uses them
+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
@@ -302,6 +431,7 @@ Use **Conventional Commits** with type and scope:
**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
@@ -311,6 +441,7 @@ Use **Conventional Commits** with type and scope:
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
```
@@ -338,26 +469,31 @@ Synthetic provides a unified API for multiple LLM providers with quota tracking.
- **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/`:
-### Known 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 |
-Suggested in completions (may or may not have specific configurations):
-- `claude` - Claude Code (has configuration)
-- `crush` - Crush agent (passthrough)
-- `amp` - Amp agent (passthrough)
-- `octo` - Octo agent (passthrough)
-- `codex` - Codex agent (passthrough)
+Passthrough agents (no config, just quota tracking): crush, amp, octo, codex
-Any command can be wrapped, these are just suggestions for completion.
+Any command can be wrapped; the above are just agents with explicit configuration.
## Future Considerations
Potential enhancements (not currently implemented):
-- Cache quota between rapid calls to reduce API hits
- Support for other quota tracking APIs besides Synthetic
- Agent-specific usage tracking/history
- Budget warnings/limits
-- More agent configurations (crush, amp, etc.)
+- More agent configurations
When implementing features, maintain the core principles:
- Don't break passthrough mode
@@ -46,8 +46,10 @@ Use `synu` as a wrapper for any AI agent:
# Check current quota
synu
-# Use with claude (auto-configured for Synthetic)
+# Use with configured agents (auto-routed through Synthetic)
synu claude "What does functions/synu.fish do?"
+synu opencode "Help me refactor this"
+synu aider "Fix the bug in main.go"
# Use with other agents (passthrough with quota tracking)
synu crush "Help me write code"
@@ -56,18 +58,36 @@ synu crush "Help me write code"
synu [agent-name] [agent-args...]
```
-### Claude Code Configuration
+> **Note**: synu's configuration is ephemeral and non-invasive. Running `synu claude` routes requests through Synthetic, but running `claude` directly still uses Anthropic's API with your normal configuration. synu never modifies the agent's own config files.
-When using Claude Code through synu, the following models are configured by default:
+### Interactive Model Selection
-| Tier | Model |
-|------|-------|
+Use `synu i <agent>` to interactively select models using gum:
+
+```fish
+synu i claude "prompt"
+synu i opencode "prompt"
+synu i aider "prompt"
+```
+
+This presents a TUI to filter and select from available Synthetic models. You'll be prompted to save your selection as the default for future sessions.
+
+### Persistent Preferences
+
+Model selections made in interactive mode can be saved to `~/.config/synu/models.conf`. These become the new defaults until changed. Command-line flags always override saved preferences.
+
+## Configured Agents
+
+### Claude Code
+
+| Tier | Default Model |
+|------|---------------|
| Opus | `hf:moonshotai/Kimi-K2-Thinking` |
| Sonnet | `hf:MiniMaxAI/MiniMax-M2` |
| Haiku | `hf:deepseek-ai/DeepSeek-V3.1-Terminus` |
| Subagent | `hf:MiniMaxAI/MiniMax-M2` |
-#### Override Models with Flags
+**Override flags:**
```fish
# Override specific models
@@ -81,41 +101,81 @@ synu claude --large hf:model "prompt" # Sets Opus, Sonnet, and Subagent
synu claude --light hf:model "prompt" # Sets Haiku
```
-#### Interactive Model Selection
+### OpenCode
-Use `synu i <agent>` to interactively select models using gum:
+| Default Model |
+|---------------|
+| `hf:MiniMaxAI/MiniMax-M2` |
```fish
-synu i claude "prompt"
+synu opencode --model hf:other/model "prompt"
```
-This will present a TUI to choose between group or individual model selection, then let you filter and select from available Synthetic models.
+### Aider
+
+| Slot | Default Model |
+|------|---------------|
+| Main | `hf:MiniMaxAI/MiniMax-M2` |
+| Editor | (none - single model mode) |
-### How it works
+```fish
+# Single model mode
+synu aider --model hf:some/model "prompt"
+
+# Architect + editor mode (two models)
+synu aider --model hf:architect/model --editor-model hf:editor/model "prompt"
+```
+
+### llxprt
+
+| Default Model |
+|---------------|
+| `hf:MiniMaxAI/MiniMax-M2` |
+
+```fish
+synu llxprt --model hf:other/model "prompt"
+```
+
+### Qwen Code
+
+| Default Model |
+|---------------|
+| `hf:MiniMaxAI/MiniMax-M2` |
+
+```fish
+synu qwen --model hf:other/model "prompt"
+```
+
+## How it works
`synu` works by:
-1. Loading agent-specific configuration if available (e.g., Claude Code model defaults)
+1. Loading agent-specific configuration if available
2. Fetching initial quota from the Synthetic API before running the agent
-3. Executing the specified agent with all provided arguments
-4. Fetching final quota after the agent completes
-5. Calculating and displaying session usage
+3. Configuring the agent's environment/CLI args to route through Synthetic
+4. Executing the specified agent with all provided arguments
+5. Cleaning up environment variables after execution
+6. Fetching final quota and displaying session usage
-The quota tracking requires the `SYNTHETIC_API_KEY` environment variable to be set. Without it, `synu` will still work but will show a warning and skip quota tracking.
+The quota tracking requires the `SYNTHETIC_API_KEY` environment variable. Without it, `synu` will show a warning and skip quota tracking, but still attempt to run the agent.
### Adding Support for New Agents
Agent definitions are stored in `functions/_synu_agents/`. Each agent file can define:
- `_synu_agent_<name>_flags` - Returns argparse flag specification
-- `_synu_agent_<name>_configure` - Sets environment variables before execution
+- `_synu_agent_<name>_configure` - Sets environment variables/state before execution
+- `_synu_agent_<name>_args` - Returns CLI arguments to inject
+- `_synu_agent_<name>_env_vars` - Lists environment variables to clean up
- `_synu_agent_<name>_interactive` - Implements interactive model selection
Agents without definitions work as passthrough with quota tracking.
+See `AGENTS.md` for detailed implementation guidance.
+
## Shell completions
-Synu includes fish shell completions for better usability.
+Synu includes fish shell completions for configured agents and their flags.
## Contributions
@@ -144,4 +204,4 @@ See "How do Patch Requests work?" on [pr.pico.sh]'s home page for a more
complete example workflow.
[amolith/llm-projects]: https://pr.pico.sh/r/amolith/llm-projects
-[pr.pico.sh]: https://pr.pico.sh
+[pr.pico.sh]: https://pr.pico.sh