1<!--
2SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
3
4SPDX-License-Identifier: Unlicense
5-->
6
7# AGENTS.md - Developer Guide for synu
8
9This document helps AI agents work effectively with the synu codebase.
10
11## Project Overview
12
13**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:
14
15- Transparent quota tracking before/after agent execution
16- Agent-specific configuration system (model routing, API endpoints)
17- Persistent model preferences via cache file
18- Interactive model selection with `gum`
19- Passthrough mode for agents without special configuration
20
21### Architecture
22
23```
24synu (main wrapper)
25 ├─ _synu_get_quota (API quota fetching)
26 ├─ _synu_cache (model preference persistence)
27 └─ _synu_agents/ (agent-specific configuration)
28 ├─ claude.fish (Claude Code: env vars, multi-model)
29 ├─ opencode.fish (OpenCode: CLI args)
30 ├─ aider.fish (Aider: env vars + CLI args)
31 ├─ llxprt.fish (llxprt: CLI args only)
32 └─ qwen.fish (Qwen Code: env vars)
33```
34
35**Control Flow:**
361. Parse arguments (check for interactive mode, agent name)
372. Load agent definition from `functions/_synu_agents/<agent>.fish` if it exists
383. Parse agent-specific flags using `argparse` with `--ignore-unknown` for passthrough
394. Configure agent environment (if agent has `_configure` function)
405. Fetch initial quota from Synthetic API
416. Execute agent with remaining arguments (plus any `_args` output)
427. Clean up environment variables (if agent has `_env_vars` function)
438. Fetch final quota and calculate/display session usage
44
45**Data Flow:**
46- Quota API → `_synu_get_quota` → space-separated "requests limit" string
47- Agent flags → `argparse` → `_flag_*` variables → agent `_configure` function
48- Cache file → `_synu_cache_get` → default model values
49- Interactive mode → `gum` + Synthetic API → model IDs → recursive `synu` call with flags
50
51## File Structure
52
53```
54├── functions/
55│ ├── synu.fish # Main wrapper function
56│ ├── _synu_get_quota.fish # Private: Fetch quota from API
57│ ├── _synu_cache.fish # Private: Model preference cache
58│ └── _synu_agents/
59│ ├── claude.fish # Claude Code agent configuration
60│ ├── opencode.fish # OpenCode agent configuration
61│ ├── aider.fish # Aider agent configuration
62│ ├── llxprt.fish # llxprt agent configuration
63│ └── qwen.fish # Qwen Code agent configuration
64├── completions/
65│ └── synu.fish # Shell completions
66├── crush.json # LSP configuration (fish-lsp)
67├── README.md # User documentation
68└── .gitignore # Ignore agent working directories
69```
70
71## Essential Commands
72
73This is a Fish shell library with no build system. Key commands:
74
75### Testing Manually
76
77```fish
78# Source the function (from repo root)
79source functions/synu.fish
80source functions/_synu_get_quota.fish
81source functions/_synu_cache.fish
82
83# Check quota only
84synu
85
86# Test with an agent
87synu claude "What does this function do?"
88
89# Test interactive mode (requires gum)
90synu i claude "prompt"
91synu i opencode "prompt"
92synu i aider "prompt"
93
94# Test with model override flags
95synu claude --opus hf:some/model "prompt"
96synu claude --large hf:some/model "prompt"
97synu opencode --model hf:some/model "prompt"
98synu aider --model hf:some/model --editor-model hf:other/model "prompt"
99```
100
101### Installation Testing
102
103```fish
104# Install via fundle (in a clean fish instance)
105fundle plugin 'synu' --url 'https://git.secluded.site/amolith/llm-projects'
106fundle install
107fundle init
108```
109
110### Checking for Syntax Errors
111
112```fish
113# Validate Fish syntax
114fish -n functions/synu.fish
115fish -n functions/_synu_get_quota.fish
116fish -n functions/_synu_cache.fish
117fish -n functions/_synu_agents/*.fish
118fish -n completions/synu.fish
119```
120
121### LSP Integration
122
123The project includes `crush.json` configuring `fish-lsp` for Fish language support. The LSP should automatically provide diagnostics when editing `.fish` files.
124
125## Code Conventions
126
127### Fish Shell Style
128
129- **Indentation**: 4 spaces (no tabs)
130- **Variable naming**: `snake_case` with descriptive names
131 - Private/local vars: `set -l var_name`
132 - Global vars: `set -g var_name` (agent defaults, selected models)
133 - Exported vars: `set -gx VAR_NAME` (environment)
134- **Function naming**:
135 - Public: `synu`
136 - Private/helper: `_synu_*`
137 - Agent-specific: `_synu_agent_<name>_<action>`
138 - Agent internal helpers: `_synu_<agent>_<helper>` (e.g., `_synu_claude_default`)
139- **Comments**: Use `#` for explanatory comments focusing on *why* not *what*
140- **Error handling**: Check `$status` after critical operations, return non-zero on errors
141- **Error output**: Use `>&2` for all error messages
142- **SPDX headers**: All files must include SPDX copyright and license headers
143
144### Agent Configuration Pattern
145
146Each agent definition in `functions/_synu_agents/<agent>.fish` can provide five functions:
147
1481. **`_synu_agent_<name>_flags`** - Returns argparse flag specification (one per line)
149 ```fish
150 function _synu_agent_myagent_flags
151 echo "m/model="
152 echo "e/extra="
153 end
154 ```
155
1562. **`_synu_agent_<name>_configure`** - Sets environment variables and/or global state
157 ```fish
158 function _synu_agent_myagent_configure
159 argparse 'm/model=' -- $argv
160 or return 1
161
162 # Configure based on flags (use cache or fallback for defaults)
163 set -g _synu_myagent_selected_model (_synu_myagent_default)
164 if set -q _flag_model
165 set -g _synu_myagent_selected_model $_flag_model
166 end
167
168 # Export env vars if agent uses them
169 set -gx AGENT_MODEL $_synu_myagent_selected_model
170 end
171 ```
172
1733. **`_synu_agent_<name>_args`** - Returns CLI arguments to inject (for agents that don't use env vars)
174 ```fish
175 function _synu_agent_myagent_args
176 echo --model
177 echo $_synu_myagent_selected_model
178 end
179 ```
180
1814. **`_synu_agent_<name>_env_vars`** - Returns list of environment variables to clean up after execution
182 ```fish
183 function _synu_agent_myagent_env_vars
184 echo AGENT_MODEL
185 echo AGENT_CONFIG
186 end
187 ```
188
1895. **`_synu_agent_<name>_interactive`** - Implements interactive model selection
190 ```fish
191 function _synu_agent_myagent_interactive
192 # Use gum to get user input
193 set -l selection (gum choose "option1" "option2")
194 or return 1
195
196 # Optionally save to cache
197 if gum confirm "Save as default?"
198 _synu_cache_set myagent model $selection
199 end
200
201 # Return flags to pass to main synu call
202 echo --model=$selection
203 end
204 ```
205
206**Agent Configuration Patterns:**
207
208| Agent | Configuration Method | Notes |
209|-------|---------------------|-------|
210| claude | Environment variables | Multi-model (opus, sonnet, haiku, subagent) |
211| qwen | Environment variables | Single model |
212| aider | Env vars + CLI args | Env for API, CLI for models |
213| opencode | CLI args only | Uses `synthetic/` prefix in model |
214| llxprt | CLI args only | Needs --provider, --baseurl flags |
215
216**Important patterns:**
217- Agents without definitions work as passthrough (no special config)
218- Use `argparse --ignore-unknown` in main wrapper so agent-native flags pass through
219- Interactive functions return flags that trigger recursive `synu` call
220- Configure functions receive flags already parsed from `_flag_*` variables
221- Environment variables listed in `_env_vars` are automatically unset after agent exits
222- Use `_synu_cache_get`/`_synu_cache_set` for persistent model preferences
223
224### Cache System
225
226The cache stores model preferences in `$XDG_CONFIG_HOME/synu/models.conf` (or `~/.config/synu/models.conf`).
227
228**Format:**
229```
230# synu model preferences
231# Format: agent.slot = model_id
232
233claude.opus = hf:moonshotai/Kimi-K2-Thinking
234claude.sonnet = hf:MiniMaxAI/MiniMax-M2
235opencode.model = hf:MiniMaxAI/MiniMax-M2
236```
237
238**Functions:**
239- `_synu_cache_get agent slot` - Returns cached value or fails
240- `_synu_cache_set agent slot value` - Creates/updates cache entry
241
242**Pattern for using cache with fallbacks:**
243```fish
244function _synu_myagent_default --description "Get default model"
245 set -l cached (_synu_cache_get myagent model)
246 if test $status -eq 0
247 echo $cached
248 else
249 echo $_synu_myagent_fallback_model # Hardcoded fallback
250 end
251end
252```
253
254### API Integration
255
256**Synthetic API endpoints:**
257- Quota: `GET https://api.synthetic.new/v2/quotas`
258- Models: `GET https://api.synthetic.new/openai/v1/models`
259- Claude routing: `https://api.synthetic.new/anthropic`
260- OpenAI-compatible: `https://api.synthetic.new/openai/v1`
261
262**Authentication**: Bearer token via `Authorization: Bearer $SYNTHETIC_API_KEY`
263
264**Response parsing**: Use `jq` with fallbacks (`// 0`) for missing fields
265
266**Error handling**: Check `curl` exit status and response emptiness before parsing
267
268## Important Gotchas
269
270### Argument Passing and argparse
271
2721. **Passthrough requires `--ignore-unknown`**: When parsing agent flags, use `argparse --ignore-unknown` so agent-native flags aren't consumed by synu's parser.
273
2742. **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.
275
2763. **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.
277
2784. **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.
279
280### Quota Tracking Edge Cases
281
2821. **API failures are non-fatal**: If quota fetch fails before/after execution, warn but continue. The agent should still run.
283
2842. **Default to zeros on failure**: If pre-execution quota fetch fails, use `"0 0"` to avoid math errors in post-execution display.
285
2863. **Exit status preservation**: Always return the agent's exit status (`$exit_status`), not the quota fetch status.
287
288### Environment Variable Scoping
289
290- Use `set -gx` for environment variables that need to be visible to child processes (the actual agent binary)
291- Agent defaults and selected models use `set -g` (global but not exported)
292- Local calculations use `set -l`
293- Environment variables are cleaned up after execution via `_env_vars`
294
295### Interactive Mode Flow
296
297Interactive mode (`synu i <agent> [args...]`) works by:
2981. Calling agent's `_interactive` function to get flags
2992. Optionally saving selections to cache (user prompted)
3003. Recursively calling `synu <agent> <flags> [args...]` (non-interactive)
3014. The recursive call then follows normal path with pre-parsed flags
302
303This means the `_configure` function must handle both direct flag input and flags from interactive mode identically.
304
305### Model ID Prefixes
306
307Different agents expect different model ID formats:
308- **opencode**: Prefix with `synthetic/` (e.g., `synthetic/hf:moonshotai/Kimi-K2`)
309- **aider**: Prefix with `openai/` (e.g., `openai/hf:moonshotai/Kimi-K2`)
310- **claude, qwen, llxprt**: Use raw model ID (e.g., `hf:moonshotai/Kimi-K2`)
311
312### Fish-specific Considerations
313
314- **Status checks**: Always use `test $status -ne 0` immediately after command, as any subsequent command overwrites `$status`
315- **Array slicing**: Use `$argv[2..-1]` syntax (1-indexed, inclusive)
316- **Command substitution**: Use `(command)` not `$(command)`
317- **String operations**: Use `string` builtin (`string split`, `string match`)
318- **No `local` keyword**: Use `set -l` for local scope
319- **Variable indirection**: Use `$$var_name` to get value of variable named by `$var_name`
320
321## Testing Approach
322
323This project has no automated test suite. Testing is manual:
324
3251. **Syntax validation**: Run `fish -n` on all `.fish` files
3262. **Quota tracking**: Run `synu` alone, check quota display with color
3273. **Passthrough**: Test with unknown agent (`synu echo "hello"`)
3284. **Agent config**: Test each agent with/without flags
3295. **Interactive mode**: Test `synu i <agent>` with gum installed
3306. **Cache persistence**: Test that interactive selections save correctly
3317. **Error cases**: Test without `SYNTHETIC_API_KEY`, with invalid API key
332
333**When making changes:**
334- Validate syntax with `fish -n`
335- Source the function and test manually with different argument combinations
336- Check that exit status is preserved
337- Verify quota display shows correct before/after values
338- Test cache file creation and updates
339
340## Common Tasks
341
342### Adding a New Agent Configuration
343
3441. Create `functions/_synu_agents/<agent>.fish`
3452. Add SPDX header
3463. Source cache functions: `source (status dirname)/../_synu_cache.fish`
3474. Define fallback default: `set -g _synu_<agent>_fallback_model "hf:..."`
3485. Define `_synu_<agent>_default` helper (uses cache with fallback)
3496. Define `_synu_agent_<agent>_flags` (return argparse flag specs)
3507. Define `_synu_agent_<agent>_configure` (set state/env vars)
3518. Define `_synu_agent_<agent>_args` (if agent uses CLI args for model)
3529. Define `_synu_agent_<agent>_env_vars` (if agent uses env vars)
35310. Optionally define `_synu_agent_<agent>_interactive` (for `synu i <agent>`)
35411. Update `completions/synu.fish` to add the new agent to suggestions
35512. Test manually
356
357### Modifying Quota Display
358
359Quota display logic is in two places:
360- **No args case**: `synu.fish` lines ~7-29
361- **Post-execution**: `synu.fish` lines ~162-180
362
363Both use the same color thresholds:
364- Green: < 33% used
365- Yellow: 33-66% used
366- Red: > 66% used
367
368### Debugging API Issues
369
370```fish
371# Test quota fetch directly
372source functions/_synu_get_quota.fish
373set -gx SYNTHETIC_API_KEY your_key_here
374_synu_get_quota
375# Should output: "requests_used limit"
376
377# Test with curl directly
378curl -s -f -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
379 "https://api.synthetic.new/v2/quotas" | jq .
380
381# Test model listing
382curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
383 "https://api.synthetic.new/openai/v1/models" | jq '.data[].name'
384```
385
386### Debugging Cache Issues
387
388```fish
389# Check cache file location
390echo $XDG_CONFIG_HOME/synu/models.conf
391# or
392echo ~/.config/synu/models.conf
393
394# View cache contents
395cat ~/.config/synu/models.conf
396
397# Test cache functions
398source functions/_synu_cache.fish
399_synu_cache_set test model hf:test/model
400_synu_cache_get test model
401```
402
403### Understanding Model Configuration Flow
404
405**Example: Claude with flag override**
4061. User runs: `synu claude --large hf:some/model "prompt"`
4072. `synu.fish` loads `functions/_synu_agents/claude.fish`
4083. Calls `_synu_agent_claude_flags` to get flag spec
4094. Parses with `argparse --ignore-unknown` → sets `_flag_large`
4105. Rebuilds flags for configure: `--large=hf:some/model`
4116. Calls `_synu_agent_claude_configure --large=hf:some/model`
4127. Configure sets opus/sonnet/subagent models from flag
4138. Exports `ANTHROPIC_*` environment variables
4149. Executes `command claude "prompt"` with those environment variables
41510. Claude Code sees Synthetic models via env vars and uses them
41611. After exit, env vars are cleaned up via `_env_vars`
417
418**Example: OpenCode with CLI args**
4191. User runs: `synu opencode "prompt"`
4202. `synu.fish` loads `functions/_synu_agents/opencode.fish`
4213. `_configure` sets `_synu_opencode_selected_model` from cache/fallback
4224. `_args` returns: `--model synthetic/hf:MiniMaxAI/MiniMax-M2`
4235. Executes: `command opencode -m "synthetic/hf:..." "prompt"`
424
425## Commit Conventions
426
427Use **Conventional Commits** with type and scope:
428
429**Types:** `feat`, `fix`, `refactor`, `docs`, `chore`, `test`
430
431**Scopes:**
432- `synu` - main wrapper function
433- `quota` - quota fetching/display
434- `cache` - model preference caching
435- `agents/<name>` - specific agent configuration (e.g., `agents/claude`)
436- `completions` - shell completions
437- `docs` - documentation
438
439**Examples:**
440```
441feat(synu): add support for model override flags
442fix(quota): handle API errors gracefully
443feat(agents/claude): add interactive model selection
444feat(cache): implement persistent model preferences
445docs(readme): document interactive mode
446```
447
448**Trailers:**
449- Use `References: HASH` for related commits/bugs
450- Always include `Assisted-by: [Model Name] via [Tool Name]` when AI-assisted
451
452## Project-Specific Context
453
454### Why Fish Shell?
455
456This is Amolith's personal project and Fish is their shell of choice. Fish provides:
457- Clean syntax without bashisms
458- Better error handling than POSIX shells
459- Native arrays and proper scoping
460- Built-in string manipulation
461
462### Why Synthetic API?
463
464Synthetic provides a unified API for multiple LLM providers with quota tracking. This wrapper makes quota visibility automatic and consistent across different agent tools.
465
466### Design Philosophy
467
468- **Minimal interference**: Pass through as much as possible to the underlying agent
469- **Graceful degradation**: If quota tracking fails, still run the agent
470- **Extensible**: Easy to add new agent configurations without modifying core wrapper
471- **Interactive when helpful**: Support both CLI flags and TUI selection
472- **Persistent preferences**: Save model selections for future sessions
473
474### Configured Agents
475
476Agents with specific configurations in `functions/_synu_agents/`:
477
478| Agent | Models | Configuration Method | Interactive |
479|-------|--------|---------------------|-------------|
480| claude | opus, sonnet, haiku, subagent | Environment variables | Yes |
481| opencode | single | CLI args | Yes |
482| aider | main, editor | Env + CLI args | Yes |
483| llxprt | single | CLI args | Yes |
484| qwen | single | Environment variables | Yes |
485
486Passthrough agents (no config, just quota tracking): crush, amp, octo, codex
487
488Any command can be wrapped; the above are just agents with explicit configuration.
489
490## Future Considerations
491
492Potential enhancements (not currently implemented):
493- Support for other quota tracking APIs besides Synthetic
494- Agent-specific usage tracking/history
495- Budget warnings/limits
496- More agent configurations
497
498When implementing features, maintain the core principles:
499- Don't break passthrough mode
500- Keep it simple and transparent
501- Graceful degradation
502- Follow Fish idioms