> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pioneer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Claude Code

> Point Claude Code at Pioneer for multi-model inference, router-backed pioneer/auto, and full /model picker support.

Pioneer exposes an [Anthropic-compatible API](/api-reference/inference/anthropic-compatible). Claude Code can use it like a custom gateway: set `ANTHROPIC_BASE_URL`, authenticate with your Pioneer API key, and switch models with `/model` or `pioneer/auto`.

## Quick setup

<Steps>
  <Step title="Install Claude Code">
    Download and [set up Claude Code](https://code.claude.com/docs/en/quickstart).
  </Step>

  <Step title="Persist Pioneer env vars">
    Create `~/.pioneer/env` with these contents:

    ```bash theme={null}
    unset ANTHROPIC_AUTH_TOKEN CLAUDE_CODE_OAUTH_TOKEN
    export ANTHROPIC_API_KEY="<YOUR_PIONEER_API_KEY>"
    export ANTHROPIC_BASE_URL="https://api.pioneer.ai"
    export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
    export ANTHROPIC_CUSTOM_MODEL_OPTION="pioneer/auto"
    ```

    <Accordion title="Shell commands to create and source this file">
      ```bash theme={null}
      mkdir -p ~/.pioneer
      cat > ~/.pioneer/env <<'EOF'
      unset ANTHROPIC_AUTH_TOKEN CLAUDE_CODE_OAUTH_TOKEN
      export ANTHROPIC_API_KEY="<YOUR_PIONEER_API_KEY>"
      export ANTHROPIC_BASE_URL="https://api.pioneer.ai"
      export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
      export ANTHROPIC_CUSTOM_MODEL_OPTION="pioneer/auto"
      EOF
      chmod 600 ~/.pioneer/env
      RC=~/.zshrc
      [ -n "$BASH_VERSION" ] && RC=~/.bashrc
      grep -qsF '. ~/.pioneer/env' "$RC" || printf '\n# Pioneer inference\n. ~/.pioneer/env\n' >> "$RC"
      . ~/.pioneer/env
      ```
    </Accordion>
  </Step>

  <Step title="Clear stale Claude.ai auth">
    Sign out of Claude.ai so Claude Code uses your Pioneer key:

    ```bash theme={null}
    claude auth logout >/dev/null 2>&1 || true
    ```

    If Claude Code previously asked whether to trust this API key and you chose **No**, move the key tail from `customApiKeyResponses.rejected` to `approved` in `~/.claude.json`, or rerun setup from the Pioneer dashboard integration guide (it repairs this automatically).
  </Step>

  <Step title="Install the routed-savings hook">
    When you use `pioneer/auto`, Pioneer stamps `pioneer_savings` on each response — the per-1M-token price difference between the model the router picked and a frontier reference model. Claude Code does not surface that by default, so install a **Stop** hook (Claude Code's end-of-turn hook) that multiplies those rate differences by your actual token usage, sums them across the session, and ends each turn with a line like:

    ```text theme={null}
    Pioneer routing saved ~$1.43 this session (vs claude-opus-4-7)
    ```

    <Accordion title="Show hook installer script">
      ```bash theme={null}
      mkdir -p ~/.pioneer/hooks
      cat > ~/.pioneer/hooks/show-pioneer-routed-model.sh <<'PIONEER_ROUTED_MODEL_HOOK'
      #!/usr/bin/env bash
      # Claude Code hook: surface how much pioneer/auto's routing has saved this session.
      #
      # Instead of the raw backend model, this shows cumulative money saved: for each
      # pioneer/auto turn the backend stamps a per-1M-token savings rate diff vs a
      # frontier reference (pioneer_savings) on the response; this hook multiplies it
      # by the per-turn token usage Claude Code records and sums across the session.
      # On a turn where the routed model changed (cold prompt cache), cache-write
      # savings are dropped so the figure stays honest. For a *direct* (non-auto)
      # model it keeps nudging toward pioneer/auto via X-Pioneer-Router-Tip.
      # No-op when not on a Pioneer gateway, when no signal is present, or when
      # cumulative savings are not positive.

      set -euo pipefail

      INPUT_FILE=$(mktemp)
      trap 'rm -f "$INPUT_FILE"' EXIT
      cat > "$INPUT_FILE"

      python3 - "$INPUT_FILE" <<'PY' 2>/dev/null || true
      from __future__ import annotations

      import json
      import os
      import re
      import sys
      import time
      from pathlib import Path


      ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
      SET_MODEL_RE = re.compile(r"Set model to\s+(.+?)\s+and saved as your default")
      AUTO_ROUTER_ALIASES = {
          "pioneer/auto",
          "anthropic/pioneer-auto",
          "anthropic/pioneer/auto",
      }
      GATEWAY_ALIAS_PREFIX = "anthropic/pioneer/"
      FRONTIER_REFERENCE_MODEL = "claude-opus-4-7"
      DEFAULT_ROUTER_TIP = (
          "Tip: use model=pioneer/auto to let Pioneer route each request automatically; "
          "named models pin that concrete model."
      )
      TOKENS_PER_MILLION = 1_000_000.0
      USAGE_TOKEN_KEYS = (
          "input_tokens",
          "output_tokens",
          "cache_read_input_tokens",
          "cache_creation_input_tokens",
          "cache_read_tokens",
          "cache_write_tokens",
      )


      def collect_pioneer_signals(
          value: object, routed_models: list[str], router_tips: list[str]
      ) -> None:
          if isinstance(value, dict):
              for key, nested in value.items():
                  key_normalized = str(key).lower().replace("_", "-")
                  if key_normalized == "pioneer-routed-model":
                      if isinstance(nested, str) and nested:
                          routed_models.append(nested)
                      continue
                  if key_normalized == "x-pioneer-router-tip":
                      if isinstance(nested, str) and nested:
                          router_tips.append(nested)
                      continue
                  collect_pioneer_signals(nested, routed_models, router_tips)
          elif isinstance(value, list):
              for item in value:
                  collect_pioneer_signals(item, routed_models, router_tips)


      def find_first(value: object, target_key: str) -> object:
          if isinstance(value, dict):
              for key, nested in value.items():
                  if str(key).lower().replace("_", "-") == target_key:
                      return nested
              for nested in value.values():
                  found = find_first(nested, target_key)
                  if found is not None:
                      return found
          elif isinstance(value, list):
              for item in value:
                  found = find_first(item, target_key)
                  if found is not None:
                      return found
          return None


      def find_usage(value: object) -> dict | None:
          if isinstance(value, dict):
              if any(key in value for key in USAGE_TOKEN_KEYS):
                  return value
              for nested in value.values():
                  found = find_usage(nested)
                  if found is not None:
                      return found
          elif isinstance(value, list):
              for item in value:
                  found = find_usage(item)
                  if found is not None:
                      return found
          return None


      def normalize_model_id(model: str) -> str:
          model = model.strip()
          lowered = model.lower()
          if lowered in AUTO_ROUTER_ALIASES:
              return "pioneer/auto"
          if lowered.startswith(GATEWAY_ALIAS_PREFIX):
              return model[len(GATEWAY_ALIAS_PREFIX) :].strip().lower()
          return lowered


      def iter_strings(value: object):
          if isinstance(value, str):
              yield value
          elif isinstance(value, dict):
              for nested in value.values():
                  yield from iter_strings(nested)
          elif isinstance(value, list):
              for item in value:
                  yield from iter_strings(item)


      def is_real_user_turn(payload: object) -> bool:
          if not isinstance(payload, dict) or payload.get("isMeta") is True:
              return False
          if payload.get("type") != "user":
              return False
          return "promptSource" in payload


      def _token_count(usage: dict, *keys: str) -> int:
          for key in keys:
              value = usage.get(key)
              if isinstance(value, bool):
                  continue
              if isinstance(value, (int, float)):
                  return max(int(value), 0)
          return 0


      def _turn_savings_usd(usage: dict, diff: dict, *, drop_cache_write: bool) -> float:
          def rate(name: str) -> float:
              value = diff.get(name)
              return float(value) if isinstance(value, (int, float)) else 0.0

          input_tokens = _token_count(usage, "input_tokens")
          output_tokens = _token_count(usage, "output_tokens")
          cache_read = _token_count(usage, "cache_read_input_tokens", "cache_read_tokens")
          cache_write = _token_count(
              usage, "cache_creation_input_tokens", "cache_write_tokens"
          )
          total = (
              input_tokens * rate("input")
              + output_tokens * rate("output")
              + cache_read * rate("cache_read")
          )
          if not drop_cache_write:
              total += cache_write * rate("cache_write")
          return total / TOKENS_PER_MILLION


      def session_savings(records: list[tuple[int, object]]) -> tuple[float, str]:
          """Sum savings across distinct assistant turns, honoring cold-cache switches."""
          total = 0.0
          baseline = ""
          previous_routed: str | None = None
          seen_turns: set = set()
          for line_number, payload in records:
              if not isinstance(payload, dict) or payload.get("type") != "assistant":
                  continue
              routed = find_first(payload, "pioneer-routed-model")
              savings = find_first(payload, "pioneer-savings")
              usage = find_usage(payload)
              if not isinstance(savings, dict) or not isinstance(usage, dict):
                  if isinstance(routed, str) and routed:
                      previous_routed = routed
                  continue
              turn_id = payload.get("uuid") or line_number
              if turn_id in seen_turns:
                  continue
              seen_turns.add(turn_id)
              diff = savings.get("rate_diff_per_mtok")
              if not isinstance(diff, dict):
                  diff = {}
              baseline = savings.get("baseline_model") or baseline
              switched = (
                  isinstance(routed, str)
                  and bool(routed)
                  and previous_routed is not None
                  and routed != previous_routed
              )
              turn_total = _turn_savings_usd(usage, diff, drop_cache_write=switched)
              if turn_total > 0:
                  total += turn_total
              if isinstance(routed, str) and routed:
                  previous_routed = routed
          return total, baseline


      def format_usd(amount: float) -> str:
          if amount >= 1:
              return f"${amount:.2f}"
          if amount >= 0.01:
              return f"${amount:.3f}"
          return f"${amount:.4f}"


      def parse_transcript(
          transcript_path: str, attempts: int = 1
      ) -> tuple[str, str, str, float, str]:
          """Return (routed, tip, selected_model, savings_total, savings_baseline)."""
          path = Path(transcript_path)
          for attempt in range(attempts):
              if path.is_file():
                  latest_user_line = 0
                  latest_selected_model = ""
                  records: list[tuple[int, object]] = []
                  for line_number, line in enumerate(
                      path.read_text(encoding="utf-8").splitlines(),
                      start=1,
                  ):
                      line = line.strip()
                      if not line:
                          continue
                      try:
                          payload = json.loads(line)
                      except json.JSONDecodeError:
                          continue
                      records.append((line_number, payload))
                      if is_real_user_turn(payload):
                          latest_user_line = line_number
                      for text in iter_strings(payload):
                          clean = ANSI_RE.sub("", text)
                          match = SET_MODEL_RE.search(clean)
                          if match:
                              latest_selected_model = match.group(1).strip()

                  latest_routed = ""
                  latest_tip = ""
                  for line_number, payload in records:
                      if line_number <= latest_user_line:
                          continue
                      routed_models: list[str] = []
                      router_tips: list[str] = []
                      collect_pioneer_signals(payload, routed_models, router_tips)
                      if router_tips:
                          latest_tip = router_tips[-1]
                      if routed_models:
                          latest_routed = routed_models[-1]
                      elif router_tips:
                          latest_routed = ""

                  savings_total, savings_baseline = session_savings(records)
                  if (
                      latest_routed
                      or latest_tip
                      or latest_selected_model
                      or savings_total > 0
                  ):
                      return (
                          latest_routed,
                          latest_tip,
                          latest_selected_model,
                          savings_total,
                          savings_baseline,
                      )
              if attempt + 1 < attempts:
                  time.sleep(0.05)
          return "", "", "", 0.0, ""


      input_path = Path(sys.argv[1])
      payload = json.loads(input_path.read_text(encoding="utf-8"))
      event_name = payload.get("hook_event_name") or "Stop"

      if event_name == "Stop" and payload.get("stop_hook_active") is True:
          sys.exit(0)
      if event_name == "MessageDisplay" and int(payload.get("index", -1)) != 0:
          sys.exit(0)

      transcript_path = payload.get("transcript_path") or ""
      if not transcript_path:
          sys.exit(0)

      routed, router_tip, selected_model, savings_total, savings_baseline = parse_transcript(
          transcript_path,
          attempts=5,
      )
      session = selected_model or os.environ.get("ANTHROPIC_CUSTOM_MODEL_OPTION", "pioneer/auto")
      normalized_session = normalize_model_id(session)
      message = ""
      if normalized_session == "pioneer/auto":
          # Hide the routed model; surface cumulative savings only when positive.
          if savings_total > 0:
              reference = savings_baseline or FRONTIER_REFERENCE_MODEL
              message = (
                  f"Pioneer routing saved ~{format_usd(savings_total)} this session "
                  f"(vs {reference})"
              )
      elif routed:
          # Direct (pinned) gateway model: nudge toward pioneer/auto without
          # framing the pinned model as a routing decision.
          message = f"Using {routed} — {router_tip or DEFAULT_ROUTER_TIP}"
      elif router_tip:
          message = router_tip
      if not message:
          sys.exit(0)

      if event_name == "MessageDisplay":
          delta = payload.get("delta") or ""
          print(
              json.dumps(
                  {
                      "hookSpecificOutput": {
                          "hookEventName": "MessageDisplay",
                          "displayContent": f"{message}\n\n{delta}",
                      }
                  }
              )
          )
          sys.exit(0)

      print(
          json.dumps(
              {"systemMessage": message, "suppressOutput": True}
          )
      )
      PY
      PIONEER_ROUTED_MODEL_HOOK
      chmod +x ~/.pioneer/hooks/show-pioneer-routed-model.sh
      python3 <<'PY'
      import json
      from pathlib import Path

      hook = Path.home() / ".pioneer" / "hooks" / "show-pioneer-routed-model.sh"
      settings_path = Path.home() / ".claude" / "settings.json"
      command = str(hook)
      settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
      hooks = settings.setdefault("hooks", {})

      # Drop any prior registration of this hook (e.g. an older MessageDisplay one)
      # so the savings summary is surfaced exactly once per turn, at Stop.
      for event_name in list(hooks):
          kept = [
              group
              for group in hooks.get(event_name, [])
              if not any(entry.get("command") == command for entry in group.get("hooks", []))
          ]
          if kept:
              hooks[event_name] = kept
          else:
              hooks.pop(event_name, None)

      hooks.setdefault("Stop", []).append(
          {"hooks": [{"type": "command", "command": command, "timeout": 5}]}
      )
      settings_path.parent.mkdir(parents=True, exist_ok=True)
      settings_path.write_text(json.dumps(settings, indent=2) + "\n")
      PY
      ```
    </Accordion>
  </Step>

  <Step title="Seed the /model picker cache">
    Claude Code reads gateway models from `~/.claude/cache/gateway-models.json`. Seed it once after setup (and again if the catalog changes):

    <Accordion title="Show seed script (Python)">
      ```bash theme={null}
      python3 <<'PY'
      import json, os, re, time, urllib.request
      base = os.environ.get("ANTHROPIC_BASE_URL", "").rstrip("/")
      key = os.environ.get("ANTHROPIC_API_KEY", "")
      if not base or not key:
          raise SystemExit("ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY must be set")
      url = f"{base}/v1/models?limit=1000"
      req = urllib.request.Request(url, headers={"x-api-key": key, "anthropic-version": "2023-06-01"})
      data = json.load(urllib.request.urlopen(req, timeout=10))
      models = [
          {"id": x["id"], **({"display_name": x["display_name"]} if x.get("display_name") else {})}
          for x in data.get("data", [])
          if re.match(r"^(claude|anthropic)", x["id"], re.I)
      ]
      cache_dir = os.path.expanduser("~/.claude/cache")
      os.makedirs(cache_dir, exist_ok=True)
      path = os.path.join(cache_dir, "gateway-models.json")
      with open(path, "w") as f:
          json.dump({"baseUrl": base, "fetchedAt": int(time.time() * 1000), "models": models}, f)
      os.chmod(path, 0o600)
      print(f"Seeded {len(models)} models for /model")
      PY
      ```
    </Accordion>
  </Step>

  <Step title="Launch Claude Code">
    ```bash theme={null}
    claude --model pioneer/auto
    ```

    Use `/model` inside Claude Code to pick a specific Pioneer model, or keep `pioneer/auto` to use the [Code Router](/concepts/router).
  </Step>
</Steps>

<Tip>
  The Pioneer dashboard **Integrations** guide copies a single shell block that runs all of the steps above with your API key filled in.
</Tip>

## Use the Code Router (`pioneer/auto`)

`pioneer/auto` sends each turn through Pioneer's [Code Router](/concepts/router). The router picks the cheapest model that meets your quality bar for that specific prompt.

* Set `ANTHROPIC_CUSTOM_MODEL_OPTION=pioneer/auto` so Claude Code treats it as a first-class custom model.
* Launch with `claude --model pioneer/auto`, or select it from `/model`.
* After each turn, the Stop hook shows how much `pioneer/auto` routing has saved you this session.

### What does `Stop says:` mean?

Claude Code has a [hooks](https://code.claude.com/docs/en/hooks) system. **Stop** is one hook event — it runs after the assistant finishes a turn (not when you exit the session).

The Pioneer setup registers a Stop hook in `~/.claude/settings.json`. That script reads the `pioneer_savings` rate differences and the per-turn token usage from the session transcript, sums the savings across the session, and emits a short message. Claude Code labels hook output with the hook name, so the UI shows:

```text theme={null}
Stop says: Pioneer routing saved ~$1.43 this session (vs claude-opus-4-7)
```

This is informational. **Stop** is Claude Code's name for the end-of-turn hook — not a warning and not a command to halt. The amount is cumulative for the session and only appears once the [Code Router](/concepts/router) has actually saved money versus the frontier reference model.

Pioneer also includes `pioneer_routed_model` and `pioneer_savings` on Anthropic-compatible responses (streaming `message_start` frames and non-streaming bodies) if you want to build your own tooling around routing metadata.

## Full model list in `/model`

Claude Code only shows third-party gateway models whose IDs start with `claude` or `anthropic`. Pioneer publishes discovery aliases such as `anthropic/pioneer/gpt-5.4` on `GET /v1/models` so non-Claude decoder models appear in the picker while still resolving to the canonical Pioneer model ID at inference time.

Requirements:

* `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`
* `ANTHROPIC_BASE_URL` pointing at Pioneer
* A populated `~/.claude/cache/gateway-models.json` (see setup step above)

<Warning>
  If `~/.claude/settings.json` sets `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`, Claude Code skips live gateway discovery. Remove that flag or rerun the cache seed step whenever you need an updated `/model` list.
</Warning>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Claude Code says Not logged in · Please run /login" icon="circle-alert">
    This usually means Claude Code did not pick up a usable Pioneer API key for the current shell. It is not asking you to sign in to Claude.ai.

    1. Run `. ~/.pioneer/env`, then restart Claude Code.
    2. If Claude Code asks whether to use the environment API key, accept it.
    3. Run `/status` and confirm:
       * **API key** is `ANTHROPIC_API_KEY`
       * **Anthropic base URL** is your Pioneer endpoint
       * **Auth token** is `none`
    4. If `/status` points at Pioneer but the error persists, check `~/.claude.json`. Under `customApiKeyResponses`, make sure the suffix for your current Pioneer key is in `approved`, not `rejected`.
    5. If you are using a non-production Pioneer endpoint, make sure the API key comes from the same environment.
  </Accordion>

  <Accordion title="Claude.ai login conflicts with your Pioneer key" icon="key-round">
    If you see `Both claude.ai and ANTHROPIC_API_KEY set · auth may not work as expected`, Claude Code is still using Claude.ai OAuth instead of your Pioneer key.

    1. Run `claude auth logout` (or `/logout` inside Claude Code).
    2. Relaunch with `claude --model pioneer/auto`. When prompted about the environment API key, accept it.
    3. Verify with `/status`:
       * **Auth token** should be `none`
       * **API key** should be `ANTHROPIC_API_KEY`
       * **Anthropic base URL** should be your Pioneer endpoint

    On macOS, if the warning persists, open Keychain Access, search for **Claude Code**, delete stored credentials, and relaunch.
  </Accordion>

  <Accordion title="/model only shows built-ins and pioneer/auto" icon="list">
    1. Confirm `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` is set in `~/.pioneer/env` and sourced.
    2. Check that `~/.claude/cache/gateway-models.json` exists and its `baseUrl` matches `ANTHROPIC_BASE_URL`.
    3. Remove `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` from `~/.claude/settings.json` if present.
    4. Rerun the cache seed step from setup.
  </Accordion>

  <Accordion title="No savings line after each turn" icon="badge-dollar-sign">
    Install the Stop hook from the setup section. It only runs when `ANTHROPIC_BASE_URL` points at Pioneer, and the line appears only once `pioneer/auto` routing has actually saved money versus the frontier reference model this session. A brand-new session, or one where the router picked the frontier model itself, will show nothing until there are positive savings to report.
  </Accordion>
</AccordionGroup>
