Skip to content

Instantly share code, notes, and snippets.

@jpcaparas
Last active July 28, 2026 02:57
Show Gist options
  • Select an option

  • Save jpcaparas/1083538d9ce187fc5b0bc3a98bf37441 to your computer and use it in GitHub Desktop.

Select an option

Save jpcaparas/1083538d9ce187fc5b0bc3a98bf37441 to your computer and use it in GitHub Desktop.
Synthetic Claude Code wrapper (defaults to K3 as the model)
- https://imgur.com/a/LlAXBm5
- https://imgur.com/a/pAKhB20

synclaude — run Claude Code against Synthetic

A shell wrapper that points Claude Code at Synthetic, so you keep the Claude Code interface — agents, tools, subagents, slash commands — while inference runs on open models like Kimi K3, GLM 5.2 and MiniMax M3.

It is one function plus three helpers. Paste the block below into your shell rc and you are done.

  • Picks a model per run with a substring: --model k3, --model glm-5.2
  • Validates the model against Synthetic's live catalogue, so a typo fails immediately with the real options instead of silently doing the wrong thing
  • Points every model slot at your choice — main, opus, sonnet, haiku and subagents — so delegated work does not quietly fall back to something else
  • Leaks nothing into your shell: every variable is scoped to the child process

Requirements

Tool Why
claude the CLI itself — npm i -g @anthropic-ai/claude-code
curl fetches the model catalogue
jq parses it — brew install jq
awk, sed, find matching and caching (already on macOS/Linux)

Shell: bash 3.2+ or zsh. It uses arrays, so it is not POSIX sh.

A Synthetic API key: https://synthetic.newSettings → API Keys.

Install

Add your key and the function to ~/.zshrc (or ~/.bashrc):

export SYNTHETIC_API_KEY="syn_xxxxxxxxxxxxxxxx"

Then paste this block:

# ── synclaude ────────────────────────────────────────────────────────────────
# Run Claude Code against Synthetic (https://synthetic.new).
# Works in bash 3.2+ and zsh. Requires: claude, curl, jq, awk.

# Fetches the model catalogue, cached for six hours under TMPDIR.
# Pass any argument to force a refetch.
_synclaude_models() {
  local cache="${TMPDIR:-/tmp}/synclaude-models-$(id -u).json"

  if [ -n "${1:-}" ] || [ ! -s "$cache" ] || [ -n "$(find "$cache" -mmin +360 2>/dev/null)" ]; then
    if curl -fsS -m 20 https://api.synthetic.new/v1/models \
         -H "Authorization: Bearer ${SYNTHETIC_API_KEY:-}" -o "$cache.tmp" 2>/dev/null; then
      mv -f "$cache.tmp" "$cache"
    else
      rm -f "$cache.tmp"
      # A stale cache still beats failing outright when the network is down.
      [ -s "$cache" ] || return 1
    fi
  fi

  jq -r '.data[]?.id' "$cache" 2>/dev/null | sort
}

# Turns a shorthand into exactly one model id, e.g. k3 -> hf:moonshotai/Kimi-K3.
#
# An exact id wins over a substring, without which a model whose name is a
# prefix of another could never be selected. Ambiguous or unknown input fails
# with the real options rather than guessing.
#
# awk, not grep: a GREP_OPTIONS containing -n in the environment prefixes line
# numbers onto every match and corrupts the resolved id.
_synclaude_resolve_model() {
  local query="$1" ids matches

  if ! command -v jq >/dev/null 2>&1; then
    printf >&2 "synclaude: --model needs 'jq' to read the catalogue (brew install jq)\n"
    return 1
  fi

  if ! ids="$(_synclaude_models)"; then
    printf >&2 'synclaude: could not fetch the catalogue from api.synthetic.new\n'
    return 1
  fi

  if [ "$query" = "--list" ]; then
    printf '%s\n' "$ids"
    return 0
  fi

  matches="$(printf '%s\n' "$ids" | awk -v q="$query" '$0 == q')"
  [ -z "$matches" ] && matches="$(printf '%s\n' "$ids" |
    awk -v q="$query" 'BEGIN{q=tolower(q)} index(tolower($0), q)')"

  # A model published since the cache was written: refetch once before failing.
  if [ -z "$matches" ]; then
    ids="$(_synclaude_models force)" || :
    matches="$(printf '%s\n' "$ids" |
      awk -v q="$query" 'BEGIN{q=tolower(q)} index(tolower($0), q)')"
  fi

  if [ -z "$matches" ]; then
    printf >&2 "synclaude: no model matches '%s'. Available:\n" "$query"
    printf '%s\n' "$ids" | sed 's/^/  /' >&2
    return 1
  fi

  if [ "$(printf '%s\n' "$matches" | awk 'END{print NR}')" -gt 1 ]; then
    printf >&2 "synclaude: '%s' is ambiguous, it matches:\n" "$query"
    printf '%s\n' "$matches" | sed 's/^/  /' >&2
    return 1
  fi

  printf '%s\n' "$matches"
}

_synclaude_help() {
  cat <<HELP
synclaude - Claude Code against Synthetic

Usage:
  synclaude [--model <id|substring>] [--thinking|--no-thinking] [claude args ...]
  synclaude --help

Options:
  --model <m>    Override every model for this run - main, opus, sonnet, haiku
                 and subagent all become <m>. Takes a full model id or any
                 case-insensitive substring that matches exactly one.
  --no-thinking  Turn extended thinking off (the default here, see below).
  --thinking     Turn extended thinking back on.
  -h, --help     Show this message. Claude's own help is 'claude --help'.

Everything else is passed to claude untouched.

Current configuration:
  credential   SYNTHETIC_API_KEY
  endpoint     ${SYNTHETIC_BASE_URL:-https://api.synthetic.new/anthropic}
  model        ${SYNTHETIC_MODEL:-hf:moonshotai/Kimi-K3}
  thinking     ${SYNTHETIC_THINKING:-off}
HELP

  printf '\nAvailable models:\n'
  _synclaude_resolve_model --list 2>/dev/null | sed 's/^/  /'
}

synclaude() {
  local model="${SYNTHETIC_MODEL:-hf:moonshotai/Kimi-K3}"
  local base_url="${SYNTHETIC_BASE_URL:-https://api.synthetic.new/anthropic}"
  # Off by default because the default model requires it - see the notes below.
  local thinking="${SYNTHETIC_THINKING:-off}"
  local requested_model=""
  local -a args
  args=()

  while [ $# -gt 0 ]; do
    case "$1" in
      -h|--help)      _synclaude_help; return 0 ;;
      --no-thinking)  thinking=off; shift ;;
      --thinking)     thinking=on; shift ;;
      --model)
        if [ -z "${2:-}" ]; then
          printf >&2 'synclaude: --model needs a value (try: synclaude --help)\n'
          return 2
        fi
        requested_model="$2"; shift 2 ;;
      --model=*)      requested_model="${1#--model=}"; shift ;;
      *)              args+=("$1"); shift ;;
    esac
  done

  if [ -z "${SYNTHETIC_API_KEY:-}" ]; then
    printf >&2 'synclaude: SYNTHETIC_API_KEY is not set\n'
    return 1
  fi

  if ! command -v claude >/dev/null 2>&1; then
    printf >&2 "synclaude: 'claude' not found in PATH. Install: npm i -g @anthropic-ai/claude-code\n"
    return 127
  fi

  if [ -n "$requested_model" ]; then
    model="$(_synclaude_resolve_model "$requested_model")" || return 1
  fi

  # Claude Code reads any non-empty value as "on", so "off" means empty.
  local disable_thinking=1
  [ "$thinking" = "on" ] && disable_thinking=

  printf >&2 'synclaude: endpoint=%s | model=%s | thinking=%s\n' "$base_url" "$model" "$thinking"

  # env(1) keeps all of this scoped to the child process, and reaches the real
  # binary rather than this function.
  env \
    ANTHROPIC_BASE_URL="$base_url" \
    ANTHROPIC_AUTH_TOKEN="$SYNTHETIC_API_KEY" \
    ANTHROPIC_API_KEY= \
    ANTHROPIC_MODEL="$model" \
    ANTHROPIC_DEFAULT_OPUS_MODEL="$model" \
    ANTHROPIC_DEFAULT_SONNET_MODEL="$model" \
    ANTHROPIC_DEFAULT_HAIKU_MODEL="$model" \
    CLAUDE_CODE_SUBAGENT_MODEL="$model" \
    CLAUDE_CODE_DISABLE_THINKING="$disable_thinking" \
    CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
    CLAUDE_CODE_ATTRIBUTION_HEADER=0 \
    CLAUDE_CODE_MAX_OUTPUT_TOKENS="${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-32000}" \
    MAX_THINKING_TOKENS="${MAX_THINKING_TOKENS:-100000}" \
    IS_DEMO="${IS_DEMO-true}" \
    claude ${args[@]+"${args[@]}"}
}

Reload with exec $SHELL -l, then run synclaude --help.

Usage

synclaude                                   # interactive, default model
synclaude -p "explain this repo"            # one-shot
synclaude --model glm-5.2 --thinking        # switch model, thinking on
synclaude --model k2.7                      # substring is enough
synclaude --help                            # options + live model list

--model accepts any case-insensitive substring that matches exactly one catalogue entry. Everything the wrapper does not recognise is forwarded to claude untouched, so --resume, --output-format json, --verbose and the rest all work as usual.

$ synclaude --model glm
synclaude: 'glm' is ambiguous, it matches:
  hf:zai-org/GLM-4.7-Flash
  hf:zai-org/GLM-5.2

The thinking caveat — read this one

Thinking is off by default, and that is not an arbitrary choice.

Claude Code sends a reasoning effort of medium to third-party models. Some models do not accept that value. Kimi K3 advertises only ['high','low','max'], so with thinking on every request fails:

API Error: 500 {"error":"Error from inference backend: 500
Unsupported thinking_effort='medium'; supported values are ['high','low','max']."}

There is currently no way to change the value Claude Code sends. --effort (its own CLI flag), CLAUDE_CODE_EFFORT_LEVEL, CLAUDE_CODE_ALWAYS_ENABLE_EFFORT and CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING were each tested against this endpoint and the error came back identical, still naming medium. Synthetic's catalogue does not publish per-model effort levels either, so the wrapper cannot map them for you.

Turning thinking off avoids the rejected parameter entirely, which is why it is the default here. The trade-off is real: you lose extended reasoning. On a model that accepts medium, turn it back on:

synclaude --model glm-5.2 --thinking

If you would rather default to a model that thinks, set SYNTHETIC_MODEL=hf:zai-org/GLM-5.2 and SYNTHETIC_THINKING=on.

Configuration

All optional except the key.

Variable Default Purpose
SYNTHETIC_API_KEY Required. Your Synthetic key
SYNTHETIC_MODEL hf:moonshotai/Kimi-K3 Default model
SYNTHETIC_THINKING off on to default extended thinking on
SYNTHETIC_BASE_URL https://api.synthetic.new/anthropic Endpoint
CLAUDE_CODE_MAX_OUTPUT_TOKENS 32000 Max output tokens
MAX_THINKING_TOKENS 100000 Thinking budget when thinking is on
IS_DEMO true Trims the startup banner. IS_DEMO= synclaude restores it

Two details worth knowing:

  • IS_DEMO hides the "Tips for getting started" and "What's new" panels and your account/org line. Claude Code treats any non-empty value as on — including 0 and false — so the only way back to the full banner is an empty one: IS_DEMO= synclaude.
  • ANTHROPIC_API_KEY is blanked on every run so a real Anthropic key sitting in your environment cannot outrank the Synthetic token.

Troubleshooting

SYNTHETIC_API_KEY is not set — export it, then reload your shell.

no model matches '...' — the error lists every available id. The catalogue is cached for six hours; to force a refresh:

rm -f "${TMPDIR:-/tmp}/synclaude-models-$(id -u).json"

Unsupported thinking_effort='medium' — that model rejects the effort level Claude Code sends. Drop --thinking, or pick another model.

Model ids change. Synthetic rotates them without notice — Kimi-K2.5 was a working default until it began returning 404. That is exactly why this wrapper reads the catalogue from the API rather than hardcoding a list. Run synclaude --help to see what is live right now.

Verified

Tested on macOS with Claude Code 2.1.220 in both bash and zsh — including pristine shells with no rc files — covering the default model, --model overrides, ambiguous and unknown input, missing key and missing binary, and live end-to-end runs against the real API on Kimi K3 and GLM 5.2.

Licence

Public domain / CC0. Use it however you like.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment