diff --git a/AGENTS.md b/AGENTS.md index c822371861d88c7a36ad515123e7c6810f76d815..17e6ab27821dd0103eb52f3a2ca2f18fb3152b55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,124 +8,60 @@ SPDX-License-Identifier: Unlicense ## 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. +**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 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` +- Configurable model defaults for each Claude tier with persistent caching - Provides interactive TUI for model selection using `gum` -- Supports group-based (`--large`/`--light`) and individual model overrides +- Supports group-based (`--heavy`/`--medium`/`--light`) and individual model overrides - Displays session API usage and remaining quota after each run +- `synclaude u` shows current quota usage - Wraps `claude` CLI completely - all unknown flags pass through -## Project Structure +### Default Models (Fallbacks) -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) -``` +- **Opus**: `hf:moonshotai/Kimi-K2-Thinking` +- **Sonnet**: `hf:zai-org/GLM-4.6` +- **Haiku**: `hf:deepseek-ai/DeepSeek-V3.1-Terminus` +- **Sub-agent**: `hf:zai-org/GLM-4.6` ### 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 +1. **Usage mode** (`synclaude u`): + - Fetches and displays current quota usage with colored badge +2. **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 + - Shows current defaults in filter headers + - Offers to save selections as new defaults + - Recursively calls `synclaude` with generated flags +3. **Flag-based mode** (default): + - Parses custom flags with `argparse --ignore-unknown` + - Loads defaults from cache (or fallbacks) + - Applies group then individual overrides + - Sets environment variables for Anthropic endpoint + - Fetches initial quota from Synthetic API + - Executes `command claude $argv` + - Fetches final quota and displays usage stats with badge-style color-coding **Key Design Patterns:** -- **Override precedence**: Individual flags override group flags, which override defaults (lines 107-121) +- **Caching**: Persistent model preferences stored in `~/.config/synclaude/models.conf` +- **Override precedence**: Individual flags override group flags, which override cached/fallback defaults - **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) +1. **`argparse --ignore-unknown` is critical**: Without it, unknown flags would error instead of passing through to `claude` +2. **Recursive call must use function name, not alias**: `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 +4. **Model ID vs Model Name**: Interactive mode fetches model names for display but must extract model IDs for API use 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 +9. **Cache file location**: Respects `XDG_CONFIG_HOME` if set, otherwise defaults to `~/.config/synclaude/` diff --git a/README.md b/README.md index 06c2b2b57a95488fa23cd52f4dea7502e5de49ce..836f5cfa2a25013dd7d20805e344805773f76694 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ 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`. -![Invoking synclaude with the interactive i subcommand to override the default models. First question is how to override the models, selecting each individually or by group. After selecting group, the next question is which group, large -L or light -l or both. After selecting large, the next question is which model to use, presented as a list of IDs with fuzzy filtering. After typing "mmm2" for MiniMax M2, Claude Code starts. After typing Hi and getting a response and quitting Claude Code, synclaude shows how many requests were used during the session and how many are remaining.](https://vhs.charm.sh/vhs-2wRBg9PpDidQIfCptnQuLz.gif) +![Invoking synclaude with the interactive i subcommand to override the default models. First question is how to override the models, selecting each individually or by group. After selecting group, the next question is which group, large -L or light -l or both. After selecting large, the next question is which model to use, presented as a list of IDs with fuzzy filtering. After typing "mmm2" for MiniMax M2, Claude Code starts. After typing Hi and getting a response and quitting Claude Code, synclaude shows how many requests were used during the session and how many are remaining.](https://vhs.charm.sh/vhs-M1NGelKGg1PdjB3sN53De.gif) ## tl;dr -- Defaults to GLM 4.6 +- Configurable defaults per model tier with persistent caching - 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 +- `synclaude u` shows current quota +- Invoke as `synclaude i` for an interactive model picker with save-as-default option +- Configure models by group or individually through the interactive picker or flags ## Requirements @@ -52,6 +52,15 @@ set -gx SYNTHETIC_API_KEY your_api_key_here ## Usage +### Check quota + +Run with `u` subcommand to see current API usage: + +```fish +synclaude u +# Usage: 42% (580/1000 remaining) +``` + ### Basic Use `synclaude` exactly like you would use `claude`: @@ -64,8 +73,16 @@ synclaude 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`. +### Default Models + +| Tier | Default Model | +|------|--------------| +| Opus | `hf:moonshotai/Kimi-K2-Thinking` | +| Sonnet | `hf:zai-org/GLM-4.6` | +| Haiku | `hf:deepseek-ai/DeepSeek-V3.1-Terminus` | +| Sub-agent | `hf:zai-org/GLM-4.6` | + +Defaults can be overridden with flags or saved persistently through interactive mode. ### Interactive overrides @@ -77,18 +94,27 @@ 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 +3. Select what to override them with (shows current defaults in header) +4. Optionally save selections as new defaults ### 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 | +#### Group flags + +| Flag | Short | Description | +|------|-------|-------------| +| `--heavy` | `-H` | Override Opus model | +| `--medium` | `-M` | Override Sonnet and Sub-agent models | +| `--light` | `-l` | Override Haiku model | + +#### Individual flags + +| Flag | Short | Description | +|------|-------|-------------| +| `--opus` | `-o` | Override Opus model | +| `--sonnet` | `-s` | Override Sonnet model | +| `--haiku` | `-k` | Override Haiku model | +| `--agent` | `-a` | Override Sub-agent model | All other flags are passed through to the `claude` command. @@ -97,27 +123,30 @@ All other flags are passed through to the `claude` command. #### 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 heavy model (Opus) to a specific model +synclaude --heavy hf:moonshotai/Kimi-K2-Thinking "Your prompt" -# Set the "light" model (Haiku) +# Set medium models (Sonnet, Sub-agent) to GLM-4.6 +synclaude --medium hf:zai-org/GLM-4.6 "Your prompt" + +# Set 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" +# Combine groups +synclaude --heavy hf:moonshotai/Kimi-K2-Thinking --medium hf:zai-org/GLM-4.6 "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 --opus hf:moonshotai/Kimi-K2-Thinking "Your prompt" +synclaude --sonnet hf:zai-org/GLM-4.6 "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. +Individual overrides take precedence over group overrides, which take precedence over cached/fallback defaults. ## How it works @@ -132,10 +161,14 @@ Individual overrides take precedence over group overrides. - `CLAUDE_CODE_SUBAGENT_MODEL`: Model for sub-agent operations - `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC`: Disabled because telemetry bad +### Caching + +Model preferences are stored in `~/.config/synclaude/models.conf` (respects `XDG_CONFIG_HOME`). Use interactive mode (`synclaude i`) to save new defaults. + 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%) +- Remaining quota (color-coded badge: green <33%, yellow 33-66%, red >66%) ## Contributions diff --git a/completions/synclaude.fish b/completions/synclaude.fish index 7f5daca0bf23ac8dec6b474c6636d95a5a8cd0d3..fc016417d9f23f3b589321f5fe8398e90e8ba121 100644 --- a/completions/synclaude.fish +++ b/completions/synclaude.fish @@ -5,15 +5,17 @@ # 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" +# Subcommands +complete -c synclaude -n "not __fish_seen_subcommand_from i u" -a i -d "Interactive model selection" +complete -c synclaude -n "not __fish_seen_subcommand_from i u" -a u -d "Show current quota usage" # Group model overrides -complete -c synclaude -s L -l large -r -d "Override Opus, Sonnet, and Sub-agent models" +complete -c synclaude -s H -l heavy -r -d "Override Opus model" +complete -c synclaude -s M -l medium -r -d "Override 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 k -l haiku -r -d "Override Haiku model" complete -c synclaude -s a -l agent -r -d "Override Sub-agent model" diff --git a/demo.tape b/demo.tape index 9f5b0080b276442366257ee4e29d05fd6e8f10c1..c43576169c467a35e6dfd2a3a8948d31097e40d6 100644 --- a/demo.tape +++ b/demo.tape @@ -26,14 +26,20 @@ Enter Sleep 2s Sleep 750ms Enter Sleep 2s -# Select heavy group +# Select light group +Down Sleep 250ms +Down Sleep 500ms Space Sleep 750ms Enter Sleep 2s # Select the heavy model -Type "mmm2" Sleep 1.25s +Type "dsv31t" Sleep 1.25s Enter Sleep 2.25s +# Decline saving as default +Right Sleep 500ms +Enter Sleep 2.2s + # Send a message Type "Hi!" Sleep 1.25s Enter Sleep 4s diff --git a/functions/synclaude.fish b/functions/synclaude.fish index c851a88f68bcf0f7c1cbdbf555beeeb39ea8a3ae..da7083d858b6170851c6036a4ef91f86b1bfa343 100644 --- a/functions/synclaude.fish +++ b/functions/synclaude.fish @@ -2,43 +2,177 @@ # # SPDX-License-Identifier: Unlicense -function synclaude --wraps=claude --description "Run claude with Synthetic GLM-4.6 backend" - set -l default_model hf:zai-org/GLM-4.6 +# Cache file for model preferences +set -g _synclaude_cache_file (set -q XDG_CONFIG_HOME; and echo "$XDG_CONFIG_HOME"; or echo "$HOME/.config")"/synclaude/models.conf" + +# Fallback defaults (used when no cache entry exists) +set -g _synclaude_fallback_opus "hf:moonshotai/Kimi-K2-Thinking" +set -g _synclaude_fallback_sonnet "hf:zai-org/GLM-4.6" +set -g _synclaude_fallback_haiku "hf:deepseek-ai/DeepSeek-V3.1-Terminus" +set -g _synclaude_fallback_agent "hf:zai-org/GLM-4.6" + +function _synclaude_cache_get --description "Get a cached value: _synclaude_cache_get slot" + set -l slot $argv[1] + set -l key "claude.$slot" + + if not test -f "$_synclaude_cache_file" + return 1 + end + + set -l match (string match -r "^$key\\s*=\\s*(.+)" < "$_synclaude_cache_file") + if test -n "$match[2]" + string trim "$match[2]" + return 0 + end + return 1 +end + +function _synclaude_cache_set --description "Set a cached value: _synclaude_cache_set slot value" + set -l slot $argv[1] + set -l value $argv[2] + set -l key "claude.$slot" + + mkdir -p (dirname "$_synclaude_cache_file") + + if not test -f "$_synclaude_cache_file" + echo "# synclaude model preferences" > "$_synclaude_cache_file" + echo "# Format: claude.slot = model_id" >> "$_synclaude_cache_file" + echo "" >> "$_synclaude_cache_file" + end + + if string match -rq "^$key\\s*=" < "$_synclaude_cache_file" + set -l tmp (mktemp) + string replace -r "^$key\\s*=.*" "$key = $value" < "$_synclaude_cache_file" > "$tmp" + mv "$tmp" "$_synclaude_cache_file" + else + echo "$key = $value" >> "$_synclaude_cache_file" + end +end + +function _synclaude_default --description "Get default model: _synclaude_default slot" + set -l slot $argv[1] + set -l cached (_synclaude_cache_get $slot) + if test $status -eq 0 + echo $cached + else + set -l var_name _synclaude_fallback_$slot + echo $$var_name + end +end + +function _synclaude_get_quota --description "Fetch quota from Synthetic API" + if not set -q SYNTHETIC_API_KEY + echo "Error: SYNTHETIC_API_KEY environment variable not set" >&2 + return 1 + end + + set -l response (curl -s -f -H "Authorization: Bearer $SYNTHETIC_API_KEY" \ + "https://api.synthetic.new/v2/quotas" 2>/dev/null) + + if test $status -ne 0 -o -z "$response" + return 1 + end + + set -l requests (echo "$response" | jq -r '.subscription.requests // 0') + set -l limit (echo "$response" | jq -r '.subscription.limit // 0') + + echo "$requests $limit" +end + +function synclaude --wraps=claude --description "Run claude with Synthetic API backend" + # Handle usage subcommand + if test (count $argv) -gt 0 && test "$argv[1]" = "u" + set -l quota (_synclaude_get_quota) + if test $status -ne 0 + echo "Error: Could not fetch quota" >&2 + return 1 + end + set -l requests (echo "$quota" | cut -d' ' -f1) + set -l limit (echo "$quota" | cut -d' ' -f2) + set -l remaining (math "$limit - $requests") + set -l percent_used (math -s0 "$requests * 100 / $limit") + printf "Usage: " + if test $percent_used -lt 33 + set_color -b green black + else if test $percent_used -lt 67 + set_color -b yellow black + else + set_color -b red black + end + printf " %s%% " $percent_used + set_color normal + printf " (%s/%s remaining)\n" $remaining $limit + return 0 + end # Handle interactive mode if test (count $argv) -gt 0 && test "$argv[1]" = "i" set -e argv[1] # Remove 'i' from args - + + # Check for gum + if not command -q gum + echo "Error: gum is required for interactive mode. Install: https://github.com/charmbracelet/gum" >&2 + return 1 + end + # 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) + 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 - + + # Get current defaults for display + set -l current_opus_id (_synclaude_default opus) + set -l current_sonnet_id (_synclaude_default sonnet) + set -l current_haiku_id (_synclaude_default haiku) + set -l current_agent_id (_synclaude_default agent) + # Prompt for groups vs individual - set -l mode (gum choose --limit 1 --header "How do you want to select models?" "Groups" "Individual models") + 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)") + set -l groups (gum choose --no-limit \ + --header "Which group(s) do you want to override?" \ + "Heavy (Opus)" "Medium (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...") + if test "$group" = "Heavy (Opus)" + set -l model_name (printf "%s\n" $model_names | \ + gum filter --limit 1 --header "Select model for Heavy group (opus: $current_opus_id)" \ + --placeholder "Filter models...") or return 1 - set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id') + 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 + set flags $flags --heavy=$model_id + end + else if test "$group" = "Medium (Sonnet, Sub-agent)" + set -l model_name (printf "%s\n" $model_names | \ + gum filter --limit 1 --header "Select model for Medium group (sonnet: $current_sonnet_id, agent: $current_agent_id)" \ + --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 --medium=$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...") + set -l model_name (printf "%s\n" $model_names | \ + gum filter --limit 1 --header "Select model for Light group (haiku: $current_haiku_id)" \ + --placeholder "Filter models...") or return 1 - set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id') + 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 @@ -46,115 +180,182 @@ function synclaude --wraps=claude --description "Run claude with Synthetic GLM-4 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") + 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...") + set -l model_name (printf "%s\n" $model_names | \ + gum filter --limit 1 --header "Select Opus model (current: $current_opus_id)" \ + --placeholder "Filter models...") or return 1 - set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id') + 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...") + set -l model_name (printf "%s\n" $model_names | \ + gum filter --limit 1 --header "Select Sonnet model (current: $current_sonnet_id)" \ + --placeholder "Filter models...") or return 1 - set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id') + 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...") + set -l model_name (printf "%s\n" $model_names | \ + gum filter --limit 1 --header "Select Haiku model (current: $current_haiku_id)" \ + --placeholder "Filter models...") or return 1 - set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id') + 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...") + set -l model_name (printf "%s\n" $model_names | \ + gum filter --limit 1 --header "Select Sub-agent model (current: $current_agent_id)" \ + --placeholder "Filter models...") or return 1 - set -l model_id (echo $models_json | jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id') + 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 - + + # Offer to save as defaults + if test (count $flags) -gt 0 + if gum confirm "Save as defaults?" + for flag in $flags + set -l parts (string match -r -- '^--([^=]+)=(.+)$' $flag) + if test -n "$parts[2]" + set -l key $parts[2] + set -l value $parts[3] + switch $key + case heavy + _synclaude_cache_set opus $value + case medium + _synclaude_cache_set sonnet $value + _synclaude_cache_set agent $value + case light + _synclaude_cache_set haiku $value + case '*' + _synclaude_cache_set $key $value + end + 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 + # -H/--heavy: Sets Opus model + # -M/--medium: Sets Sonnet and Sub-agent models # -l/--light: Sets Haiku model - # -o/--opus, -s/--sonnet, -H/--haiku, -a/--agent: Set specific models + # -o/--opus, -s/--sonnet, -k/--haiku, -a/--agent: Set specific models argparse --ignore-unknown \ - 'L/large=' 'l/light=' \ - 'o/opus=' 's/sonnet=' 'H/haiku=' 'a/agent=' -- $argv - or return + 'H/heavy=' 'M/medium=' 'l/light=' \ + 'o/opus=' 's/sonnet=' 'k/haiku=' 'a/agent=' -- $argv + or return 1 - 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 + # Start with defaults (from cache or fallback) + set -l opus_model (_synclaude_default opus) + set -l sonnet_model (_synclaude_default sonnet) + set -l haiku_model (_synclaude_default haiku) + set -l subagent_model (_synclaude_default agent) # 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 + if set -q _flag_heavy + set opus_model $_flag_heavy + end + + if set -q _flag_medium + set sonnet_model $_flag_medium + set subagent_model $_flag_medium end if set -q _flag_light - set ANTHROPIC_DEFAULT_HAIKU_MODEL $_flag_light + set 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 + # Apply specific overrides (take precedence over groups) + if set -q _flag_opus + set opus_model $_flag_opus + end + if set -q _flag_sonnet + set sonnet_model $_flag_sonnet + end + if set -q _flag_haiku + set haiku_model $_flag_haiku + end + if set -q _flag_agent + set 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 + # Export environment variables for Claude Code + set -lx ANTHROPIC_BASE_URL "https://api.synthetic.new/anthropic" + set -lx ANTHROPIC_AUTH_TOKEN $SYNTHETIC_API_KEY + set -lx ANTHROPIC_DEFAULT_OPUS_MODEL $opus_model + set -lx ANTHROPIC_DEFAULT_SONNET_MODEL $sonnet_model + set -lx ANTHROPIC_DEFAULT_HAIKU_MODEL $haiku_model + set -lx CLAUDE_CODE_SUBAGENT_MODEL $subagent_model + set -lx CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 1 + + # Fetch quota before execution + set -l quota_before (_synclaude_get_quota) + if test $status -ne 0 + echo "Warning: Could not fetch quota before execution" >&2 + set quota_before "0 0" + end + set -l requests_before (echo "$quota_before" | cut -d' ' -f1) + set -l limit (echo "$quota_before" | cut -d' ' -f2) + + # Execute claude command claude $argv + set -l exit_status $status - # 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 + # Fetch quota after execution + set -l quota_after (_synclaude_get_quota) + if test $status -ne 0 + echo "Warning: Could not fetch quota after execution" >&2 + return $exit_status + end - echo - echo "Session usage: $session_usage requests" - echo -n "Remaining: " - set_color $quota_color - echo -n "$remaining/$limit" - set_color normal - echo " requests" + set -l requests_after (echo "$quota_after" | cut -d' ' -f1) + + # Calculate session usage and remaining quota + set -l session_usage (math "$requests_after - $requests_before") + set -l remaining (math "$limit - $requests_after") + set -l percent_used (math -s0 "$requests_after * 100 / $limit") + + # Display session usage and remaining quota with color + printf "\n" + printf "Session: %s requests\n" $session_usage + printf "Overall: " + + if test $percent_used -lt 33 + set_color -b green black + else if test $percent_used -lt 67 + set_color -b yellow black + else + set_color -b red black end + printf " %s%% " $percent_used + set_color normal + printf " (%s/%s remaining)\n" $remaining $limit + + return $exit_status end