{"slug": "synthetic-claude-code-wrapper-defaults-to-k3-as-the-model", "title": "Synthetic Claude Code wrapper (defaults to K3 as the model)", "summary": "A developer created a shell wrapper called 'synclaude' that routes Claude Code's interface through Synthetic's API, allowing users to run open models like Kimi K3, GLM 5.2, and MiniMax M3 while keeping Claude Code's agent and tool features. The wrapper validates models against Synthetic's live catalogue and ensures all model slots use the chosen model.", "body_md": "A shell wrapper that points [Claude Code](https://claude.com/claude-code) at\n[Synthetic](https://synthetic.new), so you keep the Claude Code interface —\nagents, tools, subagents, slash commands — while inference runs on open models\nlike Kimi K3, GLM 5.2 and MiniMax M3.\n\nIt is one function plus three helpers. Paste the block below into your shell rc and you are done.\n\n- Picks a model per run with a substring:\n`--model k3`\n\n,`--model glm-5.2`\n\n- Validates the model against Synthetic's live catalogue, so a typo fails immediately with the real options instead of silently doing the wrong thing\n- Points\n**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\n\n| Tool | Why |\n|---|---|\n`claude` |\nthe CLI itself — `npm i -g @anthropic-ai/claude-code` |\n`curl` |\nfetches the model catalogue |\n`jq` |\nparses it — `brew install jq` |\n`awk` , `sed` , `find` |\nmatching and caching (already on macOS/Linux) |\n\nShell: **bash 3.2+ or zsh**. It uses arrays, so it is not POSIX `sh`\n\n.\n\nA Synthetic API key: [https://synthetic.new](https://synthetic.new) → **Settings → API Keys**.\n\nAdd your key and the function to `~/.zshrc`\n\n(or `~/.bashrc`\n\n):\n\n```\nexport SYNTHETIC_API_KEY=\"syn_xxxxxxxxxxxxxxxx\"\n```\n\nThen paste this block:\n\n```\n# ── synclaude ────────────────────────────────────────────────────────────────\n# Run Claude Code against Synthetic (https://synthetic.new).\n# Works in bash 3.2+ and zsh. Requires: claude, curl, jq, awk.\n\n# Fetches the model catalogue, cached for six hours under TMPDIR.\n# Pass any argument to force a refetch.\n_synclaude_models() {\n  local cache=\"${TMPDIR:-/tmp}/synclaude-models-$(id -u).json\"\n\n  if [ -n \"${1:-}\" ] || [ ! -s \"$cache\" ] || [ -n \"$(find \"$cache\" -mmin +360 2>/dev/null)\" ]; then\n    if curl -fsS -m 20 https://api.synthetic.new/v1/models \\\n         -H \"Authorization: Bearer ${SYNTHETIC_API_KEY:-}\" -o \"$cache.tmp\" 2>/dev/null; then\n      mv -f \"$cache.tmp\" \"$cache\"\n    else\n      rm -f \"$cache.tmp\"\n      # A stale cache still beats failing outright when the network is down.\n      [ -s \"$cache\" ] || return 1\n    fi\n  fi\n\n  jq -r '.data[]?.id' \"$cache\" 2>/dev/null | sort\n}\n\n# Turns a shorthand into exactly one model id, e.g. k3 -> hf:moonshotai/Kimi-K3.\n#\n# An exact id wins over a substring, without which a model whose name is a\n# prefix of another could never be selected. Ambiguous or unknown input fails\n# with the real options rather than guessing.\n#\n# awk, not grep: a GREP_OPTIONS containing -n in the environment prefixes line\n# numbers onto every match and corrupts the resolved id.\n_synclaude_resolve_model() {\n  local query=\"$1\" ids matches\n\n  if ! command -v jq >/dev/null 2>&1; then\n    printf >&2 \"synclaude: --model needs 'jq' to read the catalogue (brew install jq)\\n\"\n    return 1\n  fi\n\n  if ! ids=\"$(_synclaude_models)\"; then\n    printf >&2 'synclaude: could not fetch the catalogue from api.synthetic.new\\n'\n    return 1\n  fi\n\n  if [ \"$query\" = \"--list\" ]; then\n    printf '%s\\n' \"$ids\"\n    return 0\n  fi\n\n  matches=\"$(printf '%s\\n' \"$ids\" | awk -v q=\"$query\" '$0 == q')\"\n  [ -z \"$matches\" ] && matches=\"$(printf '%s\\n' \"$ids\" |\n    awk -v q=\"$query\" 'BEGIN{q=tolower(q)} index(tolower($0), q)')\"\n\n  # A model published since the cache was written: refetch once before failing.\n  if [ -z \"$matches\" ]; then\n    ids=\"$(_synclaude_models force)\" || :\n    matches=\"$(printf '%s\\n' \"$ids\" |\n      awk -v q=\"$query\" 'BEGIN{q=tolower(q)} index(tolower($0), q)')\"\n  fi\n\n  if [ -z \"$matches\" ]; then\n    printf >&2 \"synclaude: no model matches '%s'. Available:\\n\" \"$query\"\n    printf '%s\\n' \"$ids\" | sed 's/^/  /' >&2\n    return 1\n  fi\n\n  if [ \"$(printf '%s\\n' \"$matches\" | awk 'END{print NR}')\" -gt 1 ]; then\n    printf >&2 \"synclaude: '%s' is ambiguous, it matches:\\n\" \"$query\"\n    printf '%s\\n' \"$matches\" | sed 's/^/  /' >&2\n    return 1\n  fi\n\n  printf '%s\\n' \"$matches\"\n}\n\n_synclaude_help() {\n  cat <<HELP\nsynclaude - Claude Code against Synthetic\n\nUsage:\n  synclaude [--model <id|substring>] [--thinking|--no-thinking] [claude args ...]\n  synclaude --help\n\nOptions:\n  --model <m>    Override every model for this run - main, opus, sonnet, haiku\n                 and subagent all become <m>. Takes a full model id or any\n                 case-insensitive substring that matches exactly one.\n  --no-thinking  Turn extended thinking off (the default here, see below).\n  --thinking     Turn extended thinking back on.\n  -h, --help     Show this message. Claude's own help is 'claude --help'.\n\nEverything else is passed to claude untouched.\n\nCurrent configuration:\n  credential   SYNTHETIC_API_KEY\n  endpoint     ${SYNTHETIC_BASE_URL:-https://api.synthetic.new/anthropic}\n  model        ${SYNTHETIC_MODEL:-hf:moonshotai/Kimi-K3}\n  thinking     ${SYNTHETIC_THINKING:-off}\nHELP\n\n  printf '\\nAvailable models:\\n'\n  _synclaude_resolve_model --list 2>/dev/null | sed 's/^/  /'\n}\n\nsynclaude() {\n  local model=\"${SYNTHETIC_MODEL:-hf:moonshotai/Kimi-K3}\"\n  local base_url=\"${SYNTHETIC_BASE_URL:-https://api.synthetic.new/anthropic}\"\n  # Off by default because the default model requires it - see the notes below.\n  local thinking=\"${SYNTHETIC_THINKING:-off}\"\n  local requested_model=\"\"\n  local -a args\n  args=()\n\n  while [ $# -gt 0 ]; do\n    case \"$1\" in\n      -h|--help)      _synclaude_help; return 0 ;;\n      --no-thinking)  thinking=off; shift ;;\n      --thinking)     thinking=on; shift ;;\n      --model)\n        if [ -z \"${2:-}\" ]; then\n          printf >&2 'synclaude: --model needs a value (try: synclaude --help)\\n'\n          return 2\n        fi\n        requested_model=\"$2\"; shift 2 ;;\n      --model=*)      requested_model=\"${1#--model=}\"; shift ;;\n      *)              args+=(\"$1\"); shift ;;\n    esac\n  done\n\n  if [ -z \"${SYNTHETIC_API_KEY:-}\" ]; then\n    printf >&2 'synclaude: SYNTHETIC_API_KEY is not set\\n'\n    return 1\n  fi\n\n  if ! command -v claude >/dev/null 2>&1; then\n    printf >&2 \"synclaude: 'claude' not found in PATH. Install: npm i -g @anthropic-ai/claude-code\\n\"\n    return 127\n  fi\n\n  if [ -n \"$requested_model\" ]; then\n    model=\"$(_synclaude_resolve_model \"$requested_model\")\" || return 1\n  fi\n\n  # Claude Code reads any non-empty value as \"on\", so \"off\" means empty.\n  local disable_thinking=1\n  [ \"$thinking\" = \"on\" ] && disable_thinking=\n\n  printf >&2 'synclaude: endpoint=%s | model=%s | thinking=%s\\n' \"$base_url\" \"$model\" \"$thinking\"\n\n  # env(1) keeps all of this scoped to the child process, and reaches the real\n  # binary rather than this function.\n  env \\\n    ANTHROPIC_BASE_URL=\"$base_url\" \\\n    ANTHROPIC_AUTH_TOKEN=\"$SYNTHETIC_API_KEY\" \\\n    ANTHROPIC_API_KEY= \\\n    ANTHROPIC_MODEL=\"$model\" \\\n    ANTHROPIC_DEFAULT_OPUS_MODEL=\"$model\" \\\n    ANTHROPIC_DEFAULT_SONNET_MODEL=\"$model\" \\\n    ANTHROPIC_DEFAULT_HAIKU_MODEL=\"$model\" \\\n    CLAUDE_CODE_SUBAGENT_MODEL=\"$model\" \\\n    CLAUDE_CODE_DISABLE_THINKING=\"$disable_thinking\" \\\n    CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \\\n    CLAUDE_CODE_ATTRIBUTION_HEADER=0 \\\n    CLAUDE_CODE_MAX_OUTPUT_TOKENS=\"${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-32000}\" \\\n    MAX_THINKING_TOKENS=\"${MAX_THINKING_TOKENS:-100000}\" \\\n    IS_DEMO=\"${IS_DEMO-true}\" \\\n    claude ${args[@]+\"${args[@]}\"}\n}\n```\n\nReload with `exec $SHELL -l`\n\n, then run `synclaude --help`\n\n.\n\n```\nsynclaude                                   # interactive, default model\nsynclaude -p \"explain this repo\"            # one-shot\nsynclaude --model glm-5.2 --thinking        # switch model, thinking on\nsynclaude --model k2.7                      # substring is enough\nsynclaude --help                            # options + live model list\n```\n\n`--model`\n\naccepts any case-insensitive substring that matches exactly one\ncatalogue entry. Everything the wrapper does not recognise is forwarded to\n`claude`\n\nuntouched, so `--resume`\n\n, `--output-format json`\n\n, `--verbose`\n\nand the\nrest all work as usual.\n\n``` bash\n$ synclaude --model glm\nsynclaude: 'glm' is ambiguous, it matches:\n  hf:zai-org/GLM-4.7-Flash\n  hf:zai-org/GLM-5.2\n```\n\n**Thinking is off by default**, and that is not an arbitrary choice.\n\nClaude Code sends a reasoning effort of `medium`\n\nto third-party models. Some\nmodels do not accept that value. Kimi K3 advertises only `['high','low','max']`\n\n,\nso with thinking on **every request fails**:\n\n```\nAPI Error: 500 {\"error\":\"Error from inference backend: 500\nUnsupported thinking_effort='medium'; supported values are ['high','low','max'].\"}\n```\n\nThere is currently no way to change the value Claude Code sends. `--effort`\n\n(its own CLI flag), `CLAUDE_CODE_EFFORT_LEVEL`\n\n, `CLAUDE_CODE_ALWAYS_ENABLE_EFFORT`\n\nand `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING`\n\nwere each tested against this\nendpoint and the error came back identical, still naming `medium`\n\n. Synthetic's\ncatalogue does not publish per-model effort levels either, so the wrapper cannot\nmap them for you.\n\nTurning thinking off avoids the rejected parameter entirely, which is why it is\nthe default here. The trade-off is real: **you lose extended reasoning.** On a\nmodel that accepts `medium`\n\n, turn it back on:\n\n```\nsynclaude --model glm-5.2 --thinking\n```\n\nIf you would rather default to a model that thinks, set\n`SYNTHETIC_MODEL=hf:zai-org/GLM-5.2`\n\nand `SYNTHETIC_THINKING=on`\n\n.\n\nAll optional except the key.\n\n| Variable | Default | Purpose |\n|---|---|---|\n`SYNTHETIC_API_KEY` |\n— | Required. Your Synthetic key |\n`SYNTHETIC_MODEL` |\n`hf:moonshotai/Kimi-K3` |\nDefault model |\n`SYNTHETIC_THINKING` |\n`off` |\n`on` to default extended thinking on |\n`SYNTHETIC_BASE_URL` |\n`https://api.synthetic.new/anthropic` |\nEndpoint |\n`CLAUDE_CODE_MAX_OUTPUT_TOKENS` |\n`32000` |\nMax output tokens |\n`MAX_THINKING_TOKENS` |\n`100000` |\nThinking budget when thinking is on |\n`IS_DEMO` |\n`true` |\nTrims the startup banner. `IS_DEMO= synclaude` restores it |\n\nTwo details worth knowing:\n\nhides the \"Tips for getting started\" and \"What's new\" panels and your account/org line. Claude Code treats`IS_DEMO`\n\n*any non-empty value*as on — including`0`\n\nand`false`\n\n— so the only way back to the full banner is an empty one:`IS_DEMO= synclaude`\n\n.on every run so a real Anthropic key sitting in your environment cannot outrank the Synthetic token.`ANTHROPIC_API_KEY`\n\nis blanked\n\n** SYNTHETIC_API_KEY is not set** — export it, then reload your shell.\n\n** no model matches '...'** — the error lists every available id. The catalogue\nis cached for six hours; to force a refresh:\n\n```\nrm -f \"${TMPDIR:-/tmp}/synclaude-models-$(id -u).json\"\n```\n\n** Unsupported thinking_effort='medium'** — that model rejects the effort level\nClaude Code sends. Drop\n\n`--thinking`\n\n, or pick another model.**Model ids change.** Synthetic rotates them without notice — `Kimi-K2.5`\n\nwas a\nworking default until it began returning 404. That is exactly why this wrapper\nreads the catalogue from the API rather than hardcoding a list. Run\n`synclaude --help`\n\nto see what is live right now.\n\nTested on macOS with Claude Code 2.1.220 in both `bash`\n\nand `zsh`\n\n— including\npristine shells with no rc files — covering the default model, `--model`\n\noverrides, ambiguous and unknown input, missing key and missing binary, and\nlive end-to-end runs against the real API on Kimi K3 and GLM 5.2.\n\nPublic domain / CC0. Use it however you like.", "url": "https://wpnews.pro/news/synthetic-claude-code-wrapper-defaults-to-k3-as-the-model", "canonical_source": "https://gist.github.com/jpcaparas/1083538d9ce187fc5b0bc3a98bf37441", "published_at": "2026-07-28 01:21:39+00:00", "updated_at": "2026-07-28 02:58:41.152088+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["Claude Code", "Synthetic", "Kimi K3", "GLM 5.2", "MiniMax M3"], "alternates": {"html": "https://wpnews.pro/news/synthetic-claude-code-wrapper-defaults-to-k3-as-the-model", "markdown": "https://wpnews.pro/news/synthetic-claude-code-wrapper-defaults-to-k3-as-the-model.md", "text": "https://wpnews.pro/news/synthetic-claude-code-wrapper-defaults-to-k3-as-the-model.txt", "jsonld": "https://wpnews.pro/news/synthetic-claude-code-wrapper-defaults-to-k3-as-the-model.jsonld"}}