AGENTS.md

  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