Run Claude Code with Your ChatGPT Subscription

9 min read Original article ↗

If you’re already paying for ChatGPT Plus, Pro, or Max, you can route Claude Code through your existing subscription. No API key, no separate billing. Your normal claude command stays untouched - you get a second command, claude-codex, that runs through your ChatGPT quota instead.

The trick is LiteLLM - an open-source proxy that translates between AI provider formats. You point Claude Code at a local LiteLLM proxy, and the proxy forwards everything to OpenAI’s ChatGPT backend using your subscription auth.

I’ve been using this setup daily. It works surprisingly well for most coding tasks, though there are real limitations worth knowing about upfront.

This guide is for macOS with conda already installed. If you don’t have it, install miniconda.

Step 1 - Install LiteLLM

conda create -n litellm python=3.12 -y

conda activate litellm

pip install "litellm[proxy]==1.83.0"

conda deactivate

Verify:

conda run -n litellm pip show litellm | grep Version

# Must show: Version: 1.83.0

Step 2 - Create the config

This tells LiteLLM how to map Claude Code’s model requests to ChatGPT models.

cat > ~/.litellm/chatgpt-config.yaml << 'EOF'

model_list:

- model_name: gpt-5.6-sol

model_info:

mode: responses

litellm_params:

model: chatgpt/gpt-5.6-sol

supports_system_message: false

- model_name: gpt-5.5

model_info:

mode: responses

litellm_params:

model: chatgpt/gpt-5.5

supports_system_message: false

- model_name: gpt-5.3-codex

model_info:

mode: responses

litellm_params:

model: chatgpt/gpt-5.3-codex

supports_system_message: false

- model_name: gpt-5.3-codex-spark

model_info:

mode: responses

litellm_params:

model: chatgpt/gpt-5.3-codex-spark

supports_system_message: false

litellm_settings:

master_key: os.environ/LITELLM_MASTER_KEY

drop_params: true

EOF

The model_name values are what Claude Code asks for. The model values under litellm_params are what actually gets called via your ChatGPT subscription. supports_system_message: false tells LiteLLM to convert system messages into user messages, since the ChatGPT subscription backend rejects system messages outright.

drop_params: true silently drops any Claude-specific parameters (like context_management) that the ChatGPT backend doesn’t understand.

Claude Code asks forLiteLLM routes toBest for
gpt-5.6-solchatgpt/gpt-5.6-solDeep reasoning, complex problems
gpt-5.5chatgpt/gpt-5.5Default coding model
gpt-5.3-codexchatgpt/gpt-5.3-codexCode generation
gpt-5.3-codex-sparkchatgpt/gpt-5.3-codex-sparkFast code tasks

Step 3 - Patch a bug in LiteLLM 1.83.0

Claude Code sends message content in the Anthropic format - arrays of content blocks like [{"type": "text", "text": "..."}]. LiteLLM’s system message converter assumes plain strings and crashes when it tries to concatenate them.

Find the file to patch:

conda activate litellm

FACTORY=$(python -c "import litellm; print(litellm.__path__[0])")/litellm_core_utils/prompt_templates/factory.py

echo "$FACTORY"

conda deactivate

Back it up, then open it:

cp "$FACTORY" "$FACTORY.bak"

nano "$FACTORY"

Search for the map_system_message_pt function and replace it entirely with:

def map_system_message_pt(messages: list) -> list:

"""

Convert system messages to user messages.

Handles both string content and Anthropic-style list content blocks.

"""

def _to_str(content):

if isinstance(content, str):

return content

if isinstance(content, list):

return " ".join(

block.get("text", "") for block in content

if isinstance(block, dict) and block.get("type") == "text"

)

return str(content)

new_messages = []

for m in messages:

if m["role"] == "system":

sys_text = _to_str(m["content"])

if new_messages and new_messages[-1]["role"] == "user":

prev_text = _to_str(new_messages[-1]["content"])

new_messages[-1]["content"] = sys_text + " " + prev_text

else:

new_messages.append({"role": "user", "content": sys_text})

else:

new_messages.append(m)

return new_messages

Save and close. No reinstall needed - Python picks it up on next import.

Step 4 - Generate a local auth key

This key secures the connection between Claude Code and the proxy. It never leaves your machine.

echo "sk-local-$(openssl rand -hex 16)" > ~/.litellm/master-key.txt

chmod 600 ~/.litellm/master-key.txt

cat ~/.litellm/master-key.txt

Step 5 - Create the launcher scripts

Two scripts: one to start the proxy, one to launch Claude Code through it.

The proxy launcher

This version is designed to run silently in the background (either manually or under launchd). It does not print a startup banner to the terminal.

cat > ~/.litellm/start-proxy.sh << 'SCRIPT'

#!/bin/bash

set -euo pipefail

# Robustly find conda across common install locations.

FOUND=""

for p in "$HOME/miniforge3" "$HOME/miniconda3" "$HOME/anaconda3" "$HOME/.conda"; do

if [ -x "$p/bin/conda" ]; then

FOUND="$p"

break

fi

done

if [ -z "$FOUND" ]; then

echo "Error: conda not found in any standard location." >&2

exit 1

fi

eval "$("$FOUND/bin/conda" shell.bash hook)"

conda activate litellm

export LITELLM_MASTER_KEY="$(cat ~/.litellm/master-key.txt)"

exec litellm --config ~/.litellm/chatgpt-config.yaml --port 4000

SCRIPT

chmod +x ~/.litellm/start-proxy.sh

The claude-codex command

This script checks that the proxy is healthy, starts it silently in the background if not, and then launches Claude Code. If the proxy fails, it surfaces the last lines of the LiteLLM log so you can see the real error.

mkdir -p ~/bin

cat > ~/bin/claude-codex << 'SCRIPT'

#!/bin/bash

set -euo pipefail

#

# Launch Claude Code powered by ChatGPT subscription via LiteLLM.

# Your normal `claude` command is completely unaffected.

#

MASTER_KEY="$(cat ~/.litellm/master-key.txt 2>/dev/null)"

if [ -z "$MASTER_KEY" ]; then

echo "Error: No master key found at ~/.litellm/master-key.txt"

echo "Run the setup steps first."

exit 1

fi

LITELLM_DIR="$HOME/.litellm"

PROXY_LOG="$LITELLM_DIR/proxy.log"

PROXY_ERR="$LITELLM_DIR/proxy.err"

PROXY_URL="http://127.0.0.1:4000"

HEALTH_AUTH=( -H "Authorization: Bearer $MASTER_KEY" )

show_proxy_logs() {

local lines="${1:-50}"

echo ""

echo "--- LiteLLM recent stderr (last $lines lines) ---"

if [ -s "$PROXY_ERR" ]; then

tail -n "$lines" "$PROXY_ERR"

else

echo "(no stderr log)"

fi

echo ""

echo "--- LiteLLM recent stdout (last $lines lines) ---"

if [ -s "$PROXY_LOG" ]; then

tail -n "$lines" "$PROXY_LOG"

else

echo "(no stdout log)"

fi

}

proxy_healthy() {

curl -s "${HEALTH_AUTH[@]}" "$PROXY_URL/health" > /dev/null 2>&1

}

launchd_proxy_enabled() {

launchctl list 2>/dev/null | grep -q "^.*com\.litellm\.proxy$"

}

ensure_single_proxy() {

local pids

pids="$(pgrep -f "litellm --config $LITELLM_DIR/chatgpt-config.yaml" || true)"

if [ -n "$pids" ]; then

if proxy_healthy; then

return 0

fi

if launchd_proxy_enabled; then

echo "LiteLLM proxy (launchd) is not healthy; restarting via launchctl..."

launchctl stop com.litellm.proxy 2>/dev/null || true

launchctl start com.litellm.proxy 2>/dev/null || true

return 0

fi

echo "Stale LiteLLM proxy detected; restarting..."

echo "$pids" | xargs kill -9 2>/dev/null || true

sleep 1

fi

}

start_proxy() {

if launchd_proxy_enabled; then

return 0

fi

mkdir -p "$LITELLM_DIR"

[ -f "$PROXY_LOG" ] && mv "$PROXY_LOG" "$PROXY_LOG.prev"

[ -f "$PROXY_ERR" ] && mv "$PROXY_ERR" "$PROXY_ERR.prev"

nohup "$LITELLM_DIR/start-proxy.sh" >> "$PROXY_LOG" 2>> "$PROXY_ERR" &

disown

}

wait_for_proxy() {

for i in $(seq 1 30); do

sleep 1

if proxy_healthy; then

return 0

fi

done

return 1

}

# --- Main flow --------------------------------------------------------------

ensure_single_proxy

if ! proxy_healthy; then

start_proxy

if launchd_proxy_enabled; then

echo "Waiting for launchd-managed LiteLLM proxy..."

fi

if ! wait_for_proxy; then

echo "Error: LiteLLM proxy failed to start."

show_proxy_logs 50

exit 1

fi

fi

if ! curl -s "${HEALTH_AUTH[@]}" "$PROXY_URL/models" > /dev/null 2>&1; then

echo "Error: LiteLLM proxy is running but /models is unreachable."

show_proxy_logs 50

exit 1

fi

export ANTHROPIC_BASE_URL="$PROXY_URL"

export ANTHROPIC_AUTH_TOKEN="$MASTER_KEY"

export ANTHROPIC_MODEL="gpt-5.5"

export ANTHROPIC_DEFAULT_SONNET_MODEL="gpt-5.5"

export ANTHROPIC_DEFAULT_HAIKU_MODEL="gpt-5.3-codex-spark"

export ANTHROPIC_DEFAULT_OPUS_MODEL="gpt-5.6-sol"

export ANTHROPIC_DEFAULT_FABLE_MODEL="gpt-5.6-sol"

exec claude "$@"

SCRIPT

chmod +x ~/bin/claude-codex

Add ~/bin to your PATH:

echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc

source ~/.bashrc

Step 6 - First run and OAuth login

Start the proxy manually for the first run so you can see the OAuth device code:

~/.litellm/start-proxy.sh > ~/.litellm/proxy.log 2> ~/.litellm/proxy.err &

Then launch Claude Code:

On the first request, check the proxy log for the device code:

tail -f ~/.litellm/proxy.err

You’ll see something like:

Sign in with ChatGPT using device code:

1) Visit https://auth0.openai.com/codex/device

2) Enter code: XXXX-XXXX

ChatGPT may prompt you to enable device authentication in your account settings before this works. Follow whatever instructions they show before proceeding.

Open the URL, log in with your OpenAI account, enter the code. Tokens are saved locally - you won’t need to repeat this unless they expire.

After the first successful auth, stop the manual proxy and use the launchd service from Step 7.

Step 7 - Auto-start LiteLLM on login (macOS)

Tired of keeping a terminal tab open for the proxy? On macOS, a launchd agent starts it on login, restarts it if it crashes, and logs output to ~/.litellm/proxy.log / .err. No second terminal needed.

Create the plist:

cat > ~/Library/LaunchAgents/com.litellm.proxy.plist << 'EOF'

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"

"http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>

<key>Label</key>

<string>com.litellm.proxy</string>

<key>ProgramArguments</key>

<array>

<string>/bin/bash</string>

<string>-c</string>

<string>exec ~/.litellm/start-proxy.sh >> ~/.litellm/proxy.log 2>> ~/.litellm/proxy.err</string>

</array>

<key>RunAtLoad</key>

<true/>

<key>KeepAlive</key>

<dict>

<key>SuccessfulExit</key>

<false/>

<key>Crashed</key>

<true/>

</dict>

<key>StandardOutPath</key>

<string>/dev/null</string>

<key>StandardErrorPath</key>

<string>/dev/null</string>

<key>EnvironmentVariables</key>

<dict>

<key>HOME</key>

<string>/Users/prabal</string>

</dict>

</dict>

</plist>

EOF

chmod 644 ~/Library/LaunchAgents/com.litellm.proxy.plist

Load it:

launchctl load ~/Library/LaunchAgents/com.litellm.proxy.plist

That’s it. It starts now, and on every future login. Manage it with:

launchctl list | grep litellm # check if running

launchctl stop com.litellm.proxy # stop

launchctl start com.litellm.proxy # start

launchctl unload ~/Library/LaunchAgents/com.litellm.proxy.plist # remove permanently

tail -f ~/.litellm/proxy.log # check logs

tail -f ~/.litellm/proxy.err # check errors

Daily usage

claude-codex # ChatGPT-powered Claude Code (default gpt-5.5)

claude # normal Claude Code (unchanged)

Switch models on the fly:

claude-codex --model gpt-5.3-codex # code-heavy tasks

claude-codex --model gpt-5.6-sol # deep reasoning

File locations

FilePurpose
~/.litellm/chatgpt-config.yamlModel routing config
~/.litellm/master-key.txtLocal auth key
~/.litellm/start-proxy.shProxy launcher
~/.litellm/proxy.logProxy stdout log
~/.litellm/proxy.errProxy stderr log
~/bin/claude-codexThe claude-codex command
~/Library/LaunchAgents/com.litellm.proxy.plistmacOS auto-start service
conda env: litellmPython env with LiteLLM
~/.claude/settings.jsonNot modified

Available ChatGPT subscription models

Model stringBest forTier note
chatgpt/gpt-5.6-solHardest problems, deep reasoningPro/Max likely required
chatgpt/gpt-5.5Latest GPT, strong all-rounderWorks on Plus
chatgpt/gpt-5.3-codexCode generationWorks on Plus
chatgpt/gpt-5.3-codex-sparkFast code tasksWorks on Plus

These names change. If ChatGPT exposes a different model string in your dropdown, edit both the model_name and model fields in ~/.litellm/chatgpt-config.yaml and restart the proxy.

Troubleshooting

“LiteLLM proxy failed to start” or show_proxy_logs prints errors

Run:

tail -50 ~/.litellm/proxy.err

tail -50 ~/.litellm/proxy.log

That will show the actual LiteLLM error instead of terminal spam.


“No api key passed in” / 401 on /health

The health check needs the master key. The claude-codex script in Step 5 sends Authorization: Bearer $MASTER_KEY. If you see 401, you’re probably using an older script that doesn’t send auth. Copy the updated script from Step 5.


litellm.UnsupportedParamsError: chatgpt does not support parameters: ['context_management']

Add drop_params: true under litellm_settings in ~/.litellm/chatgpt-config.yaml. See Step 2.


litellm.BadRequestError: The 'gpt-5.6-sol-pro' model is not supported when using Codex with a ChatGPT account

Drop the -pro suffix. Use chatgpt/gpt-5.6-sol as the backend, not chatgpt/gpt-5.6-sol-pro.


ConnectionRefused retries in Claude Code

The proxy isn’t running or isn’t fully initialized. The updated claude-codex script will start it automatically and wait up to 30 seconds. If it still fails, check ~/.litellm/proxy.err.

If you use the launchd service, verify it:

launchctl list | grep litellm

launchctl stop com.litellm.proxy

launchctl start com.litellm.proxy


“Model not found”

Claude Code is requesting a model name the proxy doesn’t recognize. Check the exact names returned by:

curl -s -H "Authorization: Bearer $(cat ~/.litellm/master-key.txt)" \

http://127.0.0.1:4000/models | grep '"id"'

Then add any missing names to ~/.litellm/chatgpt-config.yaml and restart the proxy.


OAuth token expired

The proxy log will re-prompt with a new device code. Follow the browser auth flow again.


Rate limits

Your subscription tier determines limits. Plus ($20/mo) has lower ceilings; Pro and Max are better for heavy agentic use.


“System messages are not allowed”

You’re missing supports_system_message: false under litellm_params in your config. See Step 2.


“can only concatenate list (not str) to list”

You haven’t applied the patch from Step 3. Claude Code sends content as Anthropic-format arrays and LiteLLM’s merger assumes plain strings.


Verify LiteLLM is safe:

conda run -n litellm pip show litellm | grep Version # must show exactly 1.83.0

find ~ -name "litellm_init.pth" 2>/dev/null # should find nothing

ls ~/.config/sysmon/ 2>/dev/null # should not exist