# Synthetic Claude Code wrapper (defaults to K3 as the model)

> Source: <https://gist.github.com/jpcaparas/1083538d9ce187fc5b0bc3a98bf37441>
> Published: 2026-07-28 01:21:39+00:00

A shell wrapper that points [Claude Code](https://claude.com/claude-code) at
[Synthetic](https://synthetic.new), 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

| 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.new](https://synthetic.new) → **Settings → API Keys**.

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`

.

```
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.

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

**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`

.

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:

hides the "Tips for getting started" and "What's new" panels and your account/org line. Claude Code treats`IS_DEMO`

*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`

.on every run so a real Anthropic key sitting in your environment cannot outrank the Synthetic token.`ANTHROPIC_API_KEY`

is blanked

** 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.

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.

Public domain / CC0. Use it however you like.
