From f06a5fb5af47743625e6c931facd50dd7666cff7 Mon Sep 17 00:00:00 2001 From: Amolith Date: Tue, 2 Dec 2025 17:31:13 -0700 Subject: [PATCH] docs(readme,agents): updoot --- AGENTS.md | 230 +++++++++++++++++++++++++++++++++++++++++++----------- README.md | 98 ++++++++++++++++++----- 2 files changed, 262 insertions(+), 66 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f6dc8d80c169df127e375eb9ea7e37a9cc1f0775..22f26a83c516e522db7b2ffb829f5736d263bd04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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__` + - Agent internal helpers: `_synu__` (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/.fish` can provide four functions: +Each agent definition in `functions/_synu_agents/.fish` can provide five functions: 1. **`_synu_agent__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__configure`** - Sets environment variables before execution +2. **`_synu_agent__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__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__env_vars`** - Returns list of environment variables to clean up after execution +4. **`_synu_agent__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/.fish` can provide four end ``` -4. **`_synu_agent__interactive`** - Implements interactive model selection +5. **`_synu_agent__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/.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/.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/.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 [args...]`) works by: 1. Calling agent's `_interactive` function to get flags -2. Recursively calling `synu [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 [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 ` 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/.fish` 2. Add SPDX header -3. Define `_synu_agent__flags` (return argparse flag specs) -4. Define `_synu_agent__configure` (set environment variables) -5. Optionally define `_synu_agent__interactive` (for `synu i `) -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__fallback_model "hf:..."` +5. Define `_synu__default` helper (uses cache with fallback) +6. Define `_synu_agent__flags` (return argparse flag specs) +7. Define `_synu_agent__configure` (set state/env vars) +8. Define `_synu_agent__args` (if agent uses CLI args for model) +9. Define `_synu_agent__env_vars` (if agent uses env vars) +10. Optionally define `_synu_agent__interactive` (for `synu i `) +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/` - 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 diff --git a/README.md b/README.md index 51f275da0cf6850c9d9a026f5e3d78ec547ef08b..7dbce070af3dc4cdcb57bca185ff20dc22099c89 100644 --- a/README.md +++ b/README.md @@ -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 ` 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 ` 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__flags` - Returns argparse flag specification -- `_synu_agent__configure` - Sets environment variables before execution +- `_synu_agent__configure` - Sets environment variables/state before execution +- `_synu_agent__args` - Returns CLI arguments to inject +- `_synu_agent__env_vars` - Lists environment variables to clean up - `_synu_agent__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 \ No newline at end of file +[pr.pico.sh]: https://pr.pico.sh