Detailed changes
@@ -0,0 +1,125 @@
+# AGENTS.md
+
+## Project Overview
+
+**synclaude.fish** is a Fish shell wrapper for the `claude` CLI that routes requests through [Synthetic.new](https://synthetic.new)'s API. It enables using alternative AI models (like GLM-4.6, Llama, Qwen) through the Claude CLI by proxying requests through Synthetic's Anthropic-compatible endpoint.
+
+### Key Functionality
+
+- Defaults all Claude model tiers (Opus, Sonnet, Haiku, Sub-agent) to `hf:zai-org/GLM-4.6`
+- Provides interactive TUI for model selection using `gum`
+- Supports group-based (`--large`/`--light`) and individual model overrides
+- Displays session API usage and remaining quota after each run
+- Wraps `claude` CLI completely - all unknown flags pass through
+
+## Project Structure
+
+This is a standard Fish plugin with the following structure:
+
+```
+synclaude.fish/
+├── README.md # User documentation
+├── functions/
+│ └── synclaude.fish # Main wrapper function (~156 lines)
+└── completions/
+ └── synclaude.fish # Tab completions (~15 lines)
+```
+
+### Architecture
+
+**Control Flow:**
+
+1. **Interactive mode** (`synclaude i`):
+- Fetches available models from Synthetic API
+- Prompts user to choose Groups vs Individual models
+- Prompts for which groups/models to override
+- For each selection, filters available models with `gum filter`
+- Recursively calls `synclaude` with generated flags (lines 83-84)
+2. **Flag-based mode** (default):
+- Parses custom flags with `argparse --ignore-unknown` (lines 91-94)
+- Sets environment variables for Anthropic endpoint and model overrides
+- Fetches initial quota from Synthetic API
+- Executes `command claude $argv` (line 128)
+- Fetches final quota and displays usage stats with color-coding
+
+**Key Design Patterns:**
+
+- **Override precedence**: Individual flags override group flags, which override defaults (lines 107-121)
+- **Error propagation**: Uses `or return` throughout to exit on command failures
+
+## Code Conventions
+
+### Fish Shell Patterns
+
+- **Local variables**: Always use `set -l` for function-local scope
+- **Export variables**: Use `set -lx` for locally-scoped exports (lines 96-104)
+- **Variable checking**: Use `set -q variable` to test existence (lines 107-121)
+- **Array removal**: Use `set -e argv[1]` to remove elements (line 6)
+- **Command substitution**: Use `(command)` not backticks
+- **Error handling**: Chain commands with `or return` for early exits
+- **Math operations**: Use `math` builtin with `-s0` for integer results (line 139)
+
+### Code Style
+
+- **Indentation**: 4 spaces (not tabs)
+- **Line length**: No hard limit, but keep logical blocks readable
+- **Variable naming**:
+ - Descriptive names: `models_json`, `initial_requests`, `quota_color`
+ - No abbreviations except common ones: `args` → `argv`, `arg` → `argv`
+- **Comments**:
+ - Group logic blocks with descriptive comments (lines 88-90)
+ - Inline comments for non-obvious conditions (lines 125, 132)
+ - No redundant comments explaining obvious code
+
+### API Interaction Patterns
+
+All API calls wrapped in `gum spin` for UX:
+
+```fish
+set -l response (gum spin --spinner dot --title "Message..." -- curl -s -H "Authorization: Bearer $KEY" https://api.url)
+```
+
+API responses always:
+1. Use `jq -r` for raw (unquoted) output
+2. Provide fallback values with `// 0` or similar (lines 125, 132-133)
+3. Test for empty values after extraction (line 126)
+4. Redirect stderr to `/dev/null` if errors are expected (lines 124, 131)
+
+### Error Handling
+
+- **API failures**: Use `or return 1` to exit on curl/jq errors
+- **Null safety**: Always test extracted values before use (line 126)
+- **Propagate exit codes**: Return `$status` from wrapped command (line 84)
+- **Silent failures**: Only suppress stderr when errors are expected and handled
+
+## Completions
+
+File: `completions/synclaude.fish`
+
+- **Inherits from wrapped command**: `complete -c synclaude -w claude` (line 2)
+- **Subcommand prevention**: Use `-n "not __fish_seen_subcommand_from i"` to prevent completions after `i` (line 5)
+- **Flag completions**: `-s` (short), `-l` (long), `-r` (requires argument), `-d` (description)
+
+## Quota Display Logic
+
+Post-execution, displays:
+- **Session usage**: `final_requests - initial_requests`
+- **Remaining quota**: `limit - final_requests` (color-coded)
+
+Color coding (lines 141-146):
+- **Green**: <33% used
+- **Yellow**: 33-66% used
+- **Red**: >66% used
+
+Color logic uses `set_color` and `set_color normal` to wrap output (lines 151-153).
+
+## Common Gotchas
+
+1. **`argparse --ignore-unknown` is critical**: Without it, unknown flags would error instead of passing through to `claude` (line 91)
+2. **Recursive call must use function name, not alias**: Line 83 calls `synclaude`, not `command synclaude` - this works because Fish functions can recursively invoke themselves
+3. **`_flag_*` variable naming**: `argparse` creates variables from flag names with `_flag_` prefix - must use exact names (lines 107-121)
+4. **Model ID vs Model Name**: Interactive mode fetches model names for display but must extract model IDs for API use (lines 30, 37, 53, 60, 67, 74)
+5. **jq requires argument quoting**: In jq, use `--arg name "$var"` then `$name` in filter - don't interpolate directly into filter string
+6. **Quota check timing**: Initial quota fetch happens before `claude` runs to calculate session usage. If it fails, script continues anyway
+7. **`math` precision**: Use `-s0` for integer output to avoid decimal points in quota percentages
+8. **Local exports don't persist**: `set -lx` variables only exist in function scope - won't affect parent shell
@@ -0,0 +1,158 @@
+# synclaude.fish
+
+A Fish wrapper for the `claude` CLI that routes requests through
+[Synthetic.new](https://synthetic.new), enabling use of models like GLM-4.6
+through `claude`.
+
+## tl;dr
+
+- Defaults to GLM 4.6
+- Displays session usage and remaining API quota after each run
+- Invoke as `synclaude i` for an interactive model picker
+- Configure models by group or individually through the interactive picker or
+ flags
+
+## Requirements
+
+- Fish
+- `claude`
+- [gum](https://github.com/charmbracelet/gum) - for TUI prompts and spinners
+- [jq](https://jqlang.github.io/jq/) - for JSON parsing
+- `curl` - for API requests
+- `SYNTHETIC_API_KEY` environment variable
+
+## Installation
+
+### Using Fundle
+
+Add to your `~/.config/fish/config.fish`:
+
+```fish
+fundle plugin 'synclaude' --url 'https://git.secluded.site/synclaude.fish'
+fundle init
+```
+
+Then reload your shell or run `fundle install`.
+
+## Configuration
+
+Set your Synthetic API key in your `~/.config/fish/config.fish`:
+
+```fish
+set -gx SYNTHETIC_API_KEY your_api_key_here
+```
+
+## Usage
+
+### Basic
+
+Use `synclaude` exactly like you would use `claude`:
+
+```fish
+# TUI
+synclaude
+
+# Non-interactively
+synclaude -p "What does functions/synclaude.fish do?"
+```
+
+By default, all models (Opus, Sonnet, Haiku, and Sub-agent) use
+`hf:zai-org/GLM-4.6`.
+
+### Interactive overrides
+
+Launch the interactive picker with the `i` subcommand:
+
+```fish
+synclaude i
+```
+
+1. Select whether to override models by group or by specific model
+2. Select which ones to override
+3. Select what to override them with
+
+### Flags
+
+| Flag | Short | Description |
+|------|-------|-------------|
+| `--large` | `-L` | Override Opus, Sonnet, and Sub-agent models |
+| `--light` | `-l` | Override Haiku |
+| `--haiku` | `-H` | Override Haiku |
+| `--opus` | `-o` | Override Opus |
+| `--sonnet` | `-s` | Override Sonnet |
+| `--agent` | `-a` | Override sub-agent |
+
+All other flags are passed through to the `claude` command.
+
+### CLI overrides with flags
+
+#### By group
+
+```fish
+# Set all "large" models (Opus, Sonnet, Sub-agent) to GLM-4.6
+synclaude --large hf:zai-org/GLM-4.6 "Your prompt"
+
+# Set the "light" model (Haiku)
+synclaude --light hf:qwen/Qwen2.5-Coder-32B-Instruct "Your prompt"
+
+# Combine both
+synclaude --large hf:zai-org/GLM-4.6 --light hf:qwen/Qwen2.5-Coder-32B-Instruct "Your prompt"
+```
+
+#### By individual model
+
+```fish
+# Override specific models
+synclaude --opus hf:zai-org/GLM-4.6 "Your prompt"
+synclaude --sonnet hf:meta-llama/Llama-3.3-70B-Instruct "Your prompt"
+synclaude --haiku hf:qwen/Qwen2.5-Coder-32B-Instruct "Your prompt"
+synclaude --agent hf:zai-org/GLM-4.6 "Your prompt"
+```
+
+Individual overrides take precedence over group overrides.
+
+## How it works
+
+`synclaude` configures the following environment variables before invoking
+`claude`:
+
+- `ANTHROPIC_BASE_URL`: Points to Synthetic's Anthropic-compatible endpoint
+- `ANTHROPIC_AUTH_TOKEN`: Your Synthetic API key
+- `ANTHROPIC_DEFAULT_OPUS_MODEL`: Model to use for Opus-tier requests
+- `ANTHROPIC_DEFAULT_SONNET_MODEL`: Model to use for Sonnet-tier requests
+- `ANTHROPIC_DEFAULT_HAIKU_MODEL`: Model to use for Haiku-tier requests
+- `CLAUDE_CODE_SUBAGENT_MODEL`: Model for sub-agent operations
+- `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC`: Disabled because telemetry bad
+
+After each run, it queries the Synthetic API to display:
+- Requests used during the session
+- Remaining quota (color-coded: green <33%, yellow 33-66%, red >66%)
+
+## Contributions
+
+Patch requests are in [amolith/llm-projects] on [pr.pico.sh]. You don't need a
+new account to contribute, you don't need to fork this repo, you don't need to
+fiddle with `git send-email`, you don't need to faff with your email client to
+get `git request-pull` working...
+
+You just need:
+
+- Git
+- SSH
+- An SSH key
+
+```sh
+# Clone this repo, make your changes, and commit them
+# Create a new patch request with
+git format-patch origin/main --stdout | ssh pr.pico.sh pr create amolith/llm-projects
+# After potential feedback, submit a revision to an existing patch request with
+git format-patch origin/main --stdout | ssh pr.pico.sh pr add {prID}
+# List patch requests
+ssh pr.pico.sh pr ls amolith/llm-projects
+```
+
+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
@@ -0,0 +1,15 @@
+# Inherit all claude completions
+complete -c synclaude -w claude
+
+# Interactive mode
+complete -c synclaude -n "not __fish_seen_subcommand_from i" -a i -d "Interactive model selection"
+
+# Group model overrides
+complete -c synclaude -s L -l large -r -d "Override Opus, Sonnet, and Sub-agent models"
+complete -c synclaude -s l -l light -r -d "Override Haiku model"
+
+# Specific model overrides
+complete -c synclaude -s o -l opus -r -d "Override Opus model"
+complete -c synclaude -s s -l sonnet -r -d "Override Sonnet model"
+complete -c synclaude -s H -l haiku -r -d "Override Haiku model"
+complete -c synclaude -s a -l agent -r -d "Override Sub-agent model"
@@ -0,0 +1,156 @@
+function synclaude --wraps=claude --description "Run claude with Synthetic GLM-4.6 backend"
+ set -l default_model hf:zai-org/GLM-4.6
+
+ # Handle interactive mode
+ if test (count $argv) -gt 0 && test "$argv[1]" = "i"
+ set -e argv[1] # Remove 'i' from args
+
+ # Fetch available models
+ set -l models_json (gum spin --spinner dot --title "Fetching models..." -- curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" https://api.synthetic.new/openai/v1/models)
+ or return 1
+ set -l model_names (echo $models_json | jq -r '.data[].name')
+ or return 1
+
+ # Prompt for groups vs individual
+ set -l mode (gum choose --limit 1 --header "How do you want to select models?" "Groups" "Individual models")
+ or return 1
+
+ # Build flags array
+ set -l flags
+
+ if test "$mode" = "Groups"
+ # Select which groups to override
+ set -l groups (gum choose --no-limit --header "Which group(s) do you want to override?" "Large (Opus, Sonnet, Sub-agent)" "Light (Haiku)")
+ or return 1
+
+ for group in $groups
+ if test "$group" = "Large (Opus, Sonnet, Sub-agent)"
+ set -l model_name (printf "%s\n" $model_names | gum filter --limit 1 --header "Select model for Large group" --placeholder "Filter models...")
+ or return 1
+ set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+ if test -n "$model_id"
+ set flags $flags --large=$model_id
+ end
+ else if test "$group" = "Light (Haiku)"
+ set -l model_name (printf "%s\n" $model_names | gum filter --limit 1 --header "Select model for Light group" --placeholder "Filter models...")
+ or return 1
+ set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+ if test -n "$model_id"
+ set flags $flags --light=$model_id
+ end
+ end
+ end
+ else
+ # Select which individual models to override
+ set -l models (gum choose --no-limit --header "Which model(s) do you want to override?" "Opus" "Sonnet" "Haiku" "Sub-agent")
+ or return 1
+
+ for model_type in $models
+ switch $model_type
+ case "Opus"
+ set -l model_name (printf "%s\n" $model_names | gum filter --limit 1 --header "Select Opus model" --placeholder "Filter models...")
+ or return 1
+ set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+ if test -n "$model_id"
+ set flags $flags --opus=$model_id
+ end
+ case "Sonnet"
+ set -l model_name (printf "%s\n" $model_names | gum filter --limit 1 --header "Select Sonnet model" --placeholder "Filter models...")
+ or return 1
+ set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+ if test -n "$model_id"
+ set flags $flags --sonnet=$model_id
+ end
+ case "Haiku"
+ set -l model_name (printf "%s\n" $model_names | gum filter --limit 1 --header "Select Haiku model" --placeholder "Filter models...")
+ or return 1
+ set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+ if test -n "$model_id"
+ set flags $flags --haiku=$model_id
+ end
+ case "Sub-agent"
+ set -l model_name (printf "%s\n" $model_names | gum filter --limit 1 --header "Select Sub-agent model" --placeholder "Filter models...")
+ or return 1
+ set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+ if test -n "$model_id"
+ set flags $flags --agent=$model_id
+ end
+ end
+ end
+ end
+
+ # Recursively call synclaude with selected flags and remaining args
+ synclaude $flags $argv
+ return $status
+ end
+
+ # Parse arguments
+ # -L/--large: Sets Opus, Sonnet, and Sub-agent models
+ # -l/--light: Sets Haiku model
+ # -o/--opus, -s/--sonnet, -H/--haiku, -a/--agent: Set specific models
+ argparse --ignore-unknown \
+ 'L/large=' 'l/light=' \
+ 'o/opus=' 's/sonnet=' 'H/haiku=' 'a/agent=' -- $argv
+ or return
+
+ set -lx ANTHROPIC_BASE_URL https://api.synthetic.new/anthropic
+ set -lx ANTHROPIC_AUTH_TOKEN $SYNTHETIC_API_KEY
+
+ # Set defaults
+ set -lx ANTHROPIC_DEFAULT_OPUS_MODEL $default_model
+ set -lx ANTHROPIC_DEFAULT_SONNET_MODEL $default_model
+ set -lx ANTHROPIC_DEFAULT_HAIKU_MODEL $default_model
+ set -lx CLAUDE_CODE_SUBAGENT_MODEL $default_model
+ set -lx CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 1
+
+ # Apply group overrides
+ if set -q _flag_large
+ set ANTHROPIC_DEFAULT_OPUS_MODEL $_flag_large
+ set ANTHROPIC_DEFAULT_SONNET_MODEL $_flag_large
+ set CLAUDE_CODE_SUBAGENT_MODEL $_flag_large
+ end
+
+ if set -q _flag_light
+ set ANTHROPIC_DEFAULT_HAIKU_MODEL $_flag_light
+ end
+
+ # Apply specific overrides (taking precedence over groups)
+ if set -q _flag_opus; set ANTHROPIC_DEFAULT_OPUS_MODEL $_flag_opus; end
+ if set -q _flag_sonnet; set ANTHROPIC_DEFAULT_SONNET_MODEL $_flag_sonnet; end
+ if set -q _flag_haiku; set ANTHROPIC_DEFAULT_HAIKU_MODEL $_flag_haiku; end
+ if set -q _flag_agent; set CLAUDE_CODE_SUBAGENT_MODEL $_flag_agent; end
+
+ # Fetch initial quota
+ set -l initial_response (gum spin --spinner dot --title "Checking quota..." -- curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" https://api.synthetic.new/v2/quotas 2>/dev/null)
+ set -l initial_requests (echo $initial_response | jq -r '.subscription.requests // 0' 2>/dev/null)
+ test -n "$initial_requests" || set initial_requests 0
+
+ command claude $argv
+
+ # Fetch final quota
+ set -l final_response (gum spin --spinner dot --title "Fetching usage..." -- curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" https://api.synthetic.new/v2/quotas 2>/dev/null)
+ set -l final_requests (echo $final_response | jq -r '.subscription.requests // 0' 2>/dev/null)
+ set -l limit (echo $final_response | jq -r '.subscription.limit // 0' 2>/dev/null)
+
+ # Calculate and display
+ if test -n "$limit" && test "$limit" -gt 0
+ set -l session_usage (math "$final_requests - $initial_requests")
+ set -l remaining (math "$limit - $final_requests")
+ set -l percent_used (math -s0 "($final_requests * 100) / $limit")
+
+ set -l quota_color green
+ if test "$percent_used" -gt 66
+ set quota_color red
+ else if test "$percent_used" -gt 33
+ set quota_color yellow
+ end
+
+ echo
+ echo "Session usage: $session_usage requests"
+ echo -n "Remaining: "
+ set_color $quota_color
+ echo -n "$remaining/$limit"
+ set_color normal
+ echo " requests"
+ end
+end