Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"chat.tools.terminal.autoApprove": {
"make": true
}
}
66 changes: 38 additions & 28 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

.PHONY: help setup install-claude start stop clean test verify claude-enable claude-disable claude-status list-models list-models-enabled

ifeq ($(OS),Windows_NT)
NPM_CHECK = where npm >nul 2>&1
else
NPM_CHECK = command -v npm >/dev/null 2>&1
endif

# Default target
help:
@echo "Available targets:"
Expand All @@ -16,43 +22,45 @@ help:
@echo " make list-models - List all GitHub Copilot models"
@echo " make list-models-enabled - List only enabled GitHub Copilot models"

# Set up environment
# Set up environment (delegates to scripts/setup.py which uses venv)
setup:
@echo "Setting up environment..."
@mkdir -p scripts
@python3 -m venv venv
@./venv/bin/pip install -r requirements.txt
@if [ ! -f .env ]; then \
echo "Generating .env file..."; \
python3 generate_env.py; \
else \
echo "✓ .env file already exists, skipping generation"; \
fi
@echo "✓ Setup complete"
@python3 scripts/setup.py || python scripts/setup.py
@echo "Setup complete OK"

# Install Claude Code desktop application
ifeq ($(OS),Windows_NT)
install-claude:
@echo "Installing Claude Code desktop application..."
@where npm >nul 2>&1 && (echo "Installing Claude Code via npm..." & npm install -g @anthropic-ai/claude-code & echo "Claude Code installed successfully" & echo "Note: run 'make claude-enable' to configure it") || (echo "npm not found. Please install Node.js and npm first:" & echo " https://nodejs.org/" & echo " Then run: npm install -g @anthropic-ai/claude-code")
else
install-claude:
@echo "Installing Claude Code desktop application..."
@if command -v npm >/dev/null 2>&1; then \
echo "Installing Claude Code via npm..."; \
npm install -g @anthropic-ai/claude-code && echo "Claude Code installed successfully" && \
echo "💡 You can now run 'make claude-enable' to configure it"; \
echo "Installing Claude Code via npm..."; \
npm install -g @anthropic-ai/claude-code && echo "Claude Code installed successfully" && \
echo "Note: run 'make claude-enable' to configure it"; \
else \
echo "❌ npm not found. Please install Node.js and npm first:"; \
echo " https://nodejs.org/"; \
echo " Then run: npm install -g @anthropic-ai/claude-code"; \
fi
endif

# Start LiteLLM proxy
start:
@echo "Starting LiteLLM proxy..."
@source venv/bin/activate && litellm --config copilot-config.yaml --port 4444
@python3 scripts/start.py || python scripts/start.py

# Stop running processes
stop:
@echo "Stopping processes..."
ifeq ($(OS),Windows_NT)
@powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $$_.CommandLine -match 'litellm' } | ForEach-Object { Stop-Process -Id $$_.ProcessId -Force }" || echo "No matching processes found"
else
@pkill -f litellm 2>/dev/null || true
@echo "✓ Processes stopped"
endif
@echo "Processes stopped"

# Test proxy connection
test:
Expand All @@ -62,41 +70,42 @@ test:
-H "Authorization: Bearer $$(grep LITELLM_MASTER_KEY .env | cut -d'=' -f2 | tr -d '\"')" \
-d '{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}'
@echo ""
@echo "Test completed successfully!"
@echo "Test completed successfully!"

# Configure Claude Code to use local proxy
claude-enable:
@echo "Configuring Claude Code to use local proxy..."
@if [ ! -f .env ]; then echo "❌ .env file not found. Run 'make setup' first."; exit 1; fi
@MASTER_KEY=$$(grep LITELLM_MASTER_KEY .env | cut -d'=' -f2 | tr -d '"'); \
if [ -z "$$MASTER_KEY" ]; then echo "❌ LITELLM_MASTER_KEY not found in .env"; exit 1; fi; \
if [ -f ~/.claude/settings.json ]; then \
cp ~/.claude/settings.json ~/.claude/settings.json.backup.$$(date +%Y%m%d_%H%M%S); \
echo "📁 Backed up existing settings to ~/.claude/settings.json.backup.$$(date +%Y%m%d_%H%M%S)"; \
fi; \
python3 scripts/claude_enable.py "$$MASTER_KEY"
@echo "✅ Claude Code configured to use local proxy"
@echo "💡 Make sure to run 'make start' to start the LiteLLM proxy server"
@python3 scripts/claude_enable_wrapper.py || python scripts/claude_enable_wrapper.py
@echo "Claude Code configured to use local proxy"
@echo "Make sure to run 'make start' to start the LiteLLM proxy server"

# Restore Claude Code to default settings
claude-disable:
@echo "Restoring Claude Code to default settings..."

ifeq ($(OS),Windows_NT)
@powershell -NoProfile -Command "$$dir = Join-Path $$env:USERPROFILE '.claude'; if (Test-Path (Join-Path $$dir 'settings.json')) { Copy-Item (Join-Path $$dir 'settings.json') (Join-Path $$dir ('settings.json.proxy_backup.' + (Get-Date -Format 'yyyyMMdd_HHmmss'))); Write-Host 'Backed up proxy settings to' (Join-Path $$dir ('settings.json.proxy_backup.' + (Get-Date -Format 'yyyyMMdd_HHmmss')) ) } ; $$backups = Get-ChildItem $$dir -Filter 'settings.json.backup.*' -ErrorAction SilentlyContinue; if (-not $$backups) { $$backups = Get-ChildItem $$dir -Filter 'settings.json.proxy_backup.*' -ErrorAction SilentlyContinue }; if ($$backups) { $$latest = $$backups | Sort-Object LastWriteTime -Descending | Select-Object -First 1; Copy-Item $$latest.FullName (Join-Path $$dir 'settings.json'); Write-Host 'Restored settings from' $$latest.FullName } else { try { & python3 scripts/claude_disable.py } catch { & python scripts/claude_disable.py } }"
else
@if [ -f ~/.claude/settings.json ]; then \
cp ~/.claude/settings.json ~/.claude/settings.json.proxy_backup.$$(date +%Y%m%d_%H%M%S); \
echo "📁 Backed up proxy settings to ~/.claude/settings.json.proxy_backup.$$(date +%Y%m%d_%H%M%S)"; \
fi
fi; \
@if ls ~/.claude/settings.json.backup.* >/dev/null 2>&1; then \
LATEST_BACKUP=$$(ls -t ~/.claude/settings.json.backup.* | head -1); \
cp "$$LATEST_BACKUP" ~/.claude/settings.json; \
echo "✅ Restored settings from $$LATEST_BACKUP"; \
else \
python3 scripts/claude_disable.py; \
fi
endif

# Show current Claude Code configuration
claude-status:
@echo "Current Claude Code configuration:"
@echo "=================================="
ifeq ($(OS),Windows_NT)
@powershell -NoProfile -Command "if (Test-Path -Path (Join-Path $$env:USERPROFILE '.claude\\settings.json')) { Write-Host 'Settings file:' (Join-Path $$env:USERPROFILE '.claude\\settings.json'); Write-Host ''; try { Get-Content (Join-Path $$env:USERPROFILE '.claude\\settings.json') | ConvertFrom-Json | ConvertTo-Json -Depth 6 } catch { Get-Content (Join-Path $$env:USERPROFILE '.claude\\settings.json') }; Write-Host ''; $$content = Get-Content (Join-Path $$env:USERPROFILE '.claude\\settings.json') -Raw; if ($$content -match 'localhost:4444') { Write-Host 'Status: Using local proxy'; try { Invoke-WebRequest -Uri 'http://localhost:4444/health' -UseBasicParsing -TimeoutSec 2 | Out-Null; Write-Host 'Proxy server: Running' } catch { Write-Host 'Proxy server: Not running (run ''make start'')' } } else { Write-Host 'Status: Using default Anthropic servers' } } else { Write-Host 'No settings file found - using Claude Code defaults'; Write-Host 'Status: Using default Anthropic servers' }"
else
@if [ -f ~/.claude/settings.json ]; then \
echo "📄 Settings file: ~/.claude/settings.json"; \
echo ""; \
Expand All @@ -116,6 +125,7 @@ claude-status:
echo "📄 No settings file found - using Claude Code defaults"; \
echo "🌐 Status: Using default Anthropic servers"; \
fi
endif

# List available GitHub Copilot models
list-models:
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ The proxy exposes these models to Claude Code:
| Claude Code Model | Maps to GitHub Copilot |
|-------------------|----------------------------------|
| `claude-sonnet-4` | `github_copilot/claude-sonnet-4` |
| `gpt-4` | `github_copilot/gpt-4` |
| `gpt-5-mini` | `github_copilot/gpt-5-mini` |
| `gpt-4.1` | `github_copilot/gpt-4.1` |
| `gpt-4o` | `github_copilot/gpt-4o` |
| `grok-fast-1` | `github_copilot/grok-fast-1` |
| `gpt-4` | `github_copilot/gpt-4` |

## Additional Commands

Expand Down
19 changes: 19 additions & 0 deletions copilot-config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
litellm_settings:
drop_params: true

model_list:
- model_name: gpt-4
litellm_params:
Expand All @@ -7,3 +10,19 @@ model_list:
litellm_params:
model: github_copilot/claude-sonnet-4
extra_headers: {"Editor-Version": "vscode/1.85.1", "Copilot-Integration-Id": "vscode-chat"}
- model_name: gpt-5-mini
litellm_params:
model: github_copilot/gpt-5-mini
extra_headers: {"Editor-Version": "vscode/1.85.1", "Copilot-Integration-Id": "vscode-chat"}
- model_name: gpt-4.1
litellm_params:
model: github_copilot/gpt-4.1
extra_headers: {"Editor-Version": "vscode/1.85.1", "Copilot-Integration-Id": "vscode-chat"}
- model_name: gpt-4o
litellm_params:
model: github_copilot/gpt-4o
extra_headers: {"Editor-Version": "vscode/1.85.1", "Copilot-Integration-Id": "vscode-chat"}
- model_name: grok-fast-1
litellm_params:
model: github_copilot/grok-fast-1
extra_headers: {"Editor-Version": "vscode/1.85.1", "Copilot-Integration-Id": "vscode-chat"}
49 changes: 49 additions & 0 deletions scripts/claude_enable_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import time
from pathlib import Path
import subprocess

def fail(msg, code=1):
print(msg, file=sys.stderr)
sys.exit(code)

env_path = Path('.') / '.env'
if not env_path.exists():
fail("❌ .env file not found. Run 'make setup' first.")

master_key = None
for line in env_path.read_text().splitlines():
if line.strip().startswith('LITELLM_MASTER_KEY'):
parts = line.split('=', 1)
if len(parts) > 1:
master_key = parts[1].strip().strip('"').strip("'")
break

if not master_key:
fail("❌ LITELLM_MASTER_KEY not found in .env")

claude_dir = Path.home() / '.claude'
settings = claude_dir / 'settings.json'
if settings.exists():
timestamp = time.strftime("%Y%m%d_%H%M%S")
backup = claude_dir / f"settings.json.backup.{timestamp}"
try:
backup.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(settings), str(backup))
print(f"📁 Backed up existing settings to {backup}")
except Exception as e:
fail(f"❌ Failed to backup settings: {e}")

script = Path('scripts') / 'claude_enable.py'
if not script.exists():
fail('❌ scripts/claude_enable.py not found')

try:
subprocess.check_call([sys.executable, str(script), master_key])
except subprocess.CalledProcessError as e:
fail(f"❌ scripts/claude_enable.py failed: {e}", code=e.returncode)

print("✅ claude_enable completed")
76 changes: 76 additions & 0 deletions scripts/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
import sys
import shutil
import subprocess
import os
from pathlib import Path

def fail(msg, code=1):
print(msg, file=sys.stderr)
sys.exit(code)

def run(cmd):
print('> ' + ' '.join(cmd))
subprocess.check_call(cmd)

root = Path.cwd()

# Ensure scripts directory exists
try:
(root / 'scripts').mkdir(parents=True, exist_ok=True)
except Exception as e:
fail(f"Failed to create scripts directory: {e}")

# Find a Python executable
py = shutil.which('python3') or shutil.which('python') or sys.executable
if not py:
fail('Python not found on PATH')

# Create virtual environment if needed
venv_dir = root / 'venv'
if venv_dir.exists():
if os.name == 'nt':
venv_python = venv_dir / 'Scripts' / 'python.exe'
else:
venv_python = venv_dir / 'bin' / 'python'
if not venv_python.exists():
print('Existing venv incomplete; attempting to recreate')
try:
shutil.rmtree(str(venv_dir))
except Exception:
fail('Cannot remove existing venv; check permissions')
try:
run([py, '-m', 'venv', 'venv'])
except subprocess.CalledProcessError as e:
fail(f'Failed to create virtualenv: {e}')
else:
try:
run([py, '-m', 'venv', 'venv'])
except subprocess.CalledProcessError as e:
fail(f'Failed to create virtualenv: {e}')

# Determine venv python
if os.name == 'nt':
venv_python = venv_dir / 'Scripts' / 'python.exe'
else:
venv_python = venv_dir / 'bin' / 'python'

if not venv_python.exists():
# fallback to provided python
venv_python = Path(py)

# Install requirements
try:
run([str(venv_python), '-m', 'pip', 'install', '-r', 'requirements.txt'])
except subprocess.CalledProcessError as e:
fail(f'Failed to install requirements: {e}')

# Generate .env if missing
env_file = root / '.env'
if not env_file.exists():
try:
run([py, 'generate_env.py'])
except subprocess.CalledProcessError as e:
fail(f'Failed to generate .env: {e}')
else:
print('✓ .env file already exists, skipping generation')
73 changes: 73 additions & 0 deletions scripts/start.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
import os
import sys
import subprocess

VENV_DIR = os.path.join(os.getcwd(), 'venv')

def venv_python():
if os.name == 'nt':
return os.path.join(VENV_DIR, 'Scripts', 'python.exe')
return os.path.join(VENV_DIR, 'bin', 'python')

def run_litellm(vpython):
args = ['--config', 'copilot-config.yaml', '--port', '4444']

# Try running the litellm console script in the venv (preferred)
candidates = []
if os.name == 'nt':
candidates = [os.path.join(VENV_DIR, 'Scripts', 'litellm.exe'),
os.path.join(VENV_DIR, 'Scripts', 'litellm')]
else:
candidates = [os.path.join(VENV_DIR, 'bin', 'litellm')]

for exe in candidates:
if os.path.exists(exe):
cmd = [exe] + args
print('Running:', ' '.join(cmd))
try:
subprocess.check_call(cmd)
return
except subprocess.CalledProcessError as e:
print('litellm exited with', e.returncode)
sys.exit(e.returncode)

# If no console script was found, try running via the venv python as a module
cmd = [vpython, '-m', 'litellm'] + args
print('Attempting fallback:', ' '.join(cmd))
try:
subprocess.check_call(cmd)
return
except subprocess.CalledProcessError as e:
print('Fallback -m litellm failed with', e.returncode)

# Try a secondary fallback: common submodule entrypoint
cmd2 = [vpython, '-m', 'litellm.cli'] + args
print('Attempting secondary fallback:', ' '.join(cmd2))
try:
subprocess.check_call(cmd2)
return
except subprocess.CalledProcessError as e:
print('Secondary fallback litellm.cli failed with', e.returncode)

# Final attempt: try system `litellm` on PATH
try:
print('Attempting to run `litellm` from PATH')
subprocess.check_call(['litellm'] + args)
return
except Exception:
print('Error: could not start litellm. Ensure it is installed in the venv or on PATH.')
sys.exit(3)

if __name__ == '__main__':
if not os.path.isdir(VENV_DIR):
print('Error: virtual environment not found (expected at', VENV_DIR + '). Run "make setup" first.')
sys.exit(1)

vpython = venv_python()
if not os.path.exists(vpython):
print('Warning: venv python not found at', vpython)
print('Falling back to system python. Ensure litellm is installed in your environment.')
vpython = sys.executable

run_litellm(vpython)