skip to content
Voice AI @ AI Tinkerers Waterloo, July 30RSVP →
Home Prabal
← Posts

Run Claude Code with Your ChatGPT Subscription

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

Terminal window
conda create -n litellm python=3.12 -y
conda activate litellm
pip install "litellm[proxy]==1.83.0"
conda deactivate

Verify:

Terminal window
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.

Terminal window
mkdir -p ~/.litellm
Terminal window
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:

Terminal window
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:

Terminal window
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.

Terminal window
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:

Terminal window
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:

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

Then launch Claude Code:

Terminal window
claude-codex

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

Terminal window
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:

Terminal window
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:

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

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

Terminal window
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

Terminal window
claude-codex # ChatGPT-powered Claude Code (default gpt-5.5)
claude # normal Claude Code (unchanged)

Switch models on the fly:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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